あいさか たいがblogAisaka_Taiga的博客
//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

网络流学习笔记

Toretto·2022-10-05 20:04·68 次阅读

网络流学习笔记

咕咕咕
此博客参考学长的课件,代码是直接粘过来的,所以不要觉得奇怪。

网络

前言:关于网络流有个生动的比喻,想象一个自来水厂向各处供水,自来水厂有无限多的水,但每条管子单位时间内允许的最大流量有限,现在钦定一个出水口为汇点,现在要做的就是在满足每一条管子不爆的情况下,最大化汇点流出的水量。

一、几个定义#

1. 网络#

对于有向图G=(V,E),其中每条边(u,v)E都有权值wij,称之为容量,图中有两个特殊的点s,t(st) ,称s为源点, t为汇点,这个图称为网络。

2. 流#

对于任意的(u,v)E,称f(u,v)(u,v)边的流量,f(u,v)恒满足:

  1. f(u,v)w(u,v)即一条边的流量不能超过其容量。
  2. f(u,v)=f(v,u)即一条边的流量与其反向边的流量互为相反数。
  3. xE{s,t},(u,v)Ef(u,x)=(x,v)Ef(x,v)即流入一个点的流量等于流出这个点的流量。

3. 残量网络#

对于所有的w(u,v)f(u,v)>0的边组成的网络,称其为残量网络,残量网络中的边可能不属于E,具体原因等下解释。

4. 增广路#

在原图G或其某一个残量网络中,一条每条边的剩余容量都大于0的从st的路径,称为一条增广路。

二、最大流#

这就是前言中所提到的那个问题了。
一个比较容易想到的思路是,不断地在残量网络中找寻增广路,直到没有增广路,此时
的总流量即为最大流,但这个做法有点问题,例如下面这张图:

我们假设第一次增广,找到了1>2>3>4这条边,于是残量网络变成了这样:

这里做了个近似,我们直接把边的流量改为其残余容量。
此时已经无法继续增广了,算法结束,但不难发现,其实走1>3>41>2>4总流量为2,这更优。
那怎么办?
我们考虑给程序一个反悔的机会,也就是说,建立一种方法,使得已经流过了某条边的流量再流回去,也就是建立反向边,为了保持总容量不变,反向边初始容量为0

那么这时如果再走1>2>3>4,残量网络变成了这样:

依然是为了保持总容量不变,在扣除正向边容量的同时,要给反向边加上相等的容量。
这时还可以继续增广:走1>3>2>4,惊奇的发现,23的流量又让3给退回去了!而此时相当于选择了两条路径:1>3>41>2>4,总流量为2,得到了正确的结果。

FF算法#

最暴力的最大流算法,每次直接dfs找增广路,找不到了就完成。

Copy
#include<bits/stdc++.h> #define ll long long //#define int long long #define lc(k) k<<1 #define rc(k) k<<1|1 using namespace std; const int MAX=1e5+10; const int MOD=1e9+7; inline char readchar() { static char buf[100000], *p1 = buf, *p2 = buf; return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++; } inline int read() { #define readchar getchar int res = 0, f = 0; char ch = readchar(); for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1; for(; isdigit(ch); ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0'); return f ? -res : res; } inline void write(int x) { if(x<0) { putchar('-'); x=-x; } if(x>9) write(x/10); putchar(x%10+'0'); } int n,m,be,en; struct node { int v,w,inv; };//由于是vector存图,所以需要整一个变量专门记录反向边 vector<node> s[MAX]; int vis[MAX]; int dfs(int k=be,int flow=1e9) { if(k==en) return flow; vis[k]=1; for(node &v:s[k]) { int c; if(v.w>0&&!vis[v.v]&&((c=dfs(v.v,min(v.w,flow)))!=-1)) { v.w-=c;//本边剩余流量-c s[v.v][v.inv].w+=c;//反边流量+c return c;//找到增广路了 } } return -1;//找不到增广路了,算法结束 } int FF() { int ans=0,c; while((c=dfs())!=-1) { memset(vis,0,sizeof vis); ans+=c; } return ans; } signed main() { n=read(),m=read(),be=read(),en=read(); for(int i=1; i<=m; i++) { int u=read(),v=read(),w=read(); s[u].push_back((node) { v,w,(int)s[v].size() });//两边互为反向边 s[v].push_back((node) { u,0,(int)s[u].size()-1 }); } cout<<FF(); return 0; }

