最大流
一些解释:https://www.cnblogs.com/rmy020718/p/9546071.html
应用:最小割、二分图匹配
eg:水流、贷款、道宽(明显
性质:1)能量守恒 2)反对称性 3)容量限制
算法: 1)“增广路”:Edmonds-karp(EK算法)、Dinic
2)"预流推进“:ISAP
ford-fulkerson(FF方法):预留网络、预留网络的反向路径、增广路、割
其思想为:
1.在图上找到一条从源点到汇点的路径(称为‘增广路’)。
2.去增广路上的残量最小值v。(也就是流过的路径中流量最小的那一个)
3.将答案加上v。
4,.将增广路上所有边的残量减去v,反向边的残量加上v。
重复上边4个步骤直到找不到增光路为止,这称作 FF 方法
EK求最大流(FF方法,用BFS计算增广路径): 时间复杂度要大一些
每次寻找最短路进行增广,一个流是最大流时,当且仅当它的残留网络中不包含增广路
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<queue> #define LL long long const int maxn=1e4+10; const int maxm=2e5+10; using namespace std; //luogu 3376 //https://www.cnblogs.com/dx123/p/16320492.html int n,m,s,t; struct node{ int to; LL c; int nex; }e[maxm]; int head[maxn]; LL mf[maxn],pre[maxn]; int idx=1; void adde(int x,int y,LL z){ e[++idx]={y,z,head[x]}; head[x]=idx; } bool bfs(){ memset(mf,0,sizeof(mf)); queue<int> q; q.push(s); mf[s]=1e9; while(q.size()){ int x=q.front(); q.pop(); for(int i=head[x];i;i=e[i].nex){ int v=e[i].to; if(mf[v]==0&&e[i].c){ mf[v]=min(mf[x],e[i].c); pre[v]=i; //存的是边 q.push(v); if(v==t) return 1; } } } return 0; } LL ek(){ LL flow=0; while(bfs()){ int v=t; while(v!=s){ int i=pre[v]; e[i].c-=mf[t]; e[i^1].c+=mf[t]; v=e[i^1].to; } flow+=mf[t]; } return flow; } int main(){ int a,b; LL c; scanf("%d %d %d %d",&n,&m,&s,&t); while(m--){ scanf("%d %d %lld",&a,&b,&c); adde(a,b,c); adde(b,a,0); } printf("%lld\n",ek()); return 0; }
dinic算法
寻找增广路之前确定层次图,优化:当前弧优化
当前弧优化的意思就是说每次开始跑邻接表遍历不是从第一条边开始跑而是从上一次点i遍历跑到的点.
我们用cur[i]表示这个点,之后每次建完分层图之后都要进行初始化,且见分层图时不存在当前弧优化.
//https://www.luogu.com.cn/problem/P3376 //dinic算法:使用当前弧优化 ///复杂度:理论上来说,最慢应该是O((n^2)*m),n表点数,m表边数,实际上呢,应该快得不少 //当前弧优化:在DFS的时候记录当前已经计算到第几条边了,避免重复计算。 //在下一次构建层次网络时注意将head数组还原 // //1、建立网络(包括正向弧和反向弧(初始边权为0)),将总流量置为0 //2、构造层次网络:简单的说,就是求出每个点u的层次,u的层次是从源点到该点的最短路径 //(注意:这个最短路是指弧的权都为1的情况下的最短路),若与源点不连通,层次置为-1. 在这里BFS //3 判断汇点的层次是否为-1 是:再见,算法结束,输出当前的总流量 否:下一步 //4.用一次DFS完成所有增广,增广是什么呢? //增广(我的理解):通过DFS找上述的增广路,找到了之后,将每条边的权都减去该增广路中拥有最小流量的 //边的流量,将每条边的反向边的权增加这个值,同时将总流量加上这个值 //DFS直到找不到一条可行的从原点到汇点的路 //细节处理,如何快速找到一条边的反向边:边的编号从2开始,反向边加在正向边之后,反向边即为该点的编号异或1 #include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<queue> #define LL long long const int maxn=1e4+10; const int maxm=2e5+10; using namespace std; int n,m,s,t; struct node{ int to,nex; LL c; }e[maxm]; int head[maxn],cur[maxn],d[maxn]; //当前弧优化,分层图的度 int idx=1; void adde(int x,int y,LL c){ e[++idx]={y,head[x],c}; head[x]=idx; } bool bfs(){ //对点分层,找增广路 memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=e[i].nex){ int v=e[i].to; if(d[v]==0&&e[i].c){ d[v]=d[u]+1; q.push(v); if(v==t) return 1; } } } return 0; } LL dfs(int u,LL mf){ //多路增广 if(u==t) return mf; LL sum=0; for(int i=cur[u];i;i=e[i].nex){ cur[u]=i; //当前弧优化 int v=e[i].to; if(d[v]==d[u]+1&&e[i].c){ LL f=dfs(v,min(mf,e[i].c)); e[i].c-=f; e[i^1].c+=f; //更新残留网 sum+=f; //累加u的流出流量 mf-=f; //减少u的剩余流量 if(mf==0) break; //余量优化 } } if(sum==0) d[u]=0; //残枝优化 return sum; } LL dinic(){ //累加可行流 LL flow=0; while(bfs()){ memcpy(cur,head,sizeof(head)); flow+=dfs(s,1e9); } return flow; } int main(){ int a,b; LL c; scanf("%d %d %d %d",&n,&m,&s,&t); while(m--){ scanf("%d %d %lld",&a,&b,&c); adde(a,b,c); adde(b,a,0); } printf("%lld\n",dinic()); return 0; }
SAP算法
定义每个结点的距离标号,即残留网络中这个点到汇点的距离,旨在距离标号相邻的点间寻找增广路径,如果从一个点出发没有容许边,就需要对该点进行重新标记并回溯。
优化:GAP优化,如果在距离编号中存在GAP,就不会有增广路,于是算法提前停止。
也可以加上当前弧优化
https://www.cnblogs.com/longdouhzt/archive/2011/09/04/2166187.html
https://www.cnblogs.com/wally/archive/2013/05/03/3054778.html
#include<iostream> #include<cstdio> #include<cstring> #include<algorithm> using namespace std; #define MAXN 444 //邻接表要开边数的2倍 struct Edge{ int v,cap,next; }edge[MAXN]; int level[MAXN];//标记层次(距离标号) //间隙优化,定义gap[i]为标号是i的点的个数 //在重标记i时,检查gap[level[i]],若减为0,这算法结束。 int gap[MAXN]; int pre[MAXN];//前驱 int cur[MAXN]; int head[MAXN]; int NV,NE; //NE为边数,初始化为0; void Insert(int u,int v,int cap,int cc=0){ edge[NE].cap=cap;edge[NE].v=v; edge[NE].next=head[u];head[u]=NE++; edge[NE].cap=cc;edge[NE].v=u; edge[NE].next=head[v];head[v]=NE++; } //参数,源点,汇点 int SAP(int vs,int vt){ memset(level,0,sizeof(level)); memset(pre,-1,sizeof(pre)); memset(gap,0,sizeof(gap)); //cur[i]保存的是当前弧 for(int i=0;i<=NV;i++)cur[i]=head[i]; int u=pre[vs]=vs;//源点的pre还是其本身 int maxflow=0,aug=-1; gap[0]=NV; while(level[vs]<NV){ loop : for(int &i=cur[u];i!=-1;i=edge[i].next){ int v=edge[i].v;//v是u的后继 //寻找可行弧 if(edge[i].cap&&level[u]==level[v]+1){ //aug表示增广路的可改进量 aug==-1?(aug=edge[i].cap):(aug=min(aug,edge[i].cap)); pre[v]=u; u=v; //如果找到一条增广路 if(v==vt){ maxflow+=aug;//更新最大流; //路径回溯更新残留网络 for(u=pre[v];v!=vs;v=u,u=pre[u]){ //前向弧容量减少,反向弧容量增加 edge[cur[u]].cap-=aug; edge[cur[u]^1].cap+=aug; } aug=-1; } goto loop; } } int minlevel=NV; //寻找与当前点相连接的点中最小的距离标号(重标号) for(int i=head[u];i!=-1;i=edge[i].next){ int v=edge[i].v; if(edge[i].cap&&minlevel>level[v]){ cur[u]=i;//保存弧 minlevel=level[v]; } } if((--gap[level[u]])==0)break;//更新gap数组后如果出现断层,则直接退出。 level[u]=minlevel+1;//重标号 gap[level[u]]++;//距离标号为level[u]的点的个数+1; u=pre[u];//转当前点的前驱节点继续寻找可行弧 } return maxflow; } int main(){ int m;//边的条数 while(~scanf("%d%d",&m,&NV)){ memset(head,-1,sizeof(head)); NE=0; for(int i=1;i<=m;i++){ int u,v,cap; scanf("%d%d%d",&u,&v,&cap); Insert(u,v,cap); } printf("%d\n",SAP(1,NV)); } return 0; }
另一种写法
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=20010; const int maxm=880010; const int INF=0x3fffffff; typedef long long LL; //https://www.cnblogs.com/kuangbin/archive/2012/09/29/2707955.html struct node{ int from,to,nex; int cap; }ed[maxn]; int tot; int head[maxn],dep[maxn],gap[maxn]; //gap[x]=y :说明残留网络中dep[i]==x的个数为y int n;//n是总的点的个数,包括源点和汇点 void inti(){ tot=0; memset(head,-1,sizeof(head)); } void adde(int x,int y,int z){ ed[tot].from=x;ed[tot].to=y;ed[tot].cap=z; ed[tot].nex=head[x]; head[x]=tot++; ed[tot].from=y;ed[tot].to=x;ed[tot].cap=0; //反向建边 ed[tot].nex=head[y]; head[y]=tot++; } void bfs(int st,int end){//从汇点进行一次BFS算出距离标号 memset(dep,-1,sizeof(dep)); memset(gap,0,sizeof(gap)); gap[0]=1; int que[maxn]; //模拟队列 int head=0,tail=0; dep[end]=0; que[tail++]=end; //end先入队 while(head!=tail){ int u=que[head++]; if(head==maxn) head=0; for(int i=head[u];i+1;i=ed[i].nex){ int v=ed[i].to; if(dep[v]!=-1) continue; que[tail++]=v; if(tail==maxn) rear=0; dep[v]=dep[u]+1; ++gap[dep[v]]; //gap优化 } } } int sap(int st,int end){ //从源点开始递归 int res=0; bfs(st,end); int cur[maxn]; int s[maxn]; //记录路径的 int top=0; memcpy(cur,head,sizeof(head)); //每次先赋值 int u=st; while(dep[st]<n){ if(u==end){ //当i-->j时容许边并且j是汇点时,从汇点开始增广并从源点开始递归(重来一次) int temp=INF; int inser; for(int i=0;i<top;i++){ if(temp>ed[s[i]].cap){ //记录最小流 temp=ed[s[i]].cap; inser=i; } } for(int i=0;i<top;i++){ ed[s[i]].cap-=temp; ed[s[i]^1].cap+=temp; //更新残留网络 } res+=temp; top=inser; //这是不完全退回去的意思?? u=ed[s[top]].from; } if(u!=end&&gap[dep[u]-1]==0) break;//出现断层,无增广路 int i; for(i=cur[u];i!=-1;i=ed[i].nex){ if(ed[i].cap!=0&&dep[u]==dep[ed[i].to]+1) break; //找到了增广路 } if(i!=-1){ cur[u]=i; //这条边 当前弧优化的意思就是说每次开始跑邻接表遍历不是从第一条边开始跑而是从上一次点i遍历跑到的点. s[top++]=i; u=ed[i].to; } //如果找不到增广路,就对u进行重新标号,使得d[i]=mind[j]+1,并回溯 else{ int minn=n; for(i=head[u];i!=-1;i=ed[i].nex){ if(ed[i].cap==0) continue; if(minn>dep[ed[i].to]){ minn=dep[ed[i].to]; cur[u]=i; } } --gap[dep[u]]; dep[u]=minn+1; ++gap[dep[u]]; if(u!=st) u=ed[s[--top]].from; } } return res; } int main(){ return 0; }
最小割:s-t最小割,把有向流网络G=(V,E),割把图分为S,T(V-S)两部分,源点s属于S,汇点t属于T
最大流最小割原理:最小割的容量=最大流的流量
洛谷 1344
可能有的问题:(1)最小割是多少, (2)哪些的S,哪些是T (3)最小割需要多少代价(多少条边)
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<queue> #define LL long long const int maxn=1e4+10; const int maxm=2e5+10; using namespace std; //最小割 //https://www.luogu.com.cn/problem/P1344 int n,m,s,t; struct node{ int to,nex; LL c; }e[maxm]; int head[maxn],cur[maxn],d[maxn]; //当前弧优化,分层图的度 int idx=1; int vis[maxn]; void adde(int x,int y,LL c){ e[++idx]={y,head[x],c}; head[x]=idx; } bool bfs(){ //对点分层,找增广路 memset(d,0,sizeof(d)); queue<int> q; q.push(s); d[s]=1; while(q.size()){ int u=q.front(); q.pop(); for(int i=head[u];i;i=e[i].nex){ int v=e[i].to; if(d[v]==0&&e[i].c){ d[v]=d[u]+1; q.push(v); if(v==t) return 1; } } } return 0; } LL dfs(int u,LL mf){ //多路增广 if(u==t) return mf; LL sum=0; for(int i=cur[u];i;i=e[i].nex){ cur[u]=i; //当前弧优化 int v=e[i].to; if(d[v]==d[u]+1&&e[i].c){ LL f=dfs(v,min(mf,e[i].c)); e[i].c-=f; e[i^1].c+=f; //更新残留网 sum+=f; //累加u的流出流量 mf-=f; //减少u的剩余流量 if(mf==0) break; //余量优化 } } if(sum==0) d[u]=0; //残枝优化 return sum; } LL dinic(){ //累加可行流 LL flow=0; while(bfs()){ memcpy(cur,head,sizeof(head)); flow+=dfs(s,0x7fffffffffffffff); } return flow; } int a[maxn],b[maxn]; int main(){ scanf("%d %d",&n,&m); s=1;t=n; for(int i=1;i<=m;i++){ LL c; scanf("%d %d %lld",&a[i],&b[i],&c); adde(a[i],b[i],c); adde(b[i],a[i],0); } printf("%lld ",dinic()); //求最小割的最少边数 idx=1; memset(head,0,sizeof(head)); for(int i=1;i<=m;i++){ adde(a[i],b[i],1); adde(b[i],a[i],0); } printf("%lld\n",dinic()); return 0; } //最后一个点过不了 #include<cstdio> #include<cstring> #include<algorithm> #include<queue> #define int long long//注意边数乘边权会爆int using std::queue; const int N=1005,M=2005; int head[N],level[N],cur[N]; struct Edge { int next,to,c; }; Edge E[M<<1]; void __add(int u,int v,int c) { static int tot=-1; E[++tot].next=head[u]; E[tot].to=v; E[tot].c=c; head[u]=tot; } bool bfs(int s,int t) { memset(level,0x00,sizeof(level)); queue<int> q; q.push(s); level[s]=1; while(!q.empty()) { int u=q.front(); q.pop(); for(int i=head[u];~i;i=E[i].next) { int v=E[i].to; if(level[v]||E[i].c==0)continue; level[v]=level[u]+1; q.push(v); } } return level[t]; } int dfs(int u,int t,int flow) { if(u==t||flow==0)return flow; int now=flow; for(int i=cur[u];~i;cur[u]=i=E[i].next) { int v=E[i].to; if(level[v]!=level[u]+1||E[i].c==0)continue; int f=dfs(v,t,std::min(now,E[i].c)); now-=f; E[i].c-=f; E[i^1].c+=f; if(now==0)break; } return flow-now; } int dinic(int s,int t) { int ans=0; while(bfs(s,t)) { memcpy(cur,head,sizeof(cur)); ans+=dfs(s,t,0x7fffffffffffffff);//无穷大别开小了,会死循环 } return ans; } signed main() { memset(head,0xff,sizeof(head)); int n,m; scanf("%lld%lld",&n,&m); for(int i=0;i<m;++i) { int u,v,c; scanf("%lld%lld%lld",&u,&v,&c); __add(u,v,c*(m*2+1)+1); __add(v,u,0); } int res=dinic(1,n); printf("%lld %lld",res/(m*2+1),res%(m*2+1)); }
最小费用最大流:
含义:每条边有最小性参数和可加性参数,how:从零流开始,每次增加一个最小费用路径,经过多次增广, 知道无法再增加路径,就得到了最大流
由于残留网络是用到了反向边,所有肯定有负边权,所以最短路算法只能用bellman-ford或者是spfa算法,如果求最大流用FF方法并用BFS求增广路也就是EK算法的话,那么就可以用解决办法:
FF方法+bellman-ford(spfa)解决,复杂度为O(KVE),K是总流量
【例子】:一个无向图,N个点,M条边,一个人从1号点走到N号点,再从N号点走到1号点,每条路只能走一次,求来回的总长度最短的路线
【分析】:不能直接用最短路求(很好理解,画图),而是应该有最小费用最大流解决,把每条边的流量设为1,表示每条边只能用一次,把边的长度看作每个边的费用,在图中添加超级源点s(n+1)和超级汇点t(n+2),s到1有一个长度为0,容量为2的边,n到t有一个长度为0,容量为2的边,之后,最短路径的费用等于源点s到汇点t的最小费用最大流
下面是SPFA+FF算法+邻接表
注意无向图转化为有向图,一条无向边变为4条边:(1)无向边(u,v)分为两条有向边(u,v),(v,u),正向边费用为cost,容量为1,反向边费用-cost,容量为0
#include<iostream> #include<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=1010; const int INF=0x3fffffff; typedef long long LL; //最小费用最大流 //SPFA+最大流(FF算法)+邻接表 int dis[maxn],pre[maxn],preve[maxn]; int n,m; struct edge{ int to,cost,capc,rev; //rev是前驱节点 edge(int _to,int _cost,int _capc,int _rev){ to=_to;cost=_cost;capc=_capc;rev=_rev; } }; vector<edge> e[maxn]; void addedge(int from,int to,int cost,int cap){ e[from].push_back(edge(to,cost,cap,e[to].size())); //把1个有向边分为2ge e[to].push_back(edge(from,-cost,0,e[from].size()-1)); } bool spfa(int s,int t,int cnt){ //spfa模板 bool inq[maxn]; memset(pre,-1,sizeof(pre)); for(int i=1;i<=cnt;i++){ dis[i]=INF;inq[i]=false; } dis[s]=0; queue<int> q; q.push(s); inq[s]=1; while(!q.empty()){ int u=q.front(); q.pop(); inq[u]=false; for(int i=0;i<e[u].size();i++){ if(e[u][i].capc>0){ int v=e[u][i].to; int cos=e[u][i].cost; if(dis[u]+cos<dis[v]){ dis[v]=dis[u]+cos; //更新距离 pre[v]=u; //v的前驱是u preve[v]=i; //u的第i个边连接v点 if(!inq[v]){ inq[v]=true; q.push(v); } } } } } return dis[t]!=INF; } int mincost(int s,int t,int cnt){ int cost=0; while(spfa(s,t,cnt)){ int v=t,flow=INF; //每次增加的流量 while(pre[v]!=-1){ //回溯整个路径,计算路径的流 int u=pre[v],i=preve[v]; flow=min(flow,e[u][i].capc); //所有边的最小容量就是这条路的流 v=u; } v=t; //更新残留网络 while(pre[v]!=-1){ int u=pre[v],i=preve[v]; e[u][i].capc-=flow; //正向减 e[v][e[u][i].rev].capc+=flow; //反向加 v=u; } cost+=dis[t]*flow; //费用累加 } return cost; } int main(){ while(~scanf("%d %d",&n,&m)){ for(int i=0;i<n;i++) e[i].clear(); for(int i=1;i<=m;i++){ int u,v,w; scanf("%d %d %d",&u,&v,&w); addedge(u,v,w,1); //一个无向边分为2个有向边 addedge(v,u,w,1); } int s=n+1,t=n+2; //超级源点、超级汇点 addedge(s,1,0,2); addedge(n,t,0,2); printf("%d\n",mincost(s,t,n+2)); } return 0; }
P3381 【模板】最小费用最大流
#include<iostream> #include<cstdio> #include <cstring> #include <algorithm> #include <vector> #include<queue> #define LL long long #define INF 1e12 const int maxn=5e3+10; const int maxm=1e5+10; using namespace std; //最小费用最大流 BFS-->SPFA + EK算法 //spfa就是把费用拿来求最短路 int n,m,s,t; struct node{ int to,w,c,nex; }e[maxm]; int head[maxn],idx=1; void adde(int a,int b,int c,int w){ e[++idx]={b,w,c,head[a]}; head[a]=idx; } int d[maxn],mf[maxn],pre[maxn],vis[maxn]; bool spfa(){ memset(d,0x3f,sizeof(d)); d[s]=0; vis[s]=1; memset(mf,0,sizeof(mf)); queue<int> q; q.push(s); mf[s]=INF; while(q.size()){ int u=q.front(); q.pop();vis[u]=0; for(int i=head[u];i;i=e[i].nex){ int v=e[i].to; int w=e[i].w; int c=e[i].c; if(d[v]>d[u]+w&&c){ d[v]=d[u]+w; mf[v]=min(c,mf[u]); pre[v]=i; //记录的是边!!!边!!!! if(!vis[v]){ q.push(v);vis[v]=1; } } } } return mf[t]>0; } int flow,cost; void ek(){ while(spfa()){ for(int v=t;v!=s;){ int i=pre[v]; e[i].c-=mf[t]; e[i^1].c+=mf[t]; v=e[i^1].to; } flow+=mf[t]; cost+=mf[t]*d[t]; } } int main(){ scanf("%d %d %d %d",&n,&m,&s,&t); for(int i=0;i<m;i++){ int a,b,c,w; scanf("%d %d %d %d",&a,&b,&c,&w); adde(a,b,c,w); adde(b,a,0,-w); } ek(); printf("%d %d",flow,cost); return 0; }