初见 | 图论 | 网络流初步
前言
上次写二分图的时候说过要学网络流里比较初步的东西,顺便把上次那个利用网络流建模求二分图最大匹配的坑补上。
因为学的总时长不长,可能会有一定的理解误区,希望各位发现后及时指正。
缺省源
还是惯例,因为我用的缺省源确实挺长的,所以都放在前面。
#include <iostream>
#include <stdio.h>
#include <math.h>
#include <algorithm>
#include <string.h>
#include <queue>
#define Heriko return
#define Deltana 0
#define Romanno 1
#define S signed
#define LL long long
#define R register
#define I inline
#define CI const int
#define mst(a, b) memset(a, b, sizeof(a))
#define ON std::ios::sync_with_stdio(false)
using namespace std;
template<typename J>
I void fr(J &x)
{
short f=1;
char c=getchar();
x=0;
while(c<'0' or c>'9')
{
if(c=='-') f=-1;
c=getchar();
}
while (c>='0' and c<='9')
{
x=(x<<3)+(x<<1)+c-'0';
c=getchar();
}
x*=f;
}
template<typename J>
I void fw(J x,bool k)
{
if(x<0) putchar('-'),x=-x;
static short stak[35];
short top=0;
do
{
stak[top++]=x%10;
x/=10;
}
while(x);
while(top) putchar(stak[--top]+'0');
if(k) putchar('\n');
else putchar(' ');
}
那么下面就开始罢!
网络流
基本概念
网络
既然是网络流,那么我们首先需要知道什么是网络。其实网络的定义很简单,即:
- 网络 \(G=(V,E)\) 是一张有向图。
比如下面这一张有向图就是一个网络:
容量
对于网络中的每一条有向边,都有一个给定的权值:容量,即:
- \(\forall\ (x,y)\in E\),有权值 \(c(x,y)\),称为其容量。
下面这张图上的那些权值就是对应边的容量啦~
同时我们定义:
- \(\forall\ (x,y)\notin E\),有 \(c(x,y)=0\)。
源点和汇点
我们还是先用刚才那张图:
这里的 S 和 T 就为源点和汇点,一张网络的源点和汇点是可以随意指定的。
流 & 流量 & 剩余容量
设 \(f(x,y)\) 为定义在结点二元组 \((x\in V,y\in V)\) 上的实数函数,且满足:
-
\(f(x,y)\le c(x,y)\)
-
\(f(x,y)=-f(y,x)\)
-
\(\forall x\ne S , x\ne T ,\sum_{(u,x)\in E}f(u,x)=\sum_{(x,v)\in E}f(x,v)\)
\(f\) 则称为网络的流函数。对于 \((x,y)\in E\),\(f(x,y)\) 称为边的流量,\(c(x,y)-f(x,y)\) 称为边的剩余容量。[1]
对于这张图来说,我们把每条边的流量和容量以“\(f\ /\ c\)”的格式表示出来就是:
我们可以假设一个情景来帮助理解:假设这个网络是给你家供水的网络,
即,供水厂为 1 号结点,你家为 4 号结点:
虽然你计划把连通你家的两个水管的容量都开的很大,但是你发现因为供水厂出水的两个管子都太小了,能流到你家的水完全跑不满你的水管:
这个时候你觉得既然如此,安装两个水管那么费钱还不如去掉一个,于是你就看哪个剩余容量大就干掉哪个,于是你肯定保留下来的是下面红色这条:
最大流
这里用一种通俗易懂的解释:
- 网路的最大流,即为使 \(\sum_{(S,v)\in E}f(S,v)\) 最大的流函数。
解决最大流问题的做法有多种,因为我没学那么多,于是就只说说 \(\tt{FF,EK,Dinic}\) 罢。
FF 算法
即 \(\tt{Ford-Fullkerson}\) 算法,这是网络流的一个基础算法,因为这个算法的想法相当直接。
其核心思想为寻找图中的增广路,实际就是网络中从源点到汇点的仍有剩余流量的路径,也就在残量网络(剩余容量大于 0 的边和所有结点构成的网络)上进行操作。[2]
主体的思路就是寻找增广路 & 利用反向边反悔。
这里用一下 \(\tt{Dfkuaid}\) 的例子,我比较懒不再去写了(
大体的过程就是:用 DFS 找到增广路之后,利用前面流函数的第二条性质建立一条反向边更新残量网络,直到找不到增广路。
因为 FF 的做法还是挺暴力的,平常也没太有人用,就不写了。
EK 算法
即 \(\tt{Edmond-Karp}\) 算法,本身这个算法的思想是和 FF 一样的,只不过这里是用 BFS 去实现,这里之所以 EK 的时间复杂度上会更优,是因为 BFS 找到的增广路是最短的,而 DFS 找到的增广路不能保证是最短的,这样我们就省去了递归去找最短增广路的时间。
Code
这里用洛谷的模板 P3376 为例。
template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}
CI inf=(1<<29),MXX=5005,NXX=205;
struct node
{
LL to,nex,val;
}
r[MXX<<1];
LL head[NXX],cnt(1),incf[NXX],pre[NXX],n,m,maxflow,s,t;
bool vis[NXX];
I void Ass_we_can(LL x,LL y,LL z)
{
r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}
I bool BFS()
{
mst(vis,0);
queue<LL> qwq;
qwq.push(s);vis[s]=1;
incf[s]=inf;
while(qwq.size())
{
LL x=qwq.front();qwq.pop();
for(R LL i=head[x];i;i=r[i].nex)
{
if(r[i].val)
{
LL y=r[i].to;
if(vis[y]) continue;
incf[y]=Hmin(incf[x],r[i].val);
pre[y]=i;
qwq.push(y);vis[y]=1;
if(y==t) Heriko Romanno;
}
}
}
Heriko Deltana;
}
I void Update()
{
LL x=t;
while(x!=s)
{
LL i=pre[x];
r[i].val-=incf[t];
r[i^1].val+=incf[t];
x=r[i^1].to;
}
maxflow+=incf[t];
}
LL z,y,x;
S main()
{
fr(n),fr(m),fr(s),fr(t);
for(R LL i=1;i<=m;++i)
{
fr(z),fr(y),fr(x);
Ass_we_can(z,y,x);
}
while(BFS()) Update();
fw(maxflow,1);
Heriko Deltana;
}
EK 的时间复杂度是 \(O(nm^2)\) 的,在处理非稠密图的时候还是可以的。
Dinic 算法
前面说到 EK 在处理非稠密图的时候是可以的,但是如果遇到稠密图就完全炸了啊QwQ
于是我们就需要一个在稠密图上更优的算法:Dinic 算法。
其实某种意义上来说,Dinic 是 EK 的进一步优化,所以 Dinic 是 FF 的优化的优化(bushi
考虑到 EK 算法每轮都可能会为了找到 1 条增广路遍历整个残量网络,而这一点还是可以继续优化的。
于是我们就需要用到一个东西来优化我们 BFS 的次序:分层图。
定义从 S 到 x 最少要经历的边数为 dis[x]
,而在残量网络中,满足 dis[y]=dis[x]+1
的边 \((x,y)\) 构成的子图显然就是一个分层图,而分层图显然是一张有向无环图。
Dinic 主要是以下两步,最终目标是不能让 S 到达 T:
-
在残量网路上 BFS 求出结点的
dis
,构造分层图。 -
在分层图上 DFS 求增广路,在回溯的时候更新剩余容量。
下面是代码(当然内含玄学的当前弧优化)
Code
template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}
CI inf=(1<<29),MXX=5005,NXX=205;
struct node
{
LL to,nex,val;
}
r[MXX<<1];
LL head[NXX],cnt(1),incf[NXX],pre[NXX],n,m,maxflow,s,t;
bool vis[NXX];
I void Ass_we_can(LL x,LL y,LL z)
{
r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}
I bool BFS()
{
mst(vis,0);
queue<LL> qwq;
qwq.push(s);vis[s]=1;
incf[s]=inf;
while(qwq.size())
{
LL x=qwq.front();qwq.pop();
for(R LL i=head[x];i;i=r[i].nex)
{
if(r[i].val)
{
LL y=r[i].to;
if(vis[y]) continue;
incf[y]=Hmin(incf[x],r[i].val);
pre[y]=i;
qwq.push(y);vis[y]=1;
if(y==t) Heriko Romanno;
}
}
}
Heriko Deltana;
}
I void Update()
{
LL x=t;
while(x!=s)
{
LL i=pre[x];
r[i].val-=incf[t];
r[i^1].val+=incf[t];
x=r[i^1].to;
}
maxflow+=incf[t];
}
LL z,y,x;
S main()
{
fr(n),fr(m),fr(s),fr(t);
for(R LL i=1;i<=m;++i)
{
fr(z),fr(y),fr(x);
Ass_we_can(z,y,x);
}
while(BFS()) Update();
fw(maxflow,1);
Heriko Deltana;
}
ISAP 算法
于是求最大流的部分到这里大约就结束了罢(
网络最大流流建模解决二分图匹配
欸,上次我挖了个坑,于是这次就来填坑啦~
主要的思想就是新建两个虚拟点,一个是源点,一个是汇点,这源点连接全部左侧端点,而全部右侧端点连向汇点。
于是我们就有了复杂度爆切匈牙利的如下 Dinic 代码:
template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}
template<typename J>
I void Clear(queue<J> &x) {while(x.size()) x.pop();}
CI inf=998244353,MXX=1e6+5,NXX=2005;
struct node
{
LL to,nex,val;
}
r[MXX<<1];
LL head[NXX],cnt(1),dis[NXX],now[NXX],n,m,maxflow,s,t;
queue<LL> qwq;
I void Ass_we_can(LL x,LL y,LL z)
{
r[++cnt].to=y;r[cnt].nex=head[x];r[cnt].val=z;head[x]=cnt;
r[++cnt].to=x;r[cnt].nex=head[y];r[cnt].val=0;head[y]=cnt;
}
I bool BFS()
{
mst(dis,0);
Clear(qwq);
qwq.push(s);dis[s]=1;now[s]=head[s];
while(qwq.size())
{
LL x=qwq.front();qwq.pop();
for(R LL i=head[x];i;i=r[i].nex)
{
if(r[i].val and !dis[r[i].to])
{
LL y=r[i].to;
qwq.push(y);
now[y]=head[y];
dis[y]=dis[x]+1;
if(y==t) Heriko Romanno;
}
}
}
Heriko Deltana;
}
LL Dinic(LL x,LL flow)
{
if(x==t) Heriko flow;
LL rst=flow,k,i,y;
for(i=now[x];i and rst;i=r[i].nex)
{
y=r[i].to;
if(r[i].val and dis[y]==dis[x]+1)
{
k=Dinic(y,Hmin(rst,r[i].val));
if(!k) dis[y]=0;
r[i].val-=k;
r[i^1].val+=k;
rst-=k;
}
}
now[x]=i;
Heriko flow-rst;
}
LL x,y,flow,e;
S main()
{
fr(n),fr(m),fr(e);
t=n+m+1;
for(R int i=1;i<=n;++i) Ass_we_can(0,i,1);
for(R int i=n+1;i<=n+m;++i) Ass_we_can(i,t,1);
for(R int i=1;i<=e;++i)
{
fr(x),fr(y);
if(x>n or y>m) continue;
Ass_we_can(x,y+n,1);
}
while(BFS())
while((flow=Dinic(s,inf))) maxflow+=flow;
fw(maxflow,1);
Heriko Deltana;
}
最小费用最大流
费用流(最小费用最大流)与普通最大流的区别就是它在每条边上加了一个单位流量的费用,需要在满足最大流的同时满足费用最小。[3]
因为我从网上找到的大部分说自己写的 Dinic 的博客里发现的是 SSP(即 EK),发现的唯一真 Dinic 又不是很好写,于是这里就只有 EK 罢。
SSP 算法
SSP(Successive Shortest Path)算法:在最大流的 EK 算法求解最大流的基础上,把用 BFS 求解任意增广路改为用 SPFA 求解单位费用之和最小的增广路即可。[4]
简单来说就是:SSP = EK - BFS + SPFA
.
众所周知,关于 SPFA ,他死了。但是据说 NOI2019 没卡
但是在这里一般来说应该没有人会去卡 SPFA 罢,虽然是有用 Dijkstra 的,但是还要对边权有一系列操作,我就没学。
于是上代码罢,是洛谷板子 P3381 的 Code。
Code
template<typename J>
I J Hmin(J x,J y) {Heriko x<y?x:y;}
CI inf=0X3f3f3f3f,MXX=50005,NXX=5005;
int n,m,maxflow,dis[NXX],head[NXX],cnt(1),incf[NXX],pre[NXX],s,t,mincost;
bool vis[NXX];
struct node
{
int nex,to,dis,val;
}
r[MXX<<1];
I void Ass_we_can(int x,int y,int z,int w)
{
r[++cnt].to=y,r[cnt].nex=head[x],r[cnt].val=z,r[cnt].dis=w;head[x]=cnt;
}
I bool SPFA()
{
queue<int> q;
mst(dis,inf);mst(vis,0);
q.push(s);dis[s]=0;vis[s]=1;
incf[s]=(1<<30);
while(q.size())
{
int x=q.front();q.pop();
vis[x]=0;
for(R int i=head[x];i;i=r[i].nex)
{
if(!r[i].val) continue;
int y=r[i].to;
if(dis[y]>dis[x]+r[i].dis)
{
dis[y]=dis[x]+r[i].dis;
incf[y]=Hmin(incf[x],r[i].val);
pre[y]=i;
if(!vis[y]) q.push(y),vis[y]=1;
}
}
}
if(dis[t]==inf) Heriko Deltana;
Heriko Romanno;
}
I void MCMF()
{
while(SPFA())
{
int x=t;
maxflow+=incf[t];
mincost+=dis[t]*incf[t];
int i;
while(x!=s)
{
i=pre[x];
r[i].val-=incf[t];
r[i^1].val+=incf[t];
x=r[i^1].to;
}
}
}
int x,y,z,w;
S main()
{
fr(n),fr(m),fr(s),fr(t);
for(R int i=1;i<=m;++i)
{
fr(x),fr(y),fr(z),fr(w);
Ass_we_can(x,y,z,w);
Ass_we_can(y,x,0,-w);
}
MCMF();
fw(maxflow,0),fw(mincost,1);
Heriko Deltana;
}
End
于是就不加什么例题了罢,因为我懒得写了((
参考资料
-
[1] 《算法进阶指南》0x6A 网络流初步 —— 李煜东
-
[2] [图论入门] 网络最大流 - 增广路算法 —— Dfkuaid
-
[3] [整理]网络流随记——中(费用流) —— ajthreac
-
[4] 网路流 —— OI-Wiki
在写本文的时候 OI-Wiki 被墙了,放的是镜像网站的链接