浅谈网络流
本文章同步发表在洛谷博客。
网络模型
想象一些有向水管构成的图,每条水管都有固定的流量上限,有源点可以出水,有汇点可以收水。
形式化描述这个东西。设 \(f_{u \to v}\) 表示 \(u\) 向 \(v\) 流的流量,\(c_{u \to v}\) 表示 \(u \to v\) 这条路上的容量(流量)限制。\(S\) 为源点(起点),\(T\) 为汇点(终点)。
对于边 \(u \to v\) 有 \(w\) 的流量,则 \(f_{u \to v} = w\),并且 \(f_{v \to u} = -w\) 而非什么 \(f_{v \to u} = 0\)。也许你已经看出来了,这个东西和牛顿第三定律有些类似,负的流量表示反向流动。
问题来了,如何判断一个流是否合法?换句话说,合法流的充要条件是什么?
首先有反对称性,也就是所谓的 \(f_{u \to v} = -f_{v \to u}\)。
接着,流量满足限制,即 \(f_{u \to v} \le c_{u \to v}\)。
以及要满足内部流量守恒。即,对于 \(u \not= S,T\),有 \(\sum_{x} f_{u \to x} = 0\)。
推论:外部流量守恒 \(\sum_{x} f_{S \to x} = \sum_{y} f_{y \to T}\),即源点流出量等于汇点流入量。
要求找出一个合法的流所对应的 \(f\),并最大化 \(\sum_{x} f_{S \to x}\)(源点流出的流量),等价于最大化 \(\sum_{x} f_{x \to T}\)(汇点流入的流量)。
这,称为最大流问题。
这个模型和现实情况还是略有区别的,它允许凭空出现的“环流”。不过在最大流问题中,“环流”不仅不能贡献流量,还浪费了容量,所以并不会对最优解造成任何影响。
残量网络上的调整
记 \(r_{u \to v} = c_{u \to v} - f_{u \to v}\),即“剩余流量”。称 \(r\) 为残量网络。
残量调整定理:
对于网络 \(c\),记可能的所有合法流的集合为 \(F_c\)。对于任意一个 \(f_0 \in F_c\),记 \(r = c - f_0\)(即残量网络),则 \(F_c = f_0 + F_r\)。也就是说,源网络的所有合法流可以在残量网络上调整而获得。
可以直观理解,残量网络上的任意流相当于“不超出容量限制的调整”,相当于在原图上增加流量,也可以把已经流过去的流撤回来。这自然可以得到所有合法的流。
增广路与增广
根据残量调整定理,可以得到一种最大流算法的思路:对于当前网络 \(c\),求一个流方案 \(f\) 满足 \(\text{flow}(f) > 0\),然后将 \(c\) 减去 \(f\) 并将最大流答案加上 \(\text{flow}(f)\)。重复这一过程,最大流在不断变大,由于最大流必然是有限的,这个算法必然能结束。
即不断在残量网络上找出一种正的流(调整方法),直到找不到为止。
最终 \(c\) 上不能存在任何正的流,即有容量的边不能联通 \(S,T\)。
我们不妨先考虑最简单的流 \(f\)——一条路径。
于是这里就要引入两个定义:增广路与增广。
- 增广路:找出残量网络 \(r\) 中从 \(S\) 到 \(T\) 的一条路径,其中路径上的 \(r\) 都 \(>0\),称这条路径为一条增广路。
- 增广:算出增广路中 \(r\) 的最小值 \(x\),让增广路上的每条边都多流 \(x\) 的流量,这一过程叫做增广。
我们只要不断找出增广路,然后对其进行增广,直到找不出为止,即可得到最大流。
最大流的算法实现
Ford-Fulkerson 算法
简称 FF 算法。
它的做法是,直接用 DFS 找增广路,找到一条就增广。
时间复杂度上界 \(O(m \times \max x)\),\(\max x\) 表示最大流量。
Edmonds-Karp 算法
简称 EK 算法。
它在 FF 算法的基础上进行了一些改进,只考虑容量为正的边,并改用 BFS 找增广路。
这里有一个最短路单调定理:在 EK 算法不断增广的过程中,源点到各个点的最短路单调不降。根据这个最短路单调定理,可证明 EK 算法的时间复杂度为 \(O(n \times m^2)\)。
Dinic 算法
在 EK 算法的基础上改进,考虑用一次“多路增广”完成原先的“增广路长度保持为 \(L\) 的一系列连续增广”。
首先跑一次 BFS 求出 \(dis_{1 \sim n}\)。有一个性质,那就是只需要考虑在最短路上的边。即对于边 \(u \to v\),当且仅当 \(dis_v = dis_u + 1\) 时才需要考虑这条边。
用 DFS 进行多路增广。具体地,到达 \(u\) 点,流入流量为 \(f\),枚举各个出边搜索看看能流出多少。如果能流,将 \(f\) 减去流走的流量,并调整残量网络,尝试下一条边。最后返回总的流出去的流量。
这里有个优化方法叫做当前弧优化。具体地,其为点 \(u\) 记录了 \(p_u\),表示第 \(1 \sim p_u\) 条出边都已经流不出流量,下次访问该点,枚举从 \(p_u + 1\) 开始。这样,每条边都只有一次尝试失败。
总时间复杂度为 \(O(n^2 \times m)\),但是如果不加当前弧优化的话,复杂度会退化为 \(O(n^2 \times m^2)\)。
例题选讲
P3163 危桥
首先根据题目中桥的连边进行建图,正常的桥建成的边的流量为 \(+ \infty\),而危桥的流量限制为 \(2\),因为题目要求只能经过最多两次。
接着建立源点 \(S\) 和汇点 \(T\),然后让它们分别对应地去连边。具体地,源点向 \(a_1,b_1\) 分别连流量为 \(a_n,b_n\) 的边,\(a_2,b_2\) 分别向 \(T\) 连 \(a_n,b_n\) 的边。
直接对这个图跑最大流,然后判断最终的流量是否为 \(2 \times (a_n + b_n)\) 就行了……吗?
不!因为你可能往返,危桥不一定只经过了 \(2\) 次。然后这里有一个很厉害的解决办法,我们对这个图反着再跑一次最大流!看它的流量是不是也是 \(2 \times (a_n + b_n)\)。如果它也是,那么往返的问题就不存在了;如果它不是,就说明确实存在往返,危桥发生状况。
反着跑最大流到底是什么东西呢。简单来说,就是把 \(b_1\) 和 \(b_2\) 颠倒了过去,再连边跑最大流。
这里需要跑两次最大流,因此边的信息要维护好,别漏了或重了。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 100;
const int INF = 0x3f3f3f3f;
struct line{int to,wei,Db;};
int n,sa,ta,an,sb,tb,bn,St,Ed,dis[N],Ans;
vector<line> g[N],e[N];
queue<int> q;bool flag;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Add_Edge(int x,int y,int z){
int idx=e[x].size(),idy=e[y].size();
e[x].pb({y,z,idy}),e[y].pb({x,0,idx});return;
}
void Add_Gdge(int x,int y,int z){
int idx=g[x].size(),idy=g[y].size();
g[x].pb({y,z,idy}),g[y].pb({x,0,idx});return;
}
bool BFS_Canit(){
for(int i=1;i<=n+2;i++)dis[i]=0;
while(!q.empty())q.pop();
dis[St]=1,q.push(St);
while(!q.empty()){
int u=q.front();q.pop();
for(auto [v,w,id]:g[u])if(w&&!dis[v])
{dis[v]=dis[u]+1;if(v==Ed)return 1;q.push(v);}
}return 0;
}
int DFS_Cntflow(int u,int flow){
if(u==Ed)return flow;int now=flow;
for(auto &[v,w,id]:g[u])
if(w&&dis[v]==dis[u]+1){
int tmp=DFS_Cntflow(v,min(now,w));
if(!tmp)dis[v]=0;else w-=tmp,g[v][id].wei+=tmp,now-=tmp;
}
return flow-now;
}
int Sol_Dinic(){
int res=0,flow=0;
while(BFS_Canit())
while(flow=DFS_Cntflow(St,INF))res+=flow;
return res;
}
int main(){
while(scanf("%d %d %d %d %d %d %d",&n,&sa,&ta,&an,&sb,&tb,&bn)!=EOF){
sa++,ta++,sb++,tb++;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
char h;cin>>h;
if(h=='O')Add_Edge(i,j,2);
if(h=='N')Add_Edge(i,j,INF);
}
for(int i=1;i<=n;i++)g[i]=e[i];
St=n+1,Ed=n+2,flag=1;
Add_Gdge(St,sa,2*an);Add_Gdge(St,sb,2*bn);
Add_Gdge(ta,Ed,2*an);Add_Gdge(tb,Ed,2*bn);
Ans=Sol_Dinic();if(Ans!=2*(an+bn))flag=0;
for(int i=1;i<=n;i++)g[i]=e[i];
Add_Gdge(St,sa,2*an);Add_Gdge(St,tb,2*bn);
Add_Gdge(ta,Ed,2*an);Add_Gdge(sb,Ed,2*bn);
Ans=Sol_Dinic();if(Ans!=2*(an+bn))flag=0;
if(flag)cout<<"Yes\n";else cout<<"No\n";
for(int i=1;i<=n+2;i++)
g[i].clear(),e[i].clear();
}
return 0;
}
P1231 教辅的组成
考虑对其建网络流的一张图。但是这张图该怎么建呢?
首先考虑让匹配的书与练习册,或者书与答案,进行一个连边。容量限制当然就是 \(1\)。
此时再建立源点 \(S\) 和汇点 \(T\),让 \(S\) 连练习册,\(T\) 连答案,接着从 \(S\) 出发向 \(T\) 跑最大流。
这样就结束了吗?不!这样跑,你会发现一个很严重的事情,那就是在这里,一本书你可能对应上了多本练习册或者答案。这样的事情是不允许的,虽然题目说这个人大致知道对应关系,但这也仅仅是可能的对应关系而已。一本书不可能对应两本练习册或者两本答案,但是题目中可能给定的一本书连向了多本练习册或者答案。
那这个东西该怎么改进呢?有一个很绝的法子——拆点。怎么说?就是把书给拆成两个点。本来我们的书是只有一个点的,现在我们给拆成两个,一个连练习册们,一个连答案们,然后再把它俩连起来。容量?限制为 \(1\)!这就是核心——你限制了容量是 \(1\),那么不管你左右连了多少分叉,你都只能抓出一个匹配上,不然这里就过不去,那这个流方案就不合法了。
于是这样你就可以解决该问题了,拆点的时候有些小细节要注意一下。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 5e4+5;
const int INF = 0x3f3f3f3f;
struct line{int to,wei,Db;};
int n1,n2,n3,mm,St,Ed,dis[N];
vector<line> g[N];queue<int> q;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
int ID(int p,int x){
if(p==1)return x;
if(p==2)return n2+x;
if(p==3)return n1+n2+x;
if(p==4)return n1+n1+n2+x;
}
void Add_edge(int x,int y,int z){
int idx=g[x].size(),idy=g[y].size();
g[x].pb({y,z,idy}),g[y].pb({x,0,idx});return;
}
bool BFS_Canit(){
for(int i=1;i<=Ed;i++)dis[i]=0;
while(!q.empty())q.pop();
dis[St]=1,q.push(St);
while(!q.empty()){
int u=q.front();q.pop();
for(auto [v,w,id]:g[u])if(w&&!dis[v])
{dis[v]=dis[u]+1;if(v==Ed)return 1;q.push(v);}
}return 0;
}
int DFS_Cntflow(int u,int flow){
if(u==Ed)return flow;int now=flow;
for(auto &[v,w,id]:g[u]){
if(now<=0)break;
if(w&&dis[v]==dis[u]+1){
int tmp=DFS_Cntflow(v,min(now,w));
if(!tmp)dis[v]=0;else w-=tmp,g[v][id].wei+=tmp,now-=tmp;
}
}return flow-now;
}
int Sol_Dinic(){
int res=0,flow=0;
while(BFS_Canit())
while(flow=DFS_Cntflow(St,INF))res+=flow;
return res;
}
int main(){
n1=read(),n2=read(),n3=read();
mm=read();
while(mm--){
int x=read(),y=read();
Add_edge(ID(1,y),ID(2,x),1);
}mm=read();
while(mm--){
int x=read(),y=read();
Add_edge(ID(3,x),ID(4,y),1);
}for(int i=1;i<=n1;i++)
Add_edge(ID(2,i),ID(3,i),1);
St=n1+n1+n2+n3+1,Ed=n1+n1+n2+n3+2;
for(int i=1;i<=n2;i++)
Add_edge(St,ID(1,i),1);
for(int i=1;i<=n3;i++)
Add_edge(ID(4,i),Ed,1);
cout<<Sol_Dinic()<<"\n";
return 0;
}

浙公网安备 33010602011771号