这个算法就是慢,板子题都过不去。
考虑这个算法为啥这么慢,主要原因还是dfs好绕远路,每次找到的不是最短的增广路,所以复杂度没有保障。
你dfs T 飞了你会想啥?
正常人应该都会想到bfs,于是就有了EK算法。

EK算法#

如上所述,EK就是bfs版的FF算法。
但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:
由于vector写EK很麻烦,于是我用了前向星。
当然这份代码也是学长的。

Copy
#include<bits/stdc++.h> #define ll long long #define int long long #define lc(k) k<<1 #define rc(k) k<<1|1 using namespace std; const int MAX=1.2e5+10; const int MOD=1e9+7; inline char readchar() { static char buf[100000], *p1 = buf, *p2 = buf; return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2) ? EOF : *p1++; } inline int read() { #define readchar getchar int res = 0, f = 0; char ch = readchar(); for(; !isdigit(ch); ch = readchar()) if(ch == '-') f = 1; for(; isdigit(ch); ch = readchar()) res = (res << 1) + (res << 3) + (ch ^ '0'); return f ? -res : res; } inline void write(int x) { if(x<0) { putchar('-'); x=-x; } if(x>9) write(x/10); putchar(x%10+'0'); } int n,m,be,en,cnt=1; int vis[MAX],let[MAX],flow[MAX]; int head[MAX]; struct node { int net,to,w; } edge[MAX<<1]; void add(int u,int v,int w) { edge[++cnt]=(node) { head[u],v,w }; head[u]=cnt; return ; } int bfs() { memset(let,0,sizeof let); queue<int> q; q.push(be); flow[be]=1e9; while(!q.empty()) { int ff=q.front(); q.pop(); if(ff==en) break; for(int i=head[ff]; i; i=edge[i].net) { int v=edge[i].to,w=edge[i].w; if(w>0&&!let[v]) { let[v]=i; flow[v]=min(flow[ff],w); q.push(v); } } } return let[en]; } int EK() { int mx=0; while(bfs()) { mx+=flow[en]; for(int i=en; i!=be; i=edge[let[i]^1].to) { edge[let[i]].w-=flow[en]; edge[let[i]^1].w+=flow[en]; } } return mx; } signed main() { n=read(),m=read(),be=read(),en=read(); for(int i=1; i<=m; i++) { int u=read(),v=read(),w=read(); add(u,v,w); add(v,u,0); } cout<<EK(); return 0; }

但是本着精益求精防毒瘤出题人的精神,这个算法还得继续优化。

Dinic算法#

然而,最常用的网络流算法是Dinic算法。作为FF/EK算法的优化,它选择了先用BFS分层,再用DFS寻找。它的时间复杂度上界是O(v2e) 。所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为0不能再走)。我们只往层数高的方向增广,可以保证不走回头路也不绕圈子。
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
这份代码是我自己写的。
模板题

Copy
#include<bits/stdc++.h> #define int long long #define NN 200010 #define N 10010 using namespace std; int n,m,s,t,ans=0,cnt=1,q[NN],l,r;//cnt存放边的数量,ans存放答案,q用于模拟队列 int head[N],nxt[NN],to[NN],val[NN],vis[N];//vis标记此点的深度,存边 inline void add(int u,int v,int w)//建边 { to[++cnt]=v;//终点 val[cnt]=w;//当前边的最大流量 nxt[cnt]=head[u];//赋值头节点 head[u]=cnt;//更新下一个的头节点 } int bfs()//bfs { memset(vis,0,sizeof(vis));//清空vis数组 q[l=r=1]=s;//赋初值 vis[s]=1;//标记不能走了 while(l<=r)//只要没到汇点就一直循环找 { int u=q[l++];//取出队头元素 for(int p=head[u];p;p=nxt[p])//循环每一条与此点相连的点 { int v=to[p];//取出终点 if(val[p]&&!vis[v])//如果当前点没有走过并且当此边是有剩余的流量的时候 { vis[v]=vis[u]+1;//计算当前点的深度 q[++r]=v;//入列 } } } return vis[t];//返回汇点的深度 } int dfs(int u,int in)//u是起点,in是从上一条边流进的流量 { if(u==t)//如果当前的u到达了汇点 return in;//就直接返回当前点的进来的流量 int out=0;//out表示从当前边流出的流量 for(int p=head[u];p&&in;p=nxt[p])//一个一个遍历能够到达的点并且保证流入的流量不为0 { int v=to[p];//取出终点 if(val[p]&&vis[v]==vis[u]+1)//如果当前点的最大流量不为0并且深度是起点加一(保证向汇点进行深搜) { int res=dfs(v,min(val[p],in));//res是当前点的内流向下一条边的最大流量 val[p]-=res;//正向边减去 val[p^1]+=res;//反向边加上 in-=res;//减去 out+=res;//加上 } } if(out==0)//如果流出的为0 vis[u]=0;//标记不能到达汇点下一次不搜了 return out;//返回到达汇点的流量 } signed main() { cin>>n>>m>>s>>t; for(int i=1;i<=m;i++) { int u,v,w; cin>>u>>v>>w; add(u,v,w);//存正向边 add(v,u,0);//存反向边 } while(bfs())//只要有能到达汇点的路线 ans+=dfs(s,1e18);//累加答案 cout<<ans<<endl;//输出 return 0;//好习惯 }

