网络最大流问题就是给你一个有向图,告诉你一个源点与一个汇点,并给每一条边一个最大流量,需要你求出从源点最多能够发出多少单位流量到汇点(哎呀我也说不清,就是给你一些或大或小的管道(每个管道都有最大秒流量),一些中转站,一座供水塔以inf单位每秒的速度供水,问你家每秒最多得到多少单位水(中转站、供水塔、你家由管道连通))
很显然看起来我们可以从源点跑dfs,只要到下一节点的边的边权>0就跑到下一节点,一路跑过去,直到汇点,然后答案加上这一路上最小的管道流量,这一条路径上所有管道减去这个流量,然后就没了。
但是这样子是有问题的。(蓝模板怎么可能这么简单)
举个栗子:(将dfs路径用红色表示)
然后你会发现这样的答案是2,然而正确的答案是4(s-2-t,s-3-t)
(不要说你的存边是2-t比2-3优先,调个位置就卡掉了)
为什么会这样?
因为上图中第一条dfs路径把原本应是第二条路径的一条边给“占用”了。
计算机可不是人,它无法判断到底该怎么跑dfs,所以这时候就需要我们人为地给计算机一个“反悔”的机会。
重点:怎么让计算机“反悔”?
给每条边建边权为0的反向边,当每次跑到汇点时,在回溯给dfs路径上的边权减去最小边权的时候还要给反向边加上最小边权。
先看效果:
然后答案就神奇地变成4了!
为什么?
用整体的思想来看:如果我正着经过一条边,再反着经过,是不是相当于没经过?好吧,我承认这不是很好理解
这一种“抵消”的思想应用十分广泛,比如洛谷P1792 [国家集训队]种树,就是一种利用类似“抵消”的方法来实现的可反悔的贪心。
没理解也没关系,反正就是要反向建边,题做多了就理解了QAQ
使用了这种“抵消”思想的dfs,大概就是所谓的EK算法了。(即不停的寻找增广路然后操作)
然后再介绍一种优化算法:Dinic算法(本质上其实差不多,只不过是一次寻找多条增广路并更新图)
PS:增广路即能够使答案增加的dfs路径
大概步骤就是:
1.bfs——给之后寻找增广路的dfs提供一个bfs序作为扩展增广路的依据,同时判断是否还存在增广路(如果遍历不到汇点,就退出输出答案)。
2.dfs——寻找增广路(注意这里一次寻找了多条)并更新答案。
3.重复1
dinic的代码实现:(贴上我洛谷P3376 【模板】网络最大流的代码)
#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;
int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans;
void insert(int x,int y,int z) {
nt[++e]=bg[x];
to[e]=y;
w[e]=z;
bg[x]=e;
}
void bfs() {
int i,f,l,u,v;
f=l=1;
q[1]=s;
dep[s]=1;
while (f<=l) {
u=q[f++];
for (i=bg[u];i;i=nt[i]) {
v=to[i];
if (dep[v] || !w[i]) continue;
q[++l]=v;
dep[v]=dep[u]+1;
}
}
}
int dfs(int x,int s) {
int i,u,tmp,res=0;
if (x==t) return s;
if (!s) return 0;
for (i=bg[x];i;i=nt[i]) {
u=to[i];
if (dep[u]==dep[x]+1) {
tmp=dfs(u,min(s,w[i]));
if (!tmp) continue;
res+=tmp;
s-=tmp;
w[i]-=tmp;
w[i^1]+=tmp;
if (!s) break;
}
}
return res;
}
int main() {
int i,j,x,y,z;
scanf("%d%d%d%d",&n,&m,&s,&t);
for (i=1;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);
insert(y,x,0);
}
while (1) {
memset(dep,0,sizeof(dep));
bfs();
if (!dep[t]) break;
ans+=dfs(s,inf);
}
printf("%d\n",ans);
return 0;
}
例题链接:
洛谷P3355 骑士共存问题 暂时我没写题解QAQ
附:当前弧优化
听名字似乎是一个很高级的东西,但其实很简单,就几句话。。。
我们维护一个now数组,在每次dfs前把链式前向星的bg数组(或者是head)拷贝一份到now上,然后在dfs枚举边找下一个节点时循环枚举边的编号不再从bg[x]开始了,而从now[x]开始。而now[x]不断地更新,即i循环到哪,now[x]都更新为i。(具体实现还是看代码吧,我的语言表达能力一向不强)
这样子可以大大提升程序效率。
代码:
#include<stdio.h>
#include<string.h>
#include<iostream>
#define maxn 20010
#define maxm 200010
#define inf 0x3f3f3f3f
using namespace std;
int bg[maxn],nt[maxm],to[maxm],w[maxm],e=1;
int dep[maxn],q[maxn],n,m,s,t,ans,now[maxn];
void insert(int x,int y,int z) {
nt[++e]=bg[x];
to[e]=y;
w[e]=z;
bg[x]=e;
}
void bfs() {
int i,f,l,u,v;
for (i=1;i<=n;i++) now[i]=bg[i];
f=l=1;
q[1]=s;
dep[s]=1;
while (f<=l) {
u=q[f++];
for (i=bg[u];i;i=nt[i]) {
v=to[i];
if (dep[v] || !w[i]) continue;
q[++l]=v;
dep[v]=dep[u]+1;
}
}
}
int dfs(int x,int s) {
int i,u,tmp,res=0;
if (x==t) return s;
if (!s) return 0;
for (i=now[x];i;i=nt[i]) {
u=to[i];
if (dep[u]==dep[x]+1) {
tmp=dfs(u,min(s,w[i]));
if (!tmp) continue;
res+=tmp;
s-=tmp;
w[i]-=tmp;
w[i^1]+=tmp;
if (!s) break;
}
now[x]=i;
}
return res;
}
int main() {
int i,j,x,y,z;
scanf("%d%d%d%d",&n,&m,&s,&t);
for (i=1;i<=m;i++) {
scanf("%d%d%d",&x,&y,&z);
insert(x,y,z);
insert(y,x,0);
}
while (1) {
memset(dep,0,sizeof(dep));
bfs();
if (!dep[t]) break;
ans+=dfs(s,inf);
}
printf("%d\n",ans);
return 0;
}
实测效率截图O_O!:
优化之前:
之后:
至于这个优化的正确性,蒟蒻引用一句很6的话(毕竟我描述不出来)——
“对于一次BFS而言,它确定的层次图中每条边若已经被走完了,那么它就不可能再带来增广,下一次就直接从这条最后没走完的边走就可以了。”