参考文献:
- https://www.cnblogs.com/DuskOB/p/11216861.html
- https://blog.csdn.net/yjr3426619/article/details/82808303
- https://blog.csdn.net/lym940928/article/details/90209172
- https://baike.baidu.com/item/%E7%BD%91%E7%BB%9C%E6%B5%81/2987528?fr=aladdin
- https://www.cnblogs.com/pk28/p/8039645.html
- https://blog.csdn.net/disgustinglemon/article/details/51296636
- 什么是网络流
- 最大流(最小割) Start \color{#33cc00}\texttt{Start} Start
- D i n i c Dinic Dinic (常用) End \color{red}\texttt{End} End
- E K EK EK
- S a p Sap Sap
- F o r d − F u l k e r s o n Ford-Fulkerson Ford−Fulkerson(不讲)
- H L P P HLPP HLPP (快)
- 最大流解题
- 费用流
- E K EK EK 费用流
- D i n i c Dinic Dinic 费用流
- z k w zkw zkw 费用流
费用流解题
有上下界的网络流
- 无源汇上下界可行流
- 有源汇上下界可行流
- 有源汇上下界最大流
- 有源汇上下界最小流
- 最大权闭合子图
- 有上下界的网络流解题
即源点到汇点的最大总流量。如果把网络流图比作水管道系统,那么就是求单位时间内源点到汇点能流多少水。概念较抽象,那么拿前面那张网络流图举例:
s ⇒ f l o w = 3 1 s\xRightarrow{flow=3} 1 sflow=31
s ⇒ f l o w = 1 3 s\xRightarrow{flow=1} 3 sflow=13
1 ⇒ f l o w = 1 2 1\xRightarrow{flow=1} 2 1flow=12
3 ⇒ f l o w = 2 4 3\xRightarrow{flow=2} 4 3flow=24
1 ⇒ f l o w = 2 4 1\xRightarrow{flow=2} 4 1flow=24
2 ⇒ f l o w = 2 t 2\xRightarrow{flow=2} t 2flow=2t
4 ⇒ f l o w = 2 t 4\xRightarrow{flow=2} t 4flow=2t
结论:最大流为 3 3 3。
首先 s s s 处可以流出无限流量,但因为 s s s 到 1 1 1 的边流量只有 3 3 3,所以 s s s 先向 1 1 1 流 3 3 3 的流量,现在 1 1 1 处有 3 3 3 的流量。因为是单位时间内的流量,所以 s → 1 s\to1 s→1 的边就不能再用了。
同理, 1 1 1 处的 3 3 3 份流量分到 2 2 2 和 4 4 4 处。 1 → 2 1\to2 1→2 和 1 → 4 1\to4 1→4 的边不能再用。 2 2 2 处有 1 1 1 流量, 4 4 4 处有 2 2 2 流量。然后再通过边 2 → t 2\to t 2→t 和 4 → t 4\to t 4→t 向汇点 t t t 流 3 3 3 的流量。
然后再用 s → 3 s\to3 s→3,于是 3 3 3 处有了 1 1 1 的流量。然后再走 3 → 4 3\to 4 3→4,因为 3 3 3 处只有 1 1 1 的流量,所以虽然 3 → 4 3\to4 3→4 能流 2 2 2 的流量, 4 4 4 也只能得到 1 1 1 的流量。 然后再看 4 → t 4\to t 4→t ,因为 4 → t 4\to t 4→t 的边已经用完,所以即使 4 4 4 处有 1 1 1 的流量, t t t 也得不到一点流量。
所以最终最大流为 3 3 3。
不过现在先不讲最大流算法,过会儿你就知道为什么了。
定义
给定一个网络流图 G = ( V , E ) G=(V,E) G=(V,E),源点为 s s s,汇点为 t t t。若一个边集 E ′ ⊆ E E'⊆E E′⊆E 被删去后, s s s 和 t t t 不再联通,则称该边集为网络的割。边流量之和的最小的割称为网络的最小割。
再回到刚刚那个网络流图:
结论:最小割为 3 3 3。
先割断 1 → 2 1\to2 1→2,再割断 4 → t 4\to t 4→t, s s s 和 t t t 不再联通。并且保证没有别的割法比这样更优。所以该网络流图的最小割为 3 3 3 。
蒟蒻的粗略证明:最大流必然有一些流满的边,如果把它们删掉,就是最小割了。如果删掉后还未成功割图,说明有些支流未流满,就不是最大流了。
通过上文我们已知一个网络流图的最小割等于它的最大流,所以求最大流的算法同样也是求最小割的算法。求最大流的算法很多,其中 D i n i c Dinic Dinic 最为普遍使用。
D i n i c Dinic Dinic 算法会对网络流图做一个分层。其实也就是做一次 B f s Bfs Bfs,把源点设为第 0 0 0 层,然后遍历源点的相邻节点,设为第 1 1 1 层,再遍历这些节点的相邻节点……直到汇点。
实现:
1.将源点的层次设为 0 0 0,把源点设为已经访问,别的点设为未访问。
2.按照 B f s Bfs Bfs 序遍历网络流图,为节点分层,遍历到终点结束。
3.在层次网络中,沿着相邻层 D f s Dfs Dfs 搜索所有的增广路,并做相应的流量调整。
4.重复 1 ∼ 3 1\sim 3 1∼3。
算法较抽象,拿图举例(节点上的数为层次):
第一次建立层次网络,找到了蓝线表示的三条增广路,做流量调整。
第二次建立层次网络,上次流光的边这次不发挥作用,找到一条增广路。
第三次建立层次网络,汇点已经不能通过有用的边联通,算法终止。
如果你懂了,蒟蒻就放代码了。 D i n i c Dinic Dinic 最大流最小割算法:
#include
using namespace std;
const int inf=0x3f3f3f3f;
template<int V,int M>
class Dinic{
public:int E,g[V],to[M],nex[M],fw[M];
void clear(){memset(g,0,sizeof g),E=1;}
//E=1保证了互为反边的两条边可以通过^1互相得到
Dinic(){clear();}
//初始化
void add(int x,int y,int f){nex[++E]=g[x],to[E]=y,fw[E]=f,g[x]=E;}
//标准加边
void Add(int x,int y,int f){add(x,y,f),add(y,x,0);}
//加正边和反边,使得增广可以反悔
int dep[V],cur[V];bool vis[V];queue<int> q;
//dep表示层次,cur为单前弧优化,下面会讲。
//vis表示是否访问,queue维护bfs
bool bfs(int s,int t,int p){
for(int i=1;i<=p;i++) vis[i]=0,cur[i]=g[i];
q.push(s),vis[s]=1,dep[s]=0; //从源点开始bfs
while(q.size()){
int x=q.front();q.pop();
for(int i=g[x];i;i=nex[i])if(!vis[to[i]]&&fw[i])
q.push(to[i]),vis[to[i]]=1,dep[to[i]]=dep[x]+1;
//bfs过程中顺便给每个节点标上层次。
}
return vis[t]; //表示联通
}
int dfs(int x,int t,int F){
if(x==t||!F) return F;
int f,flow=0;
for(int&i=cur[x];i;i=nex[i]) //即i=g[x]
if(dep[to[i]]==dep[x]+1&&(f=dfs(to[i],t,min(F,fw[i])))>0) //沿着层次增广
{fw[i]-=f,fw[i^1]+=f,F-=f,flow+=f;if(!F) break;}
//边的流量调整
return flow; //一次增广的流量。
}
int dinic(int s,int t,int p){ //多次增广函数
int res=0,f;
while(bfs(s,t,p)) while((f=dfs(s,t,inf))) res+=f;
return res;
}
};
int n,m,s,t,p;
Dinic<10010,200010> net;
int main(){
scanf("%d%d%d%d",&n,&m,&s,&t),p=n;
for(int i=1,x,y,f;i<=m;i++){
scanf("%d%d%d",&x,&y,&f);
net.Add(x,y,f);
}
printf("%d\n",net.dinic(s,t,p));
return 0;
}
以上代码读者可能有两个地方不懂:
1.反向反悔边
因为一条边的流量是通过以下代码调整的:
fw[i]-=f,fw[i^1]+=f,F-=f,flow+=f;if(!F) break;
所以可以把反悔看成走反边,以得到最大流。
2.单前弧优化
for(int i=1;i<=p;i++) vis[i]=0,cur[i]=g[i];
for(int&i=cur[x];i;i=nex[i])
因为 D i n i c Dinic Dinic 的增广是沿着加边顺序(严谨地说,是沿着加边反顺序)增广的,所以每一次增广时,前面几条边可能已经增广完了,这时,如果记录第一条没增广完的边,下一次增广从这条边开始,就方便、快很多。
下一篇会讲最大流的剩下 3 3 3 种算法。
祝大家学习愉快!