三、最小割#

给一些定义:

  1. 割:对于网络G,其割代表一种点的划分方式,这种划分方式需要满足将G恰好分为两部分S,TsS,tT
  2. 割的容量:表示所有的从ST的边的容量之和,即:w(S,T)=uS,vTw(u,v)
  3. 最小割:容量最小的割即为最小割。
    如何求最小割?
    这里有一条定理,极其简洁的解决了这个问题:
    最大流=最小割。
    我们来试着证明下:
    可以把最小割认为是将一些边割断,使得整个图分为S,T两部分,那么容易得到图中所有的流量必定流经这些边中的某一条(否则无法从s到达t),所以这些边的总流量=图的总流量。
    而边的流量边的容量,
    所以这些边的总流量这些边的总容量,
    所以图的总流量这些边的总容量 ,
    所以流割,
    所以最大流=最小割。
    那么求最小割实际上就是求最大流,这里不在赘述。

四、费用流#

我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用c(u,v) ,于是为了惠民,自来水厂想找到一种方法,使得流最大的同时费用最小,这就是最小费用最大流。
回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,咋办?
最短路。
有负权咋办?
SPFA他活了。
因为有模板题所以这道题代码也是我自己写的。

Copy
#include<bits/stdc++.h> #define INF 0x7fffffff using namespace std; queue<int> q; int head[5001],cost[100001],net[100001],to[100001],val[100001];//cost为费用数组,val为容量 int cnt=1,n,m,xb[5001];//记录下标,便于修改容量 int flow[5001],pre[5001];//前驱节点 int mflow=0,mcost=0;//最大流最小费用 int dis[5001],f[5001];//记录从源点到当前节点的最小的费用值,标记是否在队列中 void add(int x,int y,int c,int z)//建边 { to[++cnt]=y;//存终点 cost[cnt]=z;//存花费 val[cnt]=c;//存边的最大流量 net[cnt]=head[x]; head[x]=cnt; } int BFS(int s,int t)//bfs { memset(dis,127,sizeof(dis));//重置dis数组 memset(f,0,sizeof(f));//清空f数组 int inf=dis[0];//给inf赋初值 while(!q.empty())//把栈清空 q.pop(); for(int i=1; i<=n; i++)//把前驱都赋成-1 pre[i]=-1; f[s]=1;//标记起点入列 dis[s]=0;//原点到当前节点的最小费用为0 pre[s]=0;//没有前驱 flow[s]=INF;//起点的 q.push(s);//起点入列 while(!q.empty())//只要栈不空 { int u=q.front();//取出队头元素 q.pop();//弹出 f[u]=0;//标记出列 for(int i=head[u]; i; i=net[i]) { int v=to[i];//取出终点 if(val[i]>0&&dis[v]>dis[u]+cost[i])//如果当前边的最大流量不为0并且加上当前点的花费比原来要小 { dis[v]=dis[u]+cost[i];//更新花费 pre[v]=u;//更新前驱 xb[v]=i;//存下标 flow[v]=min(flow[u],val[i]);//更新最大流 if(!f[v]) f[v]=1,q.push(v);//没入列就入列 } } } if(dis[t]>=inf) return 0;//如果比起点大就返回0 return 1;//否则返回1 } void max_flow(int s,int t)//算最大值 { while(BFS(s,t))//只要还能到汇点 { int k=t;//存终点 while(k!=s) { val[xb[k]]-=flow[t];//减去 val[xb[k]^1]+=flow[t];//加上 k=pre[k];//更新k值 } mflow+=flow[t];//最大流量 mcost+=flow[t]*dis[t];//最小花费 } } int main() { int s,t; cin>>n>>m>>s>>t; for(int i=1; i<=m; i++) { int x,y,c,d; cin>>x>>y>>c>>d; add(x,y,c,d);//建边 add(y,x,0,-d); } max_flow(s,t); cout<<mflow<<" "<<mcost<<endl;//输出 return 0; }

