最大流
一些解释: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<cstring> #include<cmath> #include<algorithm> #include<stack> #include<cstdio> #include<queue> #include<map> #include<vector> #include<set> using namespace std; const int maxn=330; const int INF=0x3fffffff; typedef long long LL; //最大流问题 int n,m,g[maxn][maxn],pre[maxn]; int bfs(int s,int t){ int flow[maxn]; //节点容量 memset(pre,-1,sizeof(pre)); flow[s]=INF;pre[s]=0; queue<int> q; q.push(s); while(!q.empty()){ //找一条增广路,并且返回这条路上的最小流量 int u=q.front(); q.pop(); if(t==u) break; //搜到一个路径,这次BFS结束 for(int i=1;i<=m;i++){ if(i!=s&&g[u][i]>0&&pre[i]==-1){ //!!!这个判断 pre[i]=u; //记录路径 q.push(i); flow[i]=min(flow[u],g[u][i]); //更新节点流量 } } } if(pre[t]==-1) return -1; //没有找到新的增广路 return flow[t]; } int maxflow(int s,int t){ int maxfl=0; while(1){ int flow=bfs(s,t); if(flow==-1) break; int cur=t; while(cur!=s){ //更新残留网络 int fa=pre[cur]; g[fa][cur]-=flow; //正向减 g[cur][fa]+=flow; //反向加 cur=fa; } maxfl+=flow; } return maxfl; } int main(){ while(~scanf("%d %d",&n,&m)){ memset(g,0,sizeof(g)); for(int i=0;i<n;i++){ int u,v,dis; scanf("%d %d %d",&u,&v,&dis); g[u][v]+=dis; //!!!!!!可能又重边,而且这是有向图 } printf("%d ",maxflow(1,m)); } return 0; }
dinic算法
寻找增广路之前确定层次图,优化:当前弧优化
当前弧优化的意思就是说每次开始跑邻接表遍历不是从第一条边开始跑而是从上一次点i遍历跑到的点.
我们用cur[i]表示这个点,之后每次建完分层图之后都要进行初始化,且见分层图时不存在当前弧优化.
#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; //dinic算法:使用当前弧优化 ///复杂度:理论上来说,最慢应该是O((n^2)*m),n表点数,m表边数,实际上呢,应该快得不少 //当前弧优化:在DFS的时候记录当前已经计算到第几条边了,避免重复计算。 //在下一次构建层次网络时注意将head数组还原 // //1、建立网络(包括正向弧和反向弧(初始边权为0)),将总流量置为0 //2、构造层次网络:简单的说,就是求出每个点u的层次,u的层次是从源点到该点的最短路径 //(注意:这个最短路是指弧的权都为1的情况下的最短路),若与源点不连通,层次置为-1. 在这里BFS //3 判断汇点的层次是否为-1 是:再见,算法结束,输出当前的总流量 否:下一步 //4.用一次DFS完成所有增广,增广是什么呢? //增广(我的理解):通过DFS找上述的增广路,找到了之后,将每条边的权都减去该增广路中拥有最小流量的 //边的流量,将每条边的反向边的权增加这个值,同时将总流量加上这个值 //DFS直到找不到一条可行的从原点到汇点的路 //细节处理,如何快速找到一条边的反向边:边的编号从0开始,反向边加在正向边之后,反向边即为该点的编号异或1
int deep[N+1]; int q[N+1]= {0},h,t; int cur[N+1]; bool bfs(int S,int T) { for (int i=0; i<=n; i++) deep[i]=0; //初始化深度为0 h=t=1; q[1]=S; deep[S]=1; while (h<=t) { for (int i=lin[q[h]]; i; i=e[i].next) if (!deep[e[i].y]&&e[i].v) //若未计算过深度且这条边不能是空的 { q[++t]=e[i].y; //入队一个节点 deep[q[t]]=deep[q[h]]+1; //计算深度 } ++h; } if (deep[T]) return true; else return false; } int dfs(int start,int T,int minf) { if (start==T) return minf; //若到了汇点直接返回前面流过来的流量 int sum=0,flow=0; for (int &i=cur[start]; i; i=e[i].next) //当前弧优化,运用指针在修改i的同时,将cur[start]顺便修改 if (e[i].v&&deep[start]+1==deep[e[i].y]) { flow=dfs(e[i].y,T,min(minf,e[i].v)); //继续找增广路 if (!flow) deep[e[i].y]=0; //去掉已经增广完的点 sum+=flow; //统计最大流 minf-=flow; //剩余容量 e[i].v-=flow; e[i^1].v+=flow; //更新剩余容量 if (!minf) return sum; //若前面已经流完了,直接返回 } return sum; //返回最大流量 } int maxflow(int S,int T) { int sum=0,minf; while (1) //while(1) 控制循环 { if (!bfs(S,T)) return sum; //bfs求出分层图,顺便判断是否有增广路 for (int i=1; i<=n; i++) cur[i]=lin[i]; //当前弧的初始化 minf=dfs(S,T,INF); //dfs求出流量 if (minf) sum+=minf; //若流量不为0,加入 else return sum; //流量为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=110; const int INF=0x3fffffff; typedef long long LL; typedef unsigned long long ull; int f[maxn][maxn]; int pre[maxn],cow[maxn],d[maxn]; int n,m; int bfs(int s){ //求出层次图 int q[maxn]; q[0]=s; memset(d,-1,sizeof(d)); //层次 d[s]=0; int front=0,rear=1; int j; while(front<rear){ int t=q[front++]; for(j=0;j<=n+1;j++){ if(d[j]==-1&&f[t][j]>0){ d[j]=d[t]+1; q[rear++]=j; } } } if(d[n+1]>=0) return 1; else return 0; } int dinic(int t,int sum){ int i; if(t==n+1) return sum; //可以返回了 int os=sum; for(int i=0;i<=n+1;i++){ if(d[i]==d[t]+1&&f[t][i]>0){ int a=dinic(i,min(sum,f[t][i])); f[t][i]-=a; f[i][t]+=a; sum-=a; } } return os-sum; //还能增加的最大值 } int main(){ cin>>n>>m; int tot=0,x,y; for(int i=1;i<=m;i++){ cin>>cow[i]; tot+=cow[i]; } int num; for(int i=1;i<=n;i++){ cin>>num; for(int j=1;j<=num;j++){ cin>>x; if(pre[x]==0) f[0][i]+=cow[x]; else f[pre[x]][i]=tot; //初值为最大值 pre[x]=i; } cin>>x; f[i][n+1]=x; } int res=0; while(bfs(0)){ res+=dinic(0,INF); } cout<<res<<endl; 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 ",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
最大流最小割原理:最小割的容量=最大流的流量
最小费用最大流:
含义:每条边有最小性参数和可加性参数,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 ",mincost(s,t,n+2)); } return 0; }
二分图匹配:
含义:把无向图G=(V,E),分为两个集合V1,V2,所有的边都在V1,V2之间,而V1或V2内部没有边,V1中的一个点与V2中的一个点关联,称为一个匹配。
一个图是不是二分图,一般通过染色判断,用两种颜色对所有顶点染色,如果相邻顶点的颜色都不同那么就是二分图。
ps。一个图是二分图当且仅当它不含边的数量为奇数的点
常见问题:
(1)无权图:求包含边数最多的匹配,求二分图的最大匹配
(2)带权图:求边权和最大的匹配,KM算法
QUS1:求二分图的最大匹配
做法1:转化为最大流求解
把每个边转化为有向边,流量为1,在V1上加一个人为的源点s(连接所以的V1),在V2上加上一个人为的汇点t(连接所有的V2),然后求s-->t的最大流即可
所有弧的容量都是1,这样饱和弧就对应着匹配边,最大流就是最大匹配
做法2:匈牙利求解(更简单)
可以看作是最大流的特殊实现,因为二分图是一个很简单的图,所以对s,t的操作是多余的,可以直接从V1开始找增广路径
如果用邻接矩阵:时间O(V^3),空间O(V^2)
如果有邻接表:时间O(VE),空间O(V+E)
#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; //匈牙利算法求最大二分图匹配 int g[510][510]; int match[510]; //匹配结果 int reverb[510]; int k,m,m_girl,n_boy; bool dfs(int x){ //找一个增广路径,给女孩x找一个配对男孩 for(int i=1;i<=n_boy;i++){ if(!reverb[i]&&g[x][i]){ reverb[i]=1; //预定男孩,给女孩x //上面是 if(!match[i]||dfs(match[i])){ //情况1:男孩x还没有匹配,就直接分 //情况2:如果男孩i已经配对,尝试dfs更换原有配对,更换成功后,就有属于女孩x match[i]=x; return true; } } } return false; //没有喜欢的男孩 或者 更换不成功 } int main(){ while(scanf("%d",&k)!=EOF&&k){ scanf("%d %d",&m_girl,&n_boy); memset(g,0,sizeof(g)); memset(match,0,sizeof(match)); for(int i=0;i<k;i++){ int a,b; scanf("%d %d",&a,&b); g[a][b]=1; } int summ=0; for(int i=1;i<=m_girl;i++){ memset(reverb,0,sizeof(reverb)); if(dfs(i)) summ++; } printf("%d ",summ); } return 0; }