Dinic 是一种网络最大流的算法。
<h1>引入</h1>
网络流的本质是线性规划。
定义:有向图 $(V,E)$ 存在源点 $S \in V$ 和汇点 $T \in V,S \neq T$,每条边都有一个容量 $c$。
对于任意一个时刻,设 $flow(u,v)$ 表示实际流量,则整个图 $G$ 的流网络满足如下性质:
- 容量限制:$\forall u,v $,$flow(u,v) < c(u,v)$
- 反对称性: $\forall u,v $,存在 $ flow(u,v) = -flow(v,u) $
- 流量平衡, $\forall u,u \neq S,u \neq T$,应满足 $\sum flow(u,v) = 0$,即每个点不会制造或消耗流量。
可行流:在容量网络 G 中满足以下条件的网络流 flow ,称为可行流. - 弧流量限制条件: $0 \leq flow(u,v) \leq c(u,v)$
- 平衡条件: 即流入一个点的流量要等于流出这个点的流量(源点和汇点除外).
零流:如果网络流上每条弧的流量都为 $0$ ,则该网络流成为零流。
伪流:如果一个网络流只满足弧流量限制条件,不满足平衡条件,则这种网络流为伪流,或称为容量可行流.(预流推进算法有用)
在容量网络中,满足弧流量限制条件,且满足平衡条件并且具有最大流量的可行流,称为网络最大流,简称最大流.
残余流量:可以感性理解为 $cl(u,v) = c(u,v) - flow(u,v)$
残余网络:在一个网络流图上,找到一条源到汇的路径后,对路径上所有的边,其容量都减去此次找到的量,对路径上所有的边,都添加一条反向边,其容量也等于此次找到的流量,这样得到的新图,就称为原图的残余网络。
上面讲了一些基本的定义,看完下面的内容已经足够了。
<h1>Dinic</h1>
<h2>增广路找最大流</h2>
首先注意一个性质:如果一个可行流不是最大流,那么当前网络中一定存在一条增广路。(作者也不会证明)
增广路大家在学习二分图的时候一定都听说过了。
大概找增广路的过程就是 dfs 一遍,判断这条边残量是否大于 $0$,如果大于 $0$ 就通过这条边进行增广。
详细说一下找增广路的过程:
- 找到一条从源点到汇点的路径,使得路径上每一条边的残留都 $>0$,这条路径叫做增广路;
- 找到这条路径上最小的残量,记为 $f$;
- 将这条路径上每一条有向边的残量减去 $f$,同时对于反向边的残量加上 $f$;
- 重复直到找不到增广路。
<h2>为什么要建反向边</h2>
因为你在寻找增广路的时候,在前面找到的不一定是一个最优解,如果我们建一个流量相反的边,那么就代表可以从这条反向边流过而实现反悔。
<h2>反向边的实现</h2>
因为你是一个指针选手,对于每个边都建一个指向其反向边的指针就可以了。
<h2>上述朴素算法太慢了</h2>
可以被一些奇妙的极限数据卡掉,大概就是有很大和很小的边权,如果增广策略不对,就会陷入增广许多次的情景,时间复杂度飞天。
例如下面这组数据:
手动模拟一下就会发现它要循环许多遍!这是我们不可以接受的。
于是 Dinic 算法解决了这个问题。
<h2>Dinic 算法</h2>
Dinic 算法引入一个分层图的概念(不是分层图最短路),对于每一个节点记录一个 $level$ 表示到达源点的距离(边权看作 $1$ )。我们限制每次由 $u$ 增广出来的 $v$ 必须满足 $level_u + 1 = level_v$。这样就能防止上述的奇怪情况卡掉朴素算法。
Dinic 算法的时间复杂度理论上界是 $O(n^2m)$,但是绝大多数情况都跑不满。
<h2>当前弧优化</h2>
即每一次dfs增广时不从第一条边开始,而是记录点u之前循环到了哪一条边,以此来加速。
这样能快不少,更不容易跑到上界了
<h2>具体实现</h2>
<h3>结构体定义</h3>
我们定义点结构体(Node)和边结构体(Edge)
struct Node{
struct Edge firstEdge,curEdge; // curEdge 当前弧优化用
int level; // bfs 出来的层数
}node[MAXN];
struct Edge{
Node s,t;
Edge next,reverse; // reverse 记录这条边的反向边
int cap,flow; // cap 流量,flow 目前增广出来的经过这条边花费的最大流量
}pool[MAXM<<2],*frog = pool;
剩下的操作在代码里会给出注释。
#include <algorithm>
#include <iostream>
#include <cstring>
#include <climits>
#include <cstdio>
#include <vector>
#include <cmath>
#include <queue>
#include <stack>
#include <map>
#include <set>
#define Re register
#define LL long long
#define U unsigned
#define FOR(i,a,b) for(Re int i = a;i <= b;++i)
#define ROF(i,a,b) for(Re int i = a;i >= b;--i)
#define CLR(i,a) memset(i,a,sizeof(i))
#define BR printf("--------------------n")
#define DEBUG(x) std::cerr << #x << '=' << x << std::endl
const int MAXN = 10000+5;
const int MAXM = 100000+5;
struct Node{
struct Edge firstEdge,curEdge;
int level;
}node[MAXN];
struct Edge{
Node s,t;
Edge next,reverse;
int cap,flow;
}pool[MAXM<<2],*frog = pool;
Edge New(Node s,Node *t,int cap){
Edge *ret = ++frog;
*ret = (Edge){s,t,s->firstEdge,NULL,cap,0};
return ret;
}
inline void add(int u,int v,int cap){
node[u].firstEdge = New(&node[u],&node[v],cap);
node[v].firstEdge = New(&node[v],&node[u],0);
node[u].firstEdge->reverse = node[v].firstEdge;
node[v].firstEdge->reverse = node[u].firstEdge;
// 建立边和反向边并且互相关联
}
int N,M,S,T;
inline bool bfs(Node s,Node t){
FOR(i,1,N){
node[i].level = 0;
node[i].curEdge = node[i].firstEdge; // 初始化 cur
}
std::queue<Node *> q;
q.push(s);s->level = 1;
while(!q.empty()){
Node *v = q.front();q.pop();
for(Edge *e = v->firstEdge;e;e = e->next){
if(e->flow < e->cap && !e->t->level){
e->t->level = v->level + 1;
if(e->t == t) return true;
q.push(e->t);
}
}
}
return false;
}
int dfs(Node s,Node t,int limit=INT_MAX){ // 寻找增广路
if(s == t) return limit;
for(Edge *&e = s->curEdge;e;e = e->next){ // 当前弧优化
if(e->flow < e->cap && e->t->level == s->level + 1){
int flow = dfs(e->t,t,std::min(limit,e->cap - e->flow));
if(flow > 0){ // 找到一条
e->flow += flow;
e->reverse->flow -= flow;
return flow;
}
}
}
return 0;
}
int dinic(){
int ans = 0;
while(bfs(&node[S],&node[T])){ // 一直到不能分层为止
int flow = dfs(&node[S],&node[T]);
while(flow > 0){ // 一直到不能增广位置
ans += flow;
flow = dfs(&node[S],&node[T]);
}
}
return ans;
}
int main(){
scanf("%d%d%d%d",&N,&M,&S,&T);
FOR(i,1,M){
int u,v,c;scanf("%d%d%d",&u,&v,&c);
add(u,v,c);
}
printf("%dn",dinic());
return 0;
}
One comment
太强了,wyh