当然dijkstra也存在一种方法来处理负权图,但这超出了我们的讨论范围以及我和我的学长的认知水平

如果你觉得你行了#

代码及注释因为我太懒了就不写解析了咕咕咕:

Copy
#include<bits/stdc++.h> #define inf 1<<30 using namespace std; int n,p,q,s,t,vi,m_in; struct sb{int v,val,next;} e[101101];//存放已经建好的边 int head[1010],cnt=1,vis[1010],dep[1010];//head是头节点,cnt是边的数量,vis标记此点是否入列,dep表示深度 inline void add(int u,int v,int val)//建边函数 { e[++cnt].v=v;//存终点 e[cnt].val=val;//存每一条边的最大流量 e[cnt].next=head[u];//头节点 head[u]=cnt;//更新 } int bfs()//bfs { memset(vis,0,sizeof(vis));//清空vis数组 memset(dep,0x3f,sizeof(dep));//重置dep数组 queue<int>q;//定义队列用于bfs q.push(s);//放入队列 dep[s]=1;//标记出列 while(!q.empty())//只要队列不空 { int u=q.front();//取出队头元素 q.pop();//弹出队头元素 vis[u]=0;//标记出列 for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边 { int v=e[i].v;//取出终点 if(e[i].val&&dep[v]>dep[u]+1)//如果当前边最大流量不为0并且深度比从起点到终点大 { dep[v]=dep[u]+1;//替换 if(vis[v]==0)//如果不在队列里面 { q.push(v);//放入队列 vis[v]=1;//标记入列 } } } } return dep[t]!=0x3f3f3f3f;//如果能到达汇点返回1,反之返回0 } int dfs(int u,int in)//dfs函数 { if(u==t)//到达汇点了 { vi=1;//标记找到了 return in;//返回当前路线的流量 } int out=0;//流出的流量大小 for(int i=head[u];i;i=e[i].next)//枚举每一条与u相连的边 { int v=e[i].v;//取出终点 if(e[i].val&&dep[v]==dep[u]+1)//如果当前点最大流量不为0并且是向汇点流去 { int res=dfs(v,min(e[i].val,in));//递归找此边的流量大小 e[i].val-=res;//正向边加上 e[i^1].val+=res;//反向边减去 out+=res;//累加流出的流量大小 } if(out==in)break;//如果当前点流出和流入的量相等就直接退出 } if(out==0)//如果当前的流出的流量等于0 vis[u]=0; //标记下一次不搜了 return out;//返回流量大小 } int main() { cin>>n>>p>>q; int f; s=1001,t=1002;//原点,汇点 for(int i=1;i<=n;i++)add(i,i+n,1),add(i+n,i,0);//i表示顾客入点,i+n表示顾客出点,自己与自己建边 for(int i=1;i<=p;i++)add(s,200+i,1),add(200+i,s,0); //200+i表示房间,与原点相连建边 for(int i=1;i<=q;i++)add(300+i,t,1),add(t,300+i,0); //300+i表示菜,与汇点相连建边 for(int i=1;i<=n;i++)//枚举每一个顾客 for(int j=1;j<=p;j++)//枚举每一个房间 { cin>>f;//输入 if(f==1)//喜欢此房间 add(200+j,i,1),add(i,200+j,0);//当前顾客与房间建边 } for(int i=1;i<=n;i++)//枚举每一个顾客 for(int j=1;j<=q;j++)//枚举每一道菜 { cin>>f; if(f==1)//喜欢这道菜 add(i+n,300+j,1),add(300+j,i+n,0);//之前与自己建的边与菜建边 } while(bfs())//只要还能到汇点 { vi=1; while(vi)vi=0,m_in+=dfs(s,inf);//累加答案 } cout<<m_in<<endl;//输出 return 0;//好习惯 }
posted @   北烛青澜  阅读(68)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示
目录