网络流(最大流&费用流)

柒葉灬

2019-07-24 19:19:36

Personal

# 最大流和费用流的应用 ------ ## 模板: ### 1.最大流 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=1.5e4+100,maxm=5e6+100; int n,m,k; int tot=1,cur[maxn],head[maxn],to[maxm],nxt[maxm],w[maxm]; void clear(){ tot=1; memset(head,0,sizeof(head)); } void add_edge(int u,int v,int c){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=c; head[u]=tot; } void add_edges(int u,int v,int c){ add_edge(u,v,c); add_edge(v,u,0); } int dep[maxn]; queue<int>Q; int s,t; bool bfs(){ while(!Q.empty())Q.pop(); memset(dep,0,sizeof(dep)); dep[s]=1;Q.push(s); while(!Q.empty()){ int x=Q.front(); Q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(!dep[y]&&w[i]){ dep[y]=dep[x]+1; Q.push(y); } } } return dep[t]; } int dfs(int x,int flow){ if(x==t)return flow; int used=0; for(int &i=cur[x];i;i=nxt[i]){ int y=to[i]; if(w[i]&&dep[y]==dep[x]+1){ int tmp=dfs(y,min(flow,w[i])); w[i]-=tmp;w[i^1]+=tmp; used+=tmp; flow-=tmp; if(flow==0)break; } } if(flow)dep[x]=0; return used; } long long dinic(){ long long flow=0; while(bfs()){ for(int i=1;i<=n;i++) cur[i]=head[i]; flow+=dfs(s,2e9); } return flow; } int main(){ cin>>n>>m>>s>>t; for(int i=1,x,y,z;i<=m;i++){ scanf("%d%d%d",&x,&y,&z); add_edges(x,y,z); } cout<<dinic()<<endl; return 0; } ``` ------ ### 2.最小费用最大流 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=1005,maxm=2e5; int s,t; int tot=1,head[maxn],to[maxm],nxt[maxm],w[maxm],f[maxm]; void clear(){ memset(head,0,sizeof(head)); tot=1; } void add_edge(int u,int v,int flow,int cost){ to[++tot]=v; nxt[tot]=head[u]; head[u]=tot; w[tot]=cost; f[tot]=flow; } void add_edges(int u,int v,int flow,int cost){ add_edge(u,v,flow,cost); add_edge(v,u,0,-cost); } int dis[maxn],pos[maxn],last[maxn]; bool vis[maxn]; bool spfa(){ queue<int>Q; while(!Q.empty())Q.pop(); memset(last,0,sizeof(last)); memset(dis,63,sizeof(dis)); Q.push(s);dis[s]=0; while(!Q.empty()){ int x=Q.front(); Q.pop(); vis[x]=0; for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(f[i]&&dis[y]>dis[x]+w[i]){ dis[y]=dis[x]+w[i]; last[y]=x; pos[y]=i; if(!vis[y]){ vis[y]=1; Q.push(y); } } } } return last[t]>0; } void MCMF(){ int cost=0,flow=0; while(spfa()){ int tmp=2e9; for(int x=t;x!=s;x=last[x]) tmp=min(tmp,f[pos[x]]); for(int x=t;x!=s;x=last[x]){ f[pos[x]]-=tmp; f[pos[x]^1]+=tmp; } flow+=tmp; cost+=tmp*dis[t]; } } int main(){ //连边 MCMF(); return 0; } ``` --------------- ## 题解: ### 1.方格取数(最大流,最小割) #### 题意: 给n行m列的矩阵,取出不相邻的若干个数,使得权值和最大。 #### 思路: 最大流流出去的总是权值最小的,这题如果直接写不好写, 考虑换一种方向: 选出去不要哪些点,总和减去这个值就是答案。 #### 代码: ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int rx[]={0,0,1,-1}; const int ry[]={1,-1,0,0}; const int maxn=1e4+100,maxm=1e5; int n,m,sum,mp[maxn][maxn]; int s,t; int tot=-1,head[maxn],cur[maxn],to[maxm],nxt[maxm],w[maxm]; void add_edge(int u,int v,int c){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=c; head[u]=tot; } void add_edges(int u,int v,int c){ add_edge(u,v,c); add_edge(v,u,0); } int dep[maxn]; bool bfs(){ queue<int>Q; while(!Q.empty())Q.pop(); for(int i=1;i<=t;i++) dep[i]=0; Q.push(s); dep[s]=1; while(!Q.empty()){ int x=Q.front(); Q.pop(); for(int i=head[x];~i;i=nxt[i]){ int y=to[i]; if(dep[y]==0&&w[i]){ dep[y]=dep[x]+1; Q.push(y); } } } return dep[t]!=0; } int dfs(int x,int flow){ if(x==t)return flow; for(int &i=cur[x];~i;i=nxt[i]){ int y=to[i]; if(dep[y]==dep[x]+1&&w[i]){ int d=dfs(y,min(flow,w[i])); if(d){ w[i]-=d; w[i^1]+=d; return d; } } } return 0; } void dinic(){ int ans=0; while(bfs()){ for(int i=1;i<=t;i++) cur[i]=head[i]; while(int d=dfs(s,2e9)) ans+=d; } cout<<sum-ans<<endl; } int ID(int x,int y){ if(x<1||y<1||x>n||y>m)return -1; return (x-1)*m+y; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&mp[i][j]),sum+=mp[i][j]; memset(head,-1,sizeof(head)); s=n*m+1,t=s+1; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) if(i+j&1)add_edges(ID(i,j),t,mp[i][j]); else { add_edges(s,ID(i,j),mp[i][j]); for(int k=0;k<4;k++){ int y=ID(i+rx[k],j+ry[k]); if(y==-1)continue; add_edges(ID(i,j),y,2e9); } } dinic(); return 0; } ``` **这题用了一个思想:利用网络流求出最小需要舍弃的权值** **答案则变为总和减去最小代价** **而这种思想就是“最小割”** 定义:去除一些边,使得汇点**到不了**源点。 而最后保留的就是选出最优的答案。 ### 2.餐巾计划(好题,最小费用最大流) #### 思路1: 处理流量很麻烦,不妨直接设流量就是每天需要之和, 即先买了所有的餐巾, 洗餐巾就相当于退掉,向汇点连边。 #### 思路2: 根据贪心的思路,每天的餐巾买了要么直接洗,要么就一直不洗, ![https://cdn.luogu.com.cn/upload/pic/65563.png ](https://cdn.luogu.com.cn/upload/pic/65563.png ) 下面是思路1的代码: ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=2e3+100,maxm=1e5+100; int n,P,M,F,N,S; int s,t; int A[maxn]; int tot=1,head[maxn],to[maxm],nxt[maxm],f[maxm],w[maxm]; void add_edge(int u,int v,int d,int flow){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=d; f[tot]=flow; head[u]=tot; } void add_edges(int u,int v,int d,int flow){ add_edge(u,v,d,flow); add_edge(v,u,-d,0); } int dis[maxn],fa[maxn],last[maxn]; bool vis[maxn]; queue<int>Q; bool spfa(){ memset(dis,127,sizeof(dis)); memset(last,0,sizeof(last)); while(!Q.empty())Q.pop(); Q.push(s);dis[s]=0; while(!Q.empty()){ int x=Q.front(); Q.pop(); vis[x]=0; for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(f[i]&&dis[y]>dis[x]+w[i]){ dis[y]=dis[x]+w[i]; fa[y]=x; last[y]=i; if(!vis[y]){ vis[y]=1; Q.push(y); } } } } return last[t]; } void solve(){ s=2*n+1;t=2*n+2; int cost=0,flow=0; for(int i=1;i<=n;i++){ add_edges(s,i,0,A[i]); add_edges(i+n,t,0,A[i]); if(i+M<=n)add_edges(i,i+M+n,F-P,2e9); if(i+N<=n)add_edges(i,i+N+n,S-P,2e9); if(i<n)add_edges(i,i+1,0,2e9); else add_edges(i,t,0,2e9); cost+=A[i]*P; } while(spfa()){ int tmp=2e9; for(int i=t;i!=s;i=fa[i]){ tmp=min(tmp,f[last[i]]); } for(int i=t;i!=s;i=fa[i]){ f[last[i]]-=tmp; f[last[i]^1]+=tmp; } cost+=tmp*dis[t]; flow+=tmp; } cout<<cost<<endl; } int main(){ cin>>n>>P>>M>>F>>N>>S; for(int i=1;i<=n;i++) scanf("%d",&A[i]); solve(); return 0; } ``` 网络流有很多种做法, 图建的不一样答案一样就可以了。 ---------- ### 3.星际转移(最大流) #### 思路: 对于每一个时间的每一个点建一个点, 如果跑最大流之后流量达不到人数, 说明还需要时间, 那么就继续建新点,连新边, 直到找到最小的时间或者输出-1. #### 代码: ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=1.5e4+100,maxm=5e6+100; int n,m,k; int h[25],r[25],num[25][25]; int tot=1,cur[maxn],head[maxn],to[maxm],nxt[maxm],w[maxm]; void add_edge(int u,int v,int c){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=c; head[u]=tot; } void add_edges(int u,int v,int c){ add_edge(u,v,c); add_edge(v,u,0); } int dep[maxn]; queue<int>Q; int s,t; bool bfs(){ while(!Q.empty())Q.pop(); memset(dep,0,sizeof(dep)); dep[s]=1;Q.push(s); while(!Q.empty()){ int x=Q.front(); Q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(!dep[y]&&w[i]){ dep[y]=dep[x]+1; Q.push(y); } } } return dep[t]; } int dfs(int x,int flow){ if(x==t)return flow; for(int &i=cur[x];i;i=nxt[i]){ int y=to[i]; if(w[i]&&dep[y]==dep[x]+1){ int tmp=dfs(y,min(flow,w[i])); if(tmp){ w[i]-=tmp; w[i^1]+=tmp; return tmp; } } } return 0; } int dinic(){ int flow=0; while(bfs()){ for(int i=1;i<=t;i++) cur[i]=head[i]; while(int d=dfs(s,2e9)) flow+=d; } return flow; } int solve(){ s=800*(n+2)+1,t=s+1; //n+1是月球 n+2是地球 int flow=0; for(int i=0;i<799;i++){ add_edges(s,i*(n+2)+n+2,2e9); add_edges((i+1)*(n+2)+n+1,t,2e9); for(int j=1;j<=m;j++){ int u=num[j][i%r[j]]; int v=num[j][(i+1)%r[j]]; add_edges(i*(n+2)+u,(i+1)*(n+2)+v,h[j]); } for(int j=1;j<=n+2;j++) add_edges(i*(n+2)+j,(i+1)*(n+2)+j,2e9); flow+=dinic(); if(flow>=k)return i+1; } return 0; } int main(){ // double sz=&cur2-&cur1; // cout<<sz/1024/1024<<endl; cin>>n>>m>>k; for(int i=1;i<=m;i++){ scanf("%d%d",&h[i],&r[i]); for(int j=0;j<r[i];j++){ scanf("%d",&num[i][j]); if(num[i][j]<=0)num[i][j]+=n+2; } } cout<<solve()<<endl; return 0; } ``` 网络流可以一边跑一边建新边。 -------- ### 4.最长 k 可重区间集(好题,费用流) #### 思路: 离散化坐标, 然后每两个点之间连上边, $l[i]$与$r[i]$连边, 汇点与$1$连流量为$K$的边 最后跑出来的最小费用的相反数就是最大的答案。 #### 代码: ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=1005,maxm=2e5; int s,t,n,K; int l[maxn],r[maxn],val[maxn]; int q,C[maxn]; int tot=1,head[maxn],to[maxm],nxt[maxm],w[maxm],f[maxm]; void add_edge(int u,int v,int flow,int cost){ to[++tot]=v; nxt[tot]=head[u]; head[u]=tot; w[tot]=cost; f[tot]=flow; } void add_edges(int u,int v,int flow,int cost){ add_edge(u,v,flow,cost); add_edge(v,u,0,-cost); } int dis[maxn],pos[maxn],last[maxn]; bool vis[maxn]; bool spfa(){ queue<int>Q; while(!Q.empty())Q.pop(); memset(last,0,sizeof(last)); memset(dis,63,sizeof(dis)); Q.push(s);dis[s]=0; while(!Q.empty()){ int x=Q.front(); Q.pop(); vis[x]=0; for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(f[i]&&dis[y]>dis[x]+w[i]){ dis[y]=dis[x]+w[i]; last[y]=x; pos[y]=i; if(!vis[y]){ vis[y]=1; Q.push(y); } } } } return last[t]>0; } void MCMF(){ int cost=0,flow=0; while(spfa()){ int tmp=2e9; for(int x=t;x!=s;x=last[x]) tmp=min(tmp,f[pos[x]]); for(int x=t;x!=s;x=last[x]){ f[pos[x]]-=tmp; f[pos[x]^1]+=tmp; } flow+=tmp; cost+=tmp*dis[t]; } cout<<-cost<<endl; } int main(){ cin>>n>>K; for(int i=1;i<=n;i++){ scanf("%d%d",&l[i],&r[i]); if(l[i]>r[i])swap(l[i],r[i]); C[++q]=l[i];C[++q]=r[i]; val[i]=r[i]-l[i]; } sort(C+1,C+1+q); q=unique(C+1,C+1+q)-C-1; for(int i=1;i<=n;i++){ l[i]=lower_bound(C+1,C+1+q,l[i])-C; r[i]=lower_bound(C+1,C+1+q,r[i])-C; } s=q+1;t=s+1; for(int i=1;i<q;i++) add_edges(i,i+1,2e9,0); add_edges(s,1,K,0); add_edges(q,t,K,0); for(int i=1;i<=n;i++){ add_edges(l[i],r[i],1,-val[i]); } MCMF(); return 0; } ``` ### 5.太空飞行计划(好题,最大权闭合图) 先介绍一下最大权闭合图什么意思: 给一个有向图,每个点有一个权值, 若选择一个点,那么他的后继节点也都要选, 求最大权值是多少。 #### 思路: 这题若选择了一个任务, 那么所连接的所有器材也都要选上, 任务的权值为正,器材的权值为负, 求得就是最大的利润。 (十分符合最大权闭合图模型) 处理最大权闭合子图: 将源点$s$与所有正权点连边, 将所有负权点与汇点$t$连边, 原图中的所有边变为流量为$INF$的边, 流出的最大流即最小割,即最大权闭合图。 **还有个问题就是决策** 若源点连向正权值的边流量不为$0$, 则说明这条边没有被割掉, 所以说明这个点要选上, 负权点根据正权点选择搞就行了。 ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=205,maxm=1e5; int n,m; int A[maxn],B[maxn]; int s,t; int tot,cur[maxn],head[maxn],to[maxm],nxt[maxm],w[maxm]; void clear(){ memset(head,-1,sizeof(head)); tot=-1; } void add_edge(int u,int v,int c){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=c; head[u]=tot; } void add_edges(int u,int v,int c){ add_edge(u,v,c); add_edge(v,u,0); } int dep[maxn]; bool bfs(){ queue<int>Q; while(!Q.empty())Q.pop(); for(int i=1;i<=t;i++) dep[i]=0; dep[s]=1; Q.push(s); while(!Q.empty()){ int x=Q.front(); Q.pop(); for(int i=head[x];~i;i=nxt[i]){ int y=to[i]; if(dep[y]==0&&w[i]){ dep[y]=dep[x]+1; Q.push(y); } } } return dep[t]!=0; } int dfs(int x,int flow){ if(x==t)return flow; for(int &i=cur[x];~i;i=nxt[i]){ int y=to[i]; if(w[i]&&dep[y]==dep[x]+1){ int tmp=dfs(y,min(flow,w[i])); if(tmp){ w[i]-=tmp; w[i^1]+=tmp; return tmp; } } } return 0; } void dinic(){ int res=0; while(bfs()){ for(int i=1;i<=t;i++) cur[i]=head[i]; while(int d=dfs(s,2e9)) res+=d; } } bool mark[maxn]; void solve(){ dinic(); for(int i=1;i<=n+m;i++) if(dep[i]){ mark[i]=1; } int ans=0; for(int i=1,p=0;i<=m;i++){ if(mark[i]){ if(p)putchar(' '); else p=1; printf("%d",i); ans+=A[i]; } } puts(""); for(int i=1,p=0;i<=n;i++){ if(mark[i+m]){ ans-=B[i]; if(p)putchar(' '); else p=1; printf("%d",i); } } puts(""); printf("%d\n",ans); } int main(){ clear(); cin>>m>>n; s=n+m+1,t=n+m+2; for(int i=1;i<=m;i++){ scanf("%d",&A[i]); add_edges(s,i,A[i]); char c=' '; while(c==' '){ int res=0; while((c=getchar())<48); do res=(res<<1)+(res<<3)+(c^48); while((c=getchar())>47); add_edges(i,res+m,2e9); } } for(int i=1;i<=n;i++){ scanf("%d",&B[i]); add_edges(i+m,t,B[i]); } solve(); return 0; } ``` ### 6.Harmonious Army(神题,最小割) #### 题意: 给$n$个点染黑色或白色,$m$个关系,若$u_i$与$v_i$都为黑则贡献为$a_i$,若都是白色则为$c_i$,否则是$b_i$。($b_i=\frac{a_i}{3}+\frac{c_i}{4}$) #### 思路: 考虑到正着选不好写,无法转换成最大贡献 应该想到转换为最小割问题,写最小贡献割。 建图确实很神奇,就算想到了最小割也不一定会写, 图建出来为下图所示: ![](https://cdn.luogu.com.cn/upload/pic/65608.png ) 对于一对关系有四种割法,正好可以对应$4$种选择, $$ a+b=B+C $$ $$ c+d=A+B $$ $$ a+e+d=b+e+c=A+C $$ 解得: $$ a=b=\frac{B+C}{2} $$ $$ c=d=\frac{A+B}{2} $$ $$ e=-B+\frac{A+C}{2} $$ ```cpp #include<bits/stdc++.h> #define debug(x) cerr<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl #define debug2(a,x) cerr<<#a<<"\tDEBUG: "<<#x<<" = "<<(x)<<endl using namespace std; const int maxn=505*2,maxm=1e5; int n,m,s,t; int tot,cur[maxn],head[maxn],to[maxm],nxt[maxm]; long long w[maxm]; long long ans,sum[2][maxn]; void clear(){ memset(sum,0,sizeof(sum)); memset(head,0,sizeof(head)); tot=1;ans=0; } void add_edge(int u,int v,long long c){ to[++tot]=v; nxt[tot]=head[u]; w[tot]=c; head[u]=tot; } void add_edges(int u,int v,long long c){ add_edge(u,v,c); add_edge(v,u,0); } int dep[maxn]; bool bfs(){ queue<int>Q; while(!Q.empty())Q.pop(); for(int i=1;i<=t;i++) dep[i]=0; dep[s]=1; Q.push(s); while(!Q.empty()){ int x=Q.front(); Q.pop(); for(int i=head[x];i;i=nxt[i]){ int y=to[i]; if(dep[y]==0&&w[i]){ dep[y]=dep[x]+1; Q.push(y); } } } return dep[t]!=0; } long long dfs(int x,long long flow){ if(x==t)return flow; for(int &i=cur[x];i;i=nxt[i]){ int y=to[i]; if(w[i]&&dep[y]==dep[x]+1){ int tmp=dfs(y,min(flow,w[i])); if(tmp){ w[i]-=tmp; w[i^1]+=tmp; return tmp; } } } return 0; } long long dinic(){ long long res=0; while(bfs()){ for(int i=1;i<=t;i++) cur[i]=head[i]; while(long long d=dfs(s,2e18)) res+=d; } return res; } int main(){ while(scanf("%d%d",&n,&m)!=EOF){ clear(); s=2*n+1;t=s+1; for(int i=1,u,v,a,b,c;i<=m;i++){ scanf("%d%d%d%d%d",&u,&v,&a,&b,&c); a<<=1;b<<=1;c<<=1; ans+=a+b+c; add_edges(u,v,-b+(a+c)/2); add_edges(v,u,-b+(a+c)/2); sum[0][u]+=(b+c)>>1; sum[0][v]+=(b+c)>>1; sum[1][u]+=(a+b)>>1; sum[1][v]+=(a+b)>>1; } for(int i=1;i<=n;i++){ add_edges(s,i,sum[0][i]); add_edges(i,t,sum[1][i]); } printf("%lld\n",(ans-dinic())/2); } return 0; } ```