網路流最大流、最小割學習筆記
網路流
網路流 \(G=(V,E)\) 是一個有向圖,其中每條邊 \((u, v)\) 均有一個非負的容量值,記為 \(c(u, v)\geq0\)
如果 \((u, v)\notin E\),則可以規定 \(c(u, v)=0\)
網路流中有兩個特殊的頂點,即源點 \(S\) 和匯點 \(T\) ,源點可以提供無限的流量,而匯點可以接受無限的流量
與網路流相關的一個概念是流
設 \(S\) 為網路的源點, \(T\) 為匯點,那麼 \(G\) 的流是一個函式 \(f:V×V →R\),滿足以下性質:
容量限制: \(\forall u,v∈V\),滿足 \(f(u, v) \leq c(u, v)\);
反對稱性: \(\forall u,v∈V\),滿足 \(f(u, v) = - f(v, u)\);
流守恆性: \(\forall u∈V-{S, T}\),滿足\(\sum_{v∈V}f(u,v)=0\)。
流 \(f\) 的值定義為 \(|f|=\sum_{v\in V}f(s,v)\)
通俗地講,我們想象一下自來水廠到你家的水管網是一個複雜的有向圖,每一節水管都有一個最大承載流量。
自來水廠不放水,你家就斷水了。
但是就算自來水廠拼命地往管網裡面注水,你家收到的水流量也是上限,畢竟每根水管承載量有限。
你想知道你能夠拿到多少水,這就是一種網路流問題。
以上摘自這篇部落格
最大流
模板
我們有一張圖,要求從源點流向匯點的最大流量(可以有很多條路到達匯點),就是我們的最大流問題。
最大流的演算法有很多,這裡介紹的是 \(dinic\) 演算法
演算法的流程是從源點開始進行搜尋,只要邊權不為零就繼續遞迴,直到到達匯點為止
在搜尋的過程中記錄當前路徑上經過的最小邊權,因為流量會受到最小邊權的限制,回溯時把經過的邊的邊權都減去這個最小值
但是我們的選擇不一定是最優的,所以對於每一條邊,我們多建一條反向邊
反向邊的初始邊權為 \(0\)
在正向邊中減去的權值我們在反向邊上加回來,相當於提供了一次反悔的機會
整個過程實際上就是尋找增廣路的過程,當我們找不到增廣路時演算法就結束了
為了讓演算法更加高效,我們把圖人為地進行分層
源點的深度為 \(1\),從源點進行 \(bfs\),更新能到達的每一個點的深度,把深度相同的點分到一層
在 \(dfs\) 尋找增廣路時,我們規定只能從當前一層向下一層尋找,這樣可以在一次遞迴中儘可能多得找出增廣路
這就是 \(Dinic\) 演算法的大體框架
時間複雜度為 \(O(n^2 m)\),但是基本跑不滿
為了讓程式碼跑得更快,可以使用當前弧優化和無用點優化
程式碼
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=2e5+5;
int h[maxn],h2[maxn],tot=2;
struct asd{
int to,nxt,val;
}b[maxn];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int q[maxn],dep[maxn],head,tail,n,m,s,t;
//bfs對原圖進行分層
bool bfs(){
for(rg int i=1;i<=n;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(dep[u] || b[i].val==0) continue;
//如果已經訪問過或者邊權為0,continue
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
//如果不能到達匯點,返回0,否則返回1
return dep[t];
}
//dfs尋找增廣路
long long dfs(int now,long long ac1){
if(now==t) return ac1;
rg long long ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
h[now]=i;
//優化一:當前弧優化,已經更新過並且邊權變為0的節點下次不再訪問
if(b[i].val && dep[u]==dep[now]+1){
rg long long nans=dfs(u,std::min(ac1,(long long)b[i].val));
b[i].val-=nans;
b[i^1].val+=nans;
ac2+=nans;
ac1-=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
//優化二:無用點優化,如果這個點不能對答案產生貢獻下次不再訪問
return ac2;
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read(),s=read(),t=read();
rg int aa,bb,cc;
for(rg int i=1;i<=m;i++){
aa=read(),bb=read(),cc=read();
ad(aa,bb,cc);
ad(bb,aa,0);
}
for(rg int i=1;i<=n;i++){
h2[i]=h[i];
}
rg long long ans=0;
while(bfs()){
ans+=dfs(s,1e18);
}
printf("%lld\n",ans);
return 0;
}
例題一、洛谷P2472 [SCOI2007] 蜥蜴
題目描述
分析
網路流的題難點還是在建圖上
這道題用到了建圖時的一個技巧:拆點
我們把一個石柱拆成兩個點,一代表入口,另一個代表出口
首先,我們從超級源點向有蜥蜴的石柱的入口建一條邊權為 \(1\) 的邊,代表有一條蜥蜴可以經過這個石柱
然後,我們列舉所有的石柱,從當前石柱的出口向它能到達的所有石柱的入口建一條邊權為無窮大的邊
為了滿足石柱高度的限制,我們還要從石柱的入口向自己的出口建一條邊權為石柱高度的邊,代表最多能從這裡跳出的蜥蜴的個數
最後,我們再從所有石柱的出口向超級匯點連一條權值為無窮大的邊
這道題拆點的目的就是為了便於處理石柱高度的限制
一個石柱跳的次數是有限的,如果我們不去拆點,這個限制是不好表示的
而我們把它人為地拆開之後,我們不需要去管它向外連了多少條邊,我們只需要限制她經過這個石柱多少次就可以了
建完圖後跑一次最大流,求出能逃走的蜥蜴最大數量
用蜥蜴的總數減去這個數量得到的就是答案
程式碼
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5,maxm=1e6+5,maxk=25;
int h[maxn],tot=2,h2[maxn],n,m;
struct asd{
int to,nxt,val;
}b[maxm];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].val=cc;
b[tot].nxt=h[aa];
h[aa]=tot++;
}
char ss[maxk][maxk];
int a[maxk][maxk],dep[maxn],mmax,q[maxn],head,tail,s,t,d,totcnt;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
h[i]=h2[i];
dep[i]=0;
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(int now,int ac1){
if(now==t) return ac1;
rg int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
h[now]=i;
if(dep[u]==dep[now]+1 && b[i].val){
rg int nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int js(int i,int j){
return (i-1)*m+j;
}
double jl(int ax,int ay,int bx,int by){
return (double)sqrt((ax-bx)*(ax-bx)+(ay-by)*(ay-by));
}
bool pd(int i,int j){
if(i<=d || j<=d) return 1;
if(i+d>n || j+d>m) return 1;
return 0;
}
int main(){
n=read(),m=read(),d=read();
memset(h,-1,sizeof(h));
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
scanf("%1d",&a[i][j]);
}
}
for(rg int i=1;i<=n;i++){
scanf("%s",ss[i]+1);
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(ss[i][j]=='L'){
totcnt++;
ad(0,js(i,j),1);
ad(js(i,j),0,0);
}
if(a[i][j]!=0){
ad(js(i,j),js(i,j)+n*m,a[i][j]);
ad(js(i,j)+n*m,js(i,j),0);
for(rg int o=1;o<=n;o++){
for(rg int p=1;p<=m;p++){
if(a[o][p]!=0 && jl(i,j,o,p)<=(double)d && (o!=i || p!=j)){
ad(js(i,j)+n*m,js(o,p),0x3f3f3f3f);
ad(js(o,p),js(i,j)+n*m,0);
}
}
}
}
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(a[i][j]!=0 && pd(i,j)){
ad(js(i,j)+n*m,n*m*2+1,0x3f3f3f3f);
ad(n*m*2+1,js(i,j)+n*m,0);
}
}
}
s=0,t=n*m*2+1;
mmax=n*m*2+1;
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg int ans=0;
while(bfs()) ans+=dfs(s,0x3f3f3f3f);
printf("%d\n",totcnt-ans);
return 0;
}
例題二、洛谷P3324 [SDOI2015]星際戰爭
題目描述
分析
我們知道的只是單位時間內鐳射武器對裝甲的傷害
時間不確定,傷害就不確定,也就無法建圖
所以我們二分時間,把這個問題轉化為一個判定性問題
具體的建圖就很簡單了
\(1\)、從源點向所有鐳射武器建一條權值為鐳射武器總傷害的邊
\(2\)、從鐳射武器向所有它能攻擊的機器人建一條權值為 \(inf\) 的邊
\(3\)、從機器人向匯點建一條權值為機器人裝甲值的邊
判斷最大流是不是裝甲值之和即可
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
typedef double db;
const db eps=1e-8;
const int maxn=105,maxm=1e4+5;
int h[maxn],tot=2;
struct asd{
int to,nxt;
db val;
}b[maxm];
void ad(int aa,int bb,db cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int jla[maxn],jlb[maxn],a[maxn][maxn],n,m,s,t,mmax,h2[maxn];
db sum;
int dep[maxn],q[maxn],head,tail;
db dfs(int now,db ac1){
if(now==t) return ac1;
db ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(std::fabs(b[i].val)>eps && dep[u]==dep[now]+1){
rg db nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(std::fabs(ac1)<eps) break;
}
if(std::fabs(ac2)<eps) dep[now]=0;
return ac2;
}
bool bfs(){
for(rg int i=0;i<=mmax;i++){
h[i]=h2[i];
dep[i]=0;
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && std::fabs(b[i].val)>eps){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
db dinic(){
rg db nans=0;
while(bfs()){
nans+=dfs(s,1e9);
}
return nans;
}
bool jud(db val){
memset(h,-1,sizeof(h));
tot=2;
for(rg int i=1;i<=m;i++){
ad(s,i,(db)jlb[i]*val);
ad(i,s,0);
}
for(rg int i=1;i<=m;i++){
for(rg int j=1;j<=n;j++){
if(a[i][j]){
ad(i,j+m,1e9);
ad(j+m,i,0);
}
}
}
for(rg int i=1;i<=n;i++){
ad(i+m,t,(db)jla[i]);
ad(t,i+m,0);
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
return dinic()>=sum;
}
int main(){
n=read(),m=read();
for(rg int i=1;i<=n;i++){
jla[i]=read();
sum+=jla[i];
}
for(rg int i=1;i<=m;i++){
jlb[i]=read();
}
for(rg int i=1;i<=m;i++){
for(rg int j=1;j<=n;j++){
a[i][j]=read();
}
}
s=0,t=n+m+1,mmax=n+m+1;
db l=0,r=1e9,mids;
while(r-l>eps){
mids=(l+r)/2.0;
if(jud(mids)) r=mids;
else l=mids;
}
printf("%.6f\n",mids);
return 0;
}
例題三、洛谷P5038 [SCOI2012]奇怪的遊戲
題目描述
分析
不是很好想
看到二維棋盤上的問題考慮黑白染色
設一共有 \(cnt1\) 個白點,這些白點的權值和為 \(sum1\)
設一共有 \(cnt2\) 個黑點,這些黑點的權值和為 \(sum2\)
因為我們每一次會對相鄰的格子操作
也就是說操作若干次後,黑點和白點增加的總價值是相同的
設最後棋盤上的點都變成了 \(x\)
那麼就有 \(x \times cnt1-sum1=x \times cnt2 -sum2\)
化簡後可得 \(x(cnt1-cnt2)=sum1-sum2\)
此時,如果 \(cnt1-cnt2\) 不為 \(0\)
那麼我們就可以這一部分除過去得到 \(x\) 的值判斷是否合法即可
如果為 \(0\),那麼說明棋盤內格子的總個數一定為偶數
如果 \(x\) 合法,一定可以經過若干次操作後變成 \(x+1\)
所以可以二分答案然後判斷是否合法
判斷是否合法可以跑網路流
設棋盤中原來的數為 \(val\)
\(1\)、從源點向所有白點建一條權值為 \(x-val\) 的邊
\(2\)、從白點向所有和它相鄰的黑點建一條權值為 \(inf\) 的邊
\(3\)、從黑點向匯點建一條權值為 \(x-val\) 的邊
判斷最大流是否等於 \(\sum(x-val)\) 即可
注意二分的下界要從原圖的最大權值開始選
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=2e3+5,maxm=2e4+5;
const int dx[6]={0,0,-1,1},dy[6]={-1,1,0,0};
const long long INF=100000000000000LL;
int h[maxn],tot=2,n,m,tt;
struct asd{
int to,nxt;
long long val;
}b[maxm];
void ad(int aa,int bb,long long cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int js(int i,int j){
return (i-1)*m+j;
}
int a[maxn][maxn],s,t,h2[maxn],mmax,cnt0,cnt1,jlmax;
bool jud[maxn][maxn];
long long sum0,sum1;
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
long long dfs(int now,long long ac1){
if(now==t) return ac1;
long long ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg long long nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
bool pd(long long val){
memset(h,-1,sizeof(h));
tot=2;
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(jud[i][j]==0){
ad(s,js(i,j),std::max(val-a[i][j],0LL));
ad(js(i,j),s,0);
} else {
ad(js(i,j),t,std::max(val-a[i][j],0LL));
ad(t,js(i,j),0);
}
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(jud[i][j]) continue;
for(rg int k=0;k<4;k++){
rg int nx=i+dx[k],ny=j+dy[k];
if(nx<1 || ny<1 || nx>n || ny>m || jud[nx][ny]==jud[i][j]) continue;
ad(js(i,j),js(nx,ny),INF);
ad(js(nx,ny),js(i,j),0);
}
}
}
rg long long nans=0,mans=0;
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(jud[i][j]==0) mans+=std::max(val-a[i][j],0LL);
}
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
while(bfs()){
nans+=dfs(s,INF);
}
if(nans==mans) return 1;
else return 0;
}
int main(){
tt=read();
while(tt--){
sum0=sum1=0,cnt0=cnt1=jlmax=0;
n=read(),m=read();
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
a[i][j]=read();
jlmax=std::max(jlmax,a[i][j]);
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(j==1){
jud[i][j]=jud[i-1][j]^1;
} else {
jud[i][j]=jud[i][j-1]^1;
}
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
if(jud[i][j]){
cnt1++;
sum1+=a[i][j];
} else {
cnt0++;
sum0+=a[i][j];
}
}
}
s=0,t=n*m+1,mmax=n*m+1;
if(cnt0!=cnt1){
rg long long now=1LL*(sum1-sum0)/(cnt1-cnt0);
if(pd(now) && now>=jlmax) printf("%lld\n",1LL*now*cnt0-sum0);
else printf("-1\n");
} else {
rg long long l=jlmax,r=INF,mids;
while(l<=r){
mids=(l+r)>>1;
if(pd(mids)) r=mids-1;
else l=mids+1;
}
if(pd(l)==0) printf("-1\n");
else printf("%lld\n",1LL*cnt0*l-sum0);
}
}
return 0;
}
例題四、洛谷P4311 士兵佔領
題目描述
分析
逆向思維
考慮一開始把所有的格子都放上士兵,求出最多能夠刪掉多少士兵即可
分別計算每一行每一列最多有多少格子可以放士兵,設為 \(cnt1,cnt2\)
\(1\)、從源點向代表第 \(i\) 行的點建一條權值為 \(cnt1[i]-L[i]\) 的邊
代表這一行最多可以被刪多少士兵
\(2\)、如果 \((i,j)\) 位置可以放士兵,那麼從代表第 \(i\) 行的點向代表第 \(j\) 列的點建一條權值為 \(1\) 的點,代表這個點可以不放士兵
\(3\)、從代表第 \(j\) 列的點向匯點建一條權值為 \(cnt2[j]-L[j]\) 的邊
代表這一列最多可以被刪多少士兵
答案就是總的士兵數減去最大流
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e3+5,maxm=1e6+5;
int h[maxn],tot=2,n,m,l[maxn],c[maxn],k,h2[maxn],cnt1[maxn],cnt2[maxn],a[maxn][maxn],s,t,mmax;
struct asd{
int to,nxt,val;
}b[maxm];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(int now,int ac1){
if(now==t) return ac1;
int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg int nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int main(){
memset(h,-1,sizeof(h));
m=read(),n=read(),k=read();
for(rg int i=1;i<=m;i++){
l[i]=read();
cnt1[i]=n;
}
for(rg int i=1;i<=n;i++){
c[i]=read();
cnt2[i]=m;
}
rg int aa,bb;
for(rg int i=1;i<=k;i++){
aa=read(),bb=read();
a[aa][bb]=1;
cnt1[aa]--;
cnt2[bb]--;
}
for(rg int i=1;i<=m;i++){
if(cnt1[i]<l[i]){
printf("JIONG!\n");
return 0;
}
cnt1[i]-=l[i];
}
for(rg int i=1;i<=n;i++){
if(cnt2[i]<c[i]){
printf("JIONG!\n");
return 0;
}
cnt2[i]-=c[i];
}
s=0,t=n+m+1,mmax=n+m+1;
for(rg int i=1;i<=m;i++){
ad(s,i,cnt1[i]);
ad(i,s,0);
}
for(rg int i=1;i<=n;i++){
ad(i+m,t,cnt2[i]);
ad(t,i+m,0);
}
for(rg int i=1;i<=m;i++){
for(rg int j=1;j<=n;j++){
if(a[i][j]!=1){
ad(i,j+m,1);
ad(j+m,i,0);
}
}
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg int nans=0;
while(bfs()){
nans+=dfs(s,100000000);
}
nans=n*m-k-nans;
printf("%d\n",nans);
return 0;
}
最小割
將選中的邊刪去之後, \(s\) 和 \(t\) 不再連通, 點集 \(V\) 被分割為兩部分 \(V_s\) 和 \(V_t\) . 我們稱點集 \((V_s,V_t)\) 為流網路的一個割, 定義它的容量為所有滿足 \(u\in V_s, v\in V_t\) 的邊 \((u,v)\) 的容量之和
可以理解為找一些邊滿足從 \(s\) 到 \(t\) 的任意路徑都必須至少經過一條這些邊, 同時讓這些選中的邊的權值最小.
最大流最小割定量: 在任何的網路中,最大流的值等於最小割的容量。
例題一、洛谷P4001 [ICPC-Beijing 2006]狼抓兔子
題目描述
分析
一個最小割的板子題,把圖建出來之後直接跑一個最小割就行了
但是時間複雜度比較玄學
一種更優秀的做法是對原圖建一個對偶圖,然後跑最短路
引用 Imakf部落格 的圖可能更好理解一些
大概就是把割邊的花費變成了邊權
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=2e6+5,maxm=3e6+5;
int h[maxn],tot=2,n,m,h2[maxn],s,t,mmax;
struct asd{
int to,nxt,val;
}b[maxm<<1];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dis[maxn];
bool vis[maxn];
struct jie{
int num,dis;
jie(){}
jie(int aa,int bb){
num=aa,dis=bb;
}
bool operator < (const jie& A)const{
return dis>A.dis;
}
};
std::priority_queue<jie> q;
void dij(){
memset(dis,0x3f,sizeof(dis));
dis[s]=0;
q.push(jie(s,0));
while(!q.empty()){
rg int now=q.top().num;
q.pop();
if(vis[now]) continue;
vis[now]=1;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(dis[u]>dis[now]+b[i].val){
dis[u]=dis[now]+b[i].val;
q.push(jie(u,dis[u]));
}
}
}
}
int js(int i,int j){
return (i-1)*(m-1)+j;
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read();
rg int aa,nans=0x3f3f3f3f;
if(n==1 || m==1){
rg int d=(n==1)?m:n;
for(rg int i=1;i<d;i++){
aa=read();
nans=std::min(nans,aa);
}
printf("%d\n",nans);
return 0;
}
s=0,t=n*m+1;
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<m;j++){
aa=read();
if(i==1){
ad(s,js(i,j),aa);
ad(js(i,j),s,aa);
} else if(i==n){
ad(js(i*2-2,j),t,aa);
ad(t,js(i*2-2,j),aa);
} else {
ad(js(i*2-2,j),js(i*2-1,j),aa);
ad(js(i*2-1,j),js(i*2-2,j),aa);
}
}
}
for(rg int i=1;i<n;i++){
for(rg int j=1;j<=m;j++){
aa=read();
if(j==1){
ad(js(i*2,j),t,aa);
ad(t,js(i*2,j),aa);
} else if(j==m){
ad(s,js(i*2-1,m-1),aa);
ad(js(i*2-1,m-1),s,aa);
} else {
ad(js(i*2-1,j-1),js(i*2,j),aa);
ad(js(i*2,j),js(i*2-1,j-1),aa);
}
}
}
for(rg int i=1;i<n;i++){
for(rg int j=1;j<m;j++){
aa=read();
ad(js(i*2-1,j),js(i*2,j),aa);
ad(js(i*2,j),js(i*2-1,j),aa);
}
}
dij();
printf("%d\n",dis[t]);
return 0;
}
例題二、洛谷P3227 [HNOI2013]切糕
題目描述
分析
把割點看成割邊,新建一個虛擬層 \(R+1\),實際上就是求一個最小割
先不考慮光滑程度的限制
由源點向第一層中所有的點建一條邊權為 \(inf\) 的邊,由第 \(R+1\) 層中所有的點向匯點建一條邊權為 \(inf\) 的邊,這些邊都是割不掉的
然後對於任何一個 \(1\leq i\leq P,1\leq j\leq Q,1\leq k\leq R\),由 \((i,j,k)\) 向 \((i,j,k+1)\) 連一條容量為 \((i,j,k)\)的不和諧值的邊
對於光滑程度的限制 \(d\)
由 \((i,j,k)\) 向 \((i \pm 1,j,k−d)\) 和 \((i,j\pm 1,k-d)\) 建邊權為 \(inf\) 的邊就可以了
這樣如果割兩個高度差大於 \(d\) 的邊,就還會有一條 \(inf\) 的通路,使得 \(S,T\) 連通。
限制了不能割這樣的兩條邊。
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5,maxm=45;
const int dx[6]={0,0,1,-1},dy[6]={1,-1,0,0};
int h[maxn],tot=2,n,m,r,d,h2[maxn],s,t,mmax,a[maxm][maxm][maxm];
struct asd{
int to,nxt,val;
}b[maxn<<1];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(int now,int ac1){
if(now==t) return ac1;
int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg int nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int js(int i,int j,int k){
return (k-1)*n*m+(i-1)*m+j;
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read(),r=read(),d=read();
for(rg int k=1;k<=r;k++){
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
a[i][j][k]=read();
}
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
for(rg int k=1;k<=r;k++){
ad(js(i,j,k),js(i,j,k+1),a[i][j][k]);
ad(js(i,j,k+1),js(i,j,k),0);
}
}
}
s=0,t=n*m*(r+1)+1,mmax=n*m*(r+1)+1;
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
ad(s,js(i,j,1),0x3f3f3f3f);
ad(js(i,j,1),s,0);
ad(js(i,j,r+1),t,0x3f3f3f3f);
ad(t,js(i,j,r+1),0);
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
for(rg int k=d+1;k<=r+1;k++){
for(rg int o=0;o<4;o++){
rg int nx=i+dx[o],ny=j+dy[o];
if(nx<1 || nx>n || ny<1 || ny>m) continue;
ad(js(i,j,k),js(nx,ny,k-d),0x3f3f3f3f);
ad(js(nx,ny,k-d),js(i,j,k),0);
}
}
}
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg int nans=0;
while(bfs()){
nans+=dfs(s,0x3f3f3f3f);
}
printf("%d\n",nans);
return 0;
}
例題三、洛谷P4174 [NOI2006] 最大獲利
題目描述
分析
經典的最大權閉合子圖問題
\(1\)、由源點向所有的使用者建邊權為收益的邊
\(2\)、由使用者向基站建邊權為 \(inf\) 的邊
\(3\)、由基站向匯點建邊權為花費的邊
答案就是總收益減去最小割
可以這樣理解
如果我們割掉了使用者,就代表我們放棄這個使用者的收益
如果我們割掉了基站,就代表我們要建造這個基站,這要有一定的花費
使用者和基站之間建邊權為 \(inf\) 的邊實際上是一種強制關係
這些邊是割不掉的,如果我們選擇了使用者,那麼必須建造相應的基站
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5,maxm=3e6+5;
int h[maxn],tot=2,n,m,h2[maxn],s,t,mmax,a[maxn];
struct asd{
int to,nxt,val;
}b[maxm<<1];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(int now,int ac1){
if(now==t) return ac1;
int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg int nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read();
s=0,t=n+m+1,mmax=n+m+1;
rg int ans=0;
for(rg int i=1;i<=n;i++){
a[i]=read();
ad(i+m,t,a[i]);
ad(t,i+m,0);
}
rg int aa,bb,cc;
for(rg int i=1;i<=m;i++){
aa=read(),bb=read(),cc=read();
ad(i,aa+m,0x3f3f3f3f);
ad(aa+m,i,0);
ad(i,bb+m,0x3f3f3f3f);
ad(bb+m,i,0);
ad(s,i,cc);
ad(i,s,0);
ans+=cc;
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg int nans=0;
while(bfs()){
nans+=dfs(s,0x3f3f3f3f);
}
printf("%d\n",ans-nans);
return 0;
}
例題四、洛谷P1646 [國家集訓隊]happiness
題目描述
分析
和上一道題一樣,用總價值減去圖中的最小割
問題在於如何建邊
首先由 \(s\) 向 \((i,j)\) 建邊,邊權為選文的價值
由 \((i,j)\) 向 \(t\) 建邊,邊權為選理的價值
先處理同選文的限制
對於\((i,j)\) 和 \((i+1,j)\) 兩個點的組合情況
假設這兩個點同時選文科有 \(w\) 的喜悅值
新建一個節點 \(x\),從 \(s\) 向 \(x\) 連一條容量為喜悅值 \(w\) 的邊
再從 \(x\) 向 \((i,j)\) 和 \((i+1,j)\) 分別連一條容量為 \(inf\) 的邊
其它情況同理
考慮這樣做為什麼是正確的
如果 \(a\) 和 \(b\) 中有一個沒有選文,那麼必定一條選理的邊沒有被割掉
\(s \to x \to a/b \to t\) 這條路徑仍然是聯通的,原圖並沒有被割掉
\(x \to a\) 的邊權是 \(inf\) ,肯定割不掉
所以我們只能割掉 \(s \to x\) 這條邊
也就是說恰好把同選文的貢獻割掉了
還有一種解方程的做法
上面的點為 \(i\),下面的點為 \(j\),左邊選文,右邊選理
先設 \(a_i\) 表示 \(i\) 選文科的價值,\(b_i\) 表示 \(i\) 選理科的價值,\(c_{i,j}\) 表示 \(i,j\) 同選文科的價值,\(d_{i,j}\) 表示 \(i,j\) 同選理科的價值。
然後列舉所有的最小割
第一種情況同時選文科,最小割為 \(c,d\) 邊權之和,此時需要將 \(c,d\) 選理科的貢獻減掉
就有 \(c+d=b_i+b_j+d_{i,j}\)
第二種情況同時選理科,最小割為 \(a,b\) 邊權之和
就有 \(a+b=a_i+a_j+c_{i,j}\)
第三種情況 \(i\) 選文科,\(j\) 選理科,最小割為 \(b,c,e\) 邊權之和
就有 \(b+c+e=b_i+a_j+c_{i,j}+d_{i,j}\)
第四種情況 \(i\) 選理科,\(j\) 選文科,最小割為 \(a,d,f\) 邊權之和
就有 \(a+d+f=a_i+b_j+c_{i,j}+d_{i,j}\)
還有一個方程是 \(e=f\)
然後五個未知數五個方程直接解方程就行了
解得
\(a=a_i+\frac{c_{i,j}}{2}\)
\(b=a_j+\frac{c_{i,j}}{2}\)
\(c=b_i+\frac{d_{i,j}}{2}\)
\(d=b_j+\frac{d_{i,j}}{2}\)
\(e=f=\frac{c_{i,j}+d_{i,j}}{2}\)
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1e6+5,maxm=105;
int h[maxn],tot=2,n,m,h2[maxn],s,t,mmax;
struct asd{
int to,nxt,val;
}b[maxn<<1];
void ad(int aa,int bb,int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(int now,int ac1){
if(now==t) return ac1;
int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg int nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int js(int i,int j){
return (i-1)*m+j;
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read();
s=0,t=n*m+1,mmax=n*m+1;
rg int aa,ans=0;
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
aa=read();
ad(s,js(i,j),aa);
ad(js(i,j),s,0);
ans+=aa;
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=m;j++){
aa=read();
ans+=aa;
ad(js(i,j),t,aa);
ad(t,js(i,j),0);
}
}
for(rg int i=1;i<n;i++){
for(rg int j=1;j<=m;j++){
mmax++;
aa=read();
ans+=aa;
ad(s,mmax,aa);
ad(mmax,s,0);
ad(mmax,js(i,j),0x3f3f3f3f);
ad(js(i,j),mmax,0);
ad(mmax,js(i+1,j),0x3f3f3f3f);
ad(js(i+1,j),mmax,0);
}
}
for(rg int i=1;i<n;i++){
for(rg int j=1;j<=m;j++){
mmax++;
aa=read();
ans+=aa;
ad(mmax,t,aa);
ad(t,mmax,0);
ad(js(i,j),mmax,0x3f3f3f3f);
ad(mmax,js(i,j),0);
ad(js(i+1,j),mmax,0x3f3f3f3f);
ad(mmax,js(i+1,j),0);
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<m;j++){
mmax++;
aa=read();
ans+=aa;
ad(s,mmax,aa);
ad(mmax,s,0);
ad(mmax,js(i,j),0x3f3f3f3f);
ad(js(i,j),mmax,0);
ad(mmax,js(i,j+1),0x3f3f3f3f);
ad(js(i,j+1),mmax,0);
}
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<m;j++){
mmax++;
aa=read();
ans+=aa;
ad(mmax,t,aa);
ad(t,mmax,0);
ad(js(i,j),mmax,0x3f3f3f3f);
ad(mmax,js(i,j),0);
ad(js(i,j+1),mmax,0x3f3f3f3f);
ad(mmax,js(i,j+1),0);
}
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg int nans=0;
while(bfs()){
nans+=dfs(s,0x3f3f3f3f);
}
printf("%d\n",ans-nans);
return 0;
}
例題五、洛谷P1791 [國家集訓隊]人員僱傭
題目描述
分析
用總價值減去最小割
還是用解方程的做法
左邊代表選,右邊代表不選,上邊為 \(i\),下邊為 \(j\)
如果 \(i,j\) 都選,那麼最小割為 \(c,d\),代價為 \(0\)
如果 \(i,j\) 都不選,那麼最小割為 \(a,b\) ,代價為 \(E_{i,j}+E_{j,i}\)
如果 \(i\) 選 \(j\) 不選,那麼最小割為 \(b,c,e\) ,代價為 \(E_{i,j}+E_{j,i}+E_{i,j}\)
如果 \(i\) 不選 \(j\) 選,那麼最小割為 \(a,c,f\) ,代價為 \(E_{i,j}+E_{j,i}+E_{i,j}\)
解得
\(a=b=E_{i,j},c=d=0,e=f=2E_{i,j}\)
程式碼
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=3e6+5,maxm=1e3+5;
int h[maxn],tot=2,n,h2[maxn],s,t,mmax;
long long sum[maxm];
struct asd{
int to,nxt;
long long val;
}b[maxn<<1];
void ad(int aa,int bb,long long cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=cc;
h[aa]=tot++;
}
int dep[maxn],q[maxn],head,tail;
bool bfs(){
for(rg int i=0;i<=mmax;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(!dep[u] && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
long long dfs(int now,long long ac1){
if(now==t) return ac1;
long long ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(dep[u]==dep[now]+1 && b[i].val){
rg long long nans=dfs(u,std::min(ac1,b[i].val));
ac1-=nans;
ac2+=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
int main(){
memset(h,-1,sizeof(h));
n=read();
s=0,t=n+1,mmax=n+1;
rg int aa;
rg long long ans=0;
for(rg int i=1;i<=n;i++){
aa=read();
ad(i,t,aa);
ad(t,i,0);
}
for(rg int i=1;i<=n;i++){
for(rg int j=1;j<=n;j++){
aa=read();
if(aa==0) continue;
sum[i]+=aa;
ans+=aa;
ad(i,j,aa*2LL);
ad(j,i,0);
}
}
for(rg int i=1;i<=n;i++){
ad(s,i,sum[i]);
ad(i,s,0);
}
for(rg int i=0;i<=mmax;i++){
h2[i]=h[i];
}
rg long long nans=0;
while(bfs()){
nans+=dfs(s,0x3f3f3f3f3f3f3f3f);
}
printf("%lld\n",ans-nans);
return 0;
}
例題六、洛谷P4123 [CQOI2016]不同的最小割
題目描述
分析
最小割樹的模板題
類似於分治的思想遞迴求解
把和源點在一個聯通塊的分成一部分,把和匯點在一個聯通塊的分成另一部分
判斷在哪一個聯通塊的標準就是深度是否為 \(0\)
程式碼
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<set>
#include<queue>
#define rg register
inline int read(){
rg int x=0,fh=1;
rg char ch=getchar();
while(ch<'0' || ch>'9'){
if(ch=='-') fh=-1;
ch=getchar();
}
while(ch>='0' && ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x*fh;
}
const int maxn=1005,maxm=100005;
int h[maxn],tot=2,n,m,s,t,h2[maxn],dep[maxn];
struct asd{
int to,nxt,val,pre;
}b[maxm<<1];
void ad(rg int aa,rg int bb,rg int cc){
b[tot].to=bb;
b[tot].nxt=h[aa];
b[tot].val=b[tot].pre=cc;
h[aa]=tot++;
}
int q[maxn],head,tail;
bool bfs(){
for(rg int i=1;i<=n;i++){
dep[i]=0;
h[i]=h2[i];
}
q[head=tail=1]=s;
dep[s]=1;
while(head<=tail){
rg int now=q[head++];
for(rg int i=h[now];i!=-1;i=b[i].nxt){
rg int u=b[i].to;
if(dep[u]==0 && b[i].val){
dep[u]=dep[now]+1;
q[++tail]=u;
}
}
}
return dep[t];
}
int dfs(rg int now,rg int ac1){
if(now==t) return ac1;
rg int ac2=0;
for(rg int i=h[now];i!=-1;i=b[i].nxt){
h[now]=i;
rg int u=b[i].to;
if(b[i].val && dep[u]==dep[now]+1){
rg int nans=dfs(u,std::min(b[i].val,ac1));
ac2+=nans;
ac1-=nans;
b[i].val-=nans;
b[i^1].val+=nans;
}
if(ac1==0) break;
}
if(ac2==0) dep[now]=0;
return ac2;
}
std::set<int> s2;
int fa[maxn];
bool cmp(rg int aa,rg int bb){
return dep[aa]<dep[bb];
}
void solve(rg int l,rg int r){
if(l==r) return;
s=fa[l],t=fa[r];
rg int nans=0;
while(bfs()) nans+=dfs(s,0x3f3f3f3f);
s2.insert(nans);
for(rg int i=2;i<tot;i++) b[i].val=b[i].pre;
std::sort(fa+l,fa+r+1,cmp);
rg int mids=0;
for(rg int i=l;i<=r;i++){
if(dep[fa[i]]){
mids=i;
break;
}
}
solve(l,mids-1);
solve(mids,r);
}
int main(){
memset(h,-1,sizeof(h));
n=read(),m=read();
rg int aa,bb,cc;
for(rg int i=1;i<=m;i++){
aa=read(),bb=read(),cc=read();
ad(aa,bb,cc);
ad(bb,aa,cc);
}
for(rg int i=1;i<=n;i++){
h2[i]=h[i];
fa[i]=i;
}
solve(1,n);
printf("%d\n",s2.size());
return 0;
}