【科技】 网络流学习笔记
网络瘤
前言:关于网络流有个生动的比喻,想象一个自来水厂向各处供水,自来水厂有无限多的水,但每条管子单位时间内允许的最大流量有限,现在钦定一个出水口为汇点,现在要做的就是在满足每一条管子不爆的情况下,最大化汇点流出的水量。
一、几个定义
1.网络
对于有向图
2.流
对于任意的
(1)
(2) 若
(3)
3.残量网络
对于所有的
4.增广路
在原图
二、最大流
这就是前言中所提到的那个问题了。
一个比较容易想到的思路是,不断地在残量网络中找寻增广路,直到没有增广路,此时的总流量即为最大流,但这个做法有点问题,例如下面这张图:

我们假设第一次增广,找到了

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

那么这时如果再走

依然是为了保持总容量不变,在扣除正向边容量的同时,要给反向边加上相等的容量。
这时还可以继续增广:走
算法一、FF算法
最暴力的最大流算法,每次直接dfs找增广路,找不到了就完成。
#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好绕远路,每次找到的不是最短的增广路,所以复杂度没有保障。
你dfsT飞了你会想啥?
正常人应该都会想到bfs,
于是就有了EK算法。
算法二、EK算法
如上所述,EK就是bfs版的FF算法。
但是由于没有了系统栈的加持,我们只能另开一个数组来存路径,具体看代码:
由于vector写EK很麻烦,于是我用了前向星。
//事实证明网络流还是用链式前向星吧
#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寻找。它的时间复杂度上界是
所谓分层,其实就是预处理出源点到每个点的距离(注意每次循环都要预处理一次,因为有些边可能容量变为
我们可以使用多路增广节省很多花在重复路线上的时间:在某点DFS找到一条增广路后,如果还剩下多余的流量未用,继续在该点DFS尝试找到更多增广路。
此外还有当前弧优化。因为在Dinic算法中,一条边增广一次后就不会再次增广了,所以下次增广时不需要再考虑这条边。我们把head数组复制一份,但不断更新增广的起点。
#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,cnt=1;
int vis[MAX],let[MAX],flow[MAX];
int head[MAX],dep[MAX],cur[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 ;
}
bool bfs()
{
queue<int> q;
memset(dep,0,sizeof dep);
dep[be]=1;
q.push(be);
while(!q.empty())
{
int ff=q.front();q.pop();
for(int i=head[ff];i;i=edge[i].net)
if(!dep[edge[i].to]&&edge[i].w)
{
dep[edge[i].to]=dep[ff]+1;
q.push(edge[i].to);
if(edge[i].to==en) return 1;//分层完毕
}
}
return 0;//没搜到汇点,没有增广路了
}
int dfs(int k,int mf)//mf代表当前节点现在能够供给的最大流量
{
if(k==en) return mf;
int sum=0;
for(int i=cur[k];i;i=edge[i].net)//多路增广
{
cur[k]=i;//当前弧优化,本次dfs的下次访问直接从当前弧开始
int v=edge[i].to,&w=edge[i].w;
if(dep[v]==dep[k]+1&&w)//分层限制递归层数
{
int f=dfs(v,min(mf,w));
sum+=f;mf-=f;
w-=f;edge[i^1].w+=f;
if(mf==0) break;//该节点没有流量了,停止遍历
}
}
if(sum==0) dep[k]=0;//若该节点到不了汇点,将深度置为0,防止下次再次访问
return sum;
}
int Dinic()
{
int ans=0;
while(bfs())
{
memcpy(cur,head,sizeof head);//把head弧拷贝到当前弧去
ans+=dfs(be,1e9);
}
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();
add(u,v,w);add(v,u,0);
}
cout<<Dinic();
return 0;
}
优化效果很可观:
另外提一下,Dinic在二分图上的复杂度是
此外,还有一些求最大流的算法,如ISAP,预流推进等,有兴趣可以了解一下(艹,谁会有兴趣)。
三、最小割
给一些定义:
1.割:对于网络
2.割的容量:表示所有的从
3.最小割:容量最小的割即为最小割。
如何求最小割?
这里有一条定理,极其简洁的解决了这个问题:
最大流
我们来试着证明下:
可以把最小割认为是将一些边割断,使得整个图分为
而边的流量
所以这些边的总流量
所以图的总流量
所以流
所以最大流
那么求最小割实际上就是求最大流,这里不再赘述。
四、费用流
我们把前言里改一下,现在自来水厂想赚钱,于是每一单位的水流经某一条管时需要收取一定费用
回想一下前面的EK算法,我们找增广路时是随机找的,现在我不随机找了,我给每个点一个花费,我想要每次都在残量网络中找到花费最小的,咋办?
最短路。
有负权咋办?
spfa。
但它不是死了吗?
怎么会有出题人在负权图上卡spfa
#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 head[MAX];
struct node{int to,net,w,c;}edge[MAX<<1];
int n,m,be,en,cnt=1,let[MAX],dis[MAX],flow[MAX],vis[MAX];
void add(int u,int v,int w,int c)
{
edge[++cnt]=(node){v,head[u],w,c};
head[u]=cnt;
return ;
}
bool spfa()
{
memset(let,0,sizeof let);
memset(flow,0,sizeof flow);
memset(vis,0,sizeof vis);
memset(dis,0x3f3f3f3f,sizeof dis);
queue<int> q;q.push(be);
dis[be]=0;vis[be]=1;flow[be]=1e9;
while(!q.empty())
{
int ff=q.front();q.pop();
vis[ff]=0;
// if(ff==en) break;
for(int i=head[ff];i;i=edge[i].net)
{
int v=edge[i].to,c=edge[i].c,w=edge[i].w;
if(dis[ff]+c<dis[v]&&w)//可松弛且残流不为0
{
let[v]=i;
dis[v]=dis[ff]+c;
flow[v]=min(w,flow[ff]);
if(!vis[v]) q.push(v),vis[v]=1;
}
}
}
return flow[en];
}
int ans1=0,ans2=0;
void EK()
{
while(spfa())
{
ans1+=flow[en];
ans2+=flow[en]*dis[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 ;
}
signed main()
{
n=read(),m=read(),be=read(),en=read();
for(int i=1;i<=m;i++)
{
int u=read(),v=read(),w=read(),c=read();
add(u,v,w,c);add(v,u,0,-c);
}
EK();
cout<<ans1<<" "<<ans2;
return 0;
}
当然dijkstra也存在一种方法来处理负权图,但这超出了我们的讨论范围以及我的认知水平。
五、一点例题
鲁迅曾说过:网络流最难的是建图。我们通过几道例题来看一下究竟该怎么考虑。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术