文章目录
想看详细的网络流讲解请另请高明。这篇文章太过支离。
最大流
最大流算法分为增广路算法和预流推进两种。
最大流最小割定理
零 定义
都没什么博客愿意从定义开始好好讲,然后其实我学了好久连什么是割都搞错了。这篇博客写的比较完整:薄层’s blog
需要注意:
- 割边分正向和反向
- 割的容量是所有正向割边的容量之和
- 对于任意一个割,当前网络的流量等于正向割边的流量之和减去逆向隔边的流量之和
- 对于一个最小割,正向割边的流量一定等于容量,逆向割边的流量一定等于0,否则就会有新的增广路(用最小割最大流定理胡一下)
看图(来自上面说的那篇博客):
一 证明
继续摘抄:orz zhouzhendong
说是严谨证明实际上一波显然就出来了。
对于一个网络流图G=(V,E),其中有源点s和汇点t,那么下面三个条件是等价的:
- 流f是图G的最大流
- 残留网络Gf不存在增广路
- 对于G的某一个割(S,T),此时f = C(S,T)
首先证明1 => 2:
我们利用反证法,假设流f是图G的最大流,但是残留网络中还存在有增广路p,其流量为fp。则我们有流f’=f+fp>f。这与f是最大流产生矛盾。
接着证明2 => 3:
假设残留网络Gf不存在增广路,所以在残留网络Gf中不存在路径从s到达t。我们定义S集合为:当前残留网络中s能够到达的点。同时定义T=V-S。
此时(S,T)构成一个割(S,T)。且对于任意的u∈S,v∈T,有f(u,v)=c(u,v)。若f(u,v)<c(u,v),则有Gf(u,v)>0,s可以到达v,与v属于T矛盾。
因此有f(S,T)=Σf(u,v)=Σc(u,v)=C(S,T)。
最后证明3 => 1:
由于f的上界为最小割,当f到达割的容量时,显然就已经到达最大值,因此f为最大流。
这样就说明了为什么找不到增广路时,所求得的一定是最大流。
二 求一个最小割
根据定义,跑完最大流之后,对于一个最小割,正向割边的流量一定等于容量,逆向割边的流量一定等于0。简化一下,在残量网络上出现一个断层,左边是源点出发沿着还有残量的边能够到达的点,右边是到不了的,那么这个断层的所有正向割边就组成了最小割。
Dinic
- 增广路算法核心思路: 引入反向边
- 反向边小技巧:正向边存在数组的偶数位,反向边存在奇数位,则取反向边只需要
i^1
- 反向边小技巧:正向边存在数组的偶数位,反向边存在奇数位,则取反向边只需要
- 常用增广路算法:
- Ford-Fulkerson:找到增广路径就更新
- Edmonds-Karp (EK 算法) :BFS 找边数最少的增广路径更新, O ( N M 2 ) O(NM^2) O(NM2)
- Dinic:在 EK 的基础上,分层 (BFS) + 多路增广(DFS),复杂度
O
(
N
2
M
)
O(N^2M)
O(N2M)
- 当前弧优化:已经增广过的边不再增广,引用写法
for (int &i = cur[u]; i; i = g.nxt[i])
- 当前弧优化:已经增广过的边不再增广,引用写法
#include<bits/stdc++.h>
using namespace std;
const int inf = ;
const int N = ;
const int M = ;
struct G{
int h[N], e, nxt[M], v[M], f[M];
void clear(){
e = 1;
memset(h, 0, sizeof(h));
}
void add_dir(int _u, int _v, int _f){
nxt[++e] = h[_u];
v[e] = _v; f[e] = _f;
h[_u] = e;
}
void add(int _u, int _v, int _f){
add_dir(_u, _v, _f);
add_dir(_v, _u, 0);
}
}g;
int n, m, s, t;
int dep[N], cur[N];
void get_input()
{
// 读入+建图
}
int que[N], ql, qr;
bool init()
{
for (int i = 1; i <= n; ++ i) // change
cur[i] = g.h[i];
memset(dep, 0, sizeof(dep));
dep[s] = 1;
que[ql = qr = 1] = s;
while (ql <= qr){
int u = que[ql++];
for (int i = g.h[u]; i; i= g.nxt[i]){
int v = g.v[i], f = g.f[i];
if (dep[v] || !f) continue;
dep[v] = dep[u]+1;
if (v == t) return true;
que[++qr] = v;
}
}
return false;
}
int aug(int u, int maxf)
{
if (u == t) return maxf;
int nowf = 0;
for (int &i = cur[u]; i; i = g.nxt[i]){
int v = g.v[i], f = g.f[i];
if (dep[v] != dep[u]+1 || f == 0) continue;
int tmp = aug(v, min(maxf-nowf, f));
nowf += tmp;
g.f[i] -= tmp;
g.f[i^1] += tmp;
if (nowf == maxf) return nowf;
}
return nowf;
}
int dinic()
{
int ret = 0;
while (init()) ret += aug(s, inf);
return ret;
}
int main()
{
get_input();
printf("%d\n", dinic());
return 0;
}
ISAP
ISAP是增广路算法里面最快的。复杂度大概 O ( n 2 m ) O(n^2m) O(n2m),但是上界非常松,导致一般的不是恶意卡ISAP的题一般都过得去。具体复杂度证明这篇博客有:传送门
但是在某些情况下还是Dinic跑得快,如图:
模板(upd. 2019.7.10)
#include<bits/stdc++.h>
using namespace std;
const int inf = ;
const int N = ;
const int M = ;
struct G{
int h[N], e, nxt[M], v[M], f[M];
void clear(){
e = 1;
memset(h, 0, sizeof(h));
}
void add_dir(int _u, int _v, int _f){
v[++e] = _v; f[e] = _f;
nxt[e] = h[_u]; h[_u] = e;
}
void add(int _u, int _v, int _f){
add_dir(_u, _v, _f);
add_dir(_v, _u, 0);
}
}g;
int n, s, t; // 这里n是网络流图的点数
int dep[N], gap[N], cur[N];
void get_input()
{
// 读入+建图
}
int que[N], ql, qr;
void init()
{
for (int i = s; i <= t; ++ i)
cur[i] = g.h[i];
// 这里注意一下,需要给所有点的当前弧赋初始值,当s和t不是最大或者最小的时候要记得改
memset(gap, 0, sizeof(gap));
memset(dep, 0, sizeof(dep));
++ gap[dep[t] = 1];
que[ql = qr = 1] = t;
while (ql <= qr){
int u = que[ql++];
for (int i = g.h[u]; i; i= g.nxt[i]){
int v = g.v[i];
if (dep[v]) continue;
++gap[dep[v] = dep[u]+1];
que[++qr] = v;
}
}
}
int aug(int u, int maxf)
{
if (u == t) return maxf;
int nowf = 0;
for (int &i = cur[u]; i; i = g.nxt[i]){
int v = g.v[i], f = g.f[i];
if (dep[v] != dep[u]-1 || f == 0) continue;
int tmp = aug(v, min(maxf-nowf, f));
nowf += tmp;
g.f[i] -= tmp;
g.f[i^1] += tmp;
if (nowf == maxf) return nowf;
}
if (--gap[dep[u]] == 0) dep[s] = t+2;
++gap[++dep[u]];
cur[u] = g.h[u];
return nowf;
}
int isap()
{
init();
int ret = 0;
while (dep[s] <= t+1) ret += aug(s, inf);
// 这里稍微注意一下,深度应该小于等于包含源汇的所有点,所以是t+1
return ret;
}
int main()
{
get_input();
printf("%d\n", isap());
return 0;
}
HLPP
复杂度大概 O ( n 2 m ) O(n^2 \sqrt{m}) O(n2m),但是上限比较紧。具体复杂度证明不会。
模板
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf = 1e18+7;
const int int_inf = 1e9+7;
const int N = 1200+7;
const int M = 120000+7;
int n, m, s, t;
struct G{
int h[N], e, nxt[M<<1], p[M<<1];
ll f[M<<1];
void clear(){
e = 0;
memset(h, -1, sizeof(h));
memset(nxt, -1, sizeof(nxt));
}
void add_edge(int u, int v, ll c){
nxt[e] = h[u];
p[e] = v;
f[e] = c;
h[u] = e;
++ e;
}
void add(int u, int v, ll c){
add_edge(u, v, c);
add_edge(v, u, 0);
}
}g;
int dpt[N], gap[N];
bool inq[N];
ll rf[N];
struct NODE{
int id, dpt;
NODE(){}
NODE(int x, int y){id = x; dpt = y;}
bool operator < (const NODE &u)const{
return dpt < u.dpt;
}
};
priority_queue<NODE> q;
void Init()
{
scanf("%d%d%d%d", &n, &m, &s, &t);
g.clear();
for (int i = 1; i <= m; ++ i){
int x, y;
ll z;
scanf("%d%d%lld", &x, &y, &z);
g.add(x, y, z);
}
}
void Bfs()
{
queue<int> qq;
memset(dpt, 0, sizeof(dpt));
memset(gap, 0, sizeof(gap));
qq.push(t); dpt[t] = 1; gap[1] = 1;
while (!qq.empty()){
int u = qq.front();
qq.pop();
// cout << u << " : " << dpt[u] << endl;
++ gap[dpt[u]];
for (int i = g.h[u]; i != -1; i = g.nxt[i]){
int v = g.p[i];
int f = g.f[i^1];
if (!dpt[v] && f){
dpt[v] = dpt[u]+1;
qq.push(v);
}
}
}
if (dpt[s] == 0){
puts("0");
exit(0);
}
}
void Push(int u)
{
for (int i = g.h[u]; i != -1 && rf[u]; i = g.nxt[i]){
int v = g.p[i];
ll f = min(g.f[i], rf[u]);
if (dpt[v] == dpt[u]-1 && f){
rf[u] -= f;
rf[v] += f;
g.f[i] -= f;
g.f[i^1] += f;
if (!inq[v] && v != s && v != t){
inq[v] = 1;
q.push(NODE(v, dpt[v]));
}
}
}
}
int reLabel(int u)
{
int pre = dpt[u], now = int_inf;
for (int i = g.h[u]; i != -1; i = g.nxt[i]){
int v = g.p[i];
int f = g.f[i];
if (f) now = min(now, dpt[v]);
}
-- gap[pre];
if (now == int_inf)
dpt[u] = n+1;
else{
dpt[u] = now+1;
++ gap[now+1];
q.push(NODE(u, dpt[u]));
}
return gap[pre] ? 0 : pre;
}
void Gap(int d)
{
if (d == 0) return;
for (int i = 1; i <= n; ++ i)
if (dpt[i] > d)
dpt[i] = n+1;
}
ll Hlpp()
{
Bfs();
memset(rf, 0, sizeof(rf));
memset(inq, 0, sizeof(inq));
q.push(NODE(s, dpt[s])); rf[s] = inf; inq[s] = 1;
while (!q.empty()){
int u = q.top().id;
q.pop(); inq[u] = 0;
if (dpt[u] == n+1) continue;
Push(u);
if (!rf[u]) continue;
Gap(reLabel(u));
}
return rf[t];
}
int main()
{
Init();
printf("%lld\n", Hlpp());
return 0;
}
费用流
EK+SPFA。复杂度 O ( N ∗ E ∗ k ) O(N*E*k) O(N∗E∗k),然后好像上限也比较松吧,这个不是很确定。
模板(upd. 2019.7.10)
#include<bits/stdc++.h>
using namespace std;
const int inf = ;
const int N = ;
const int M = ;
struct G{
int h[N], e, nxt[M], v[M], f[M], w[M];
void clear(){
e = 1;
memset(h, 0, sizeof(h));
}
void add_dir(int _u, int _v, int _f, int _w){
v[++e] = _v; f[e] = _f; w[e] = _w;
nxt[e] = h[_u]; h[_u] = e;
}
void add(int _u, int _v, int _f, int _w){
add_dir(_u, _v, _f, _w);
add_dir(_v, _u, 0, -_w);
}
}g;
int n, s, t; // n是网络图的点数,谨防混淆
void get_input()
{
// 读入+建图
}
queue<int> que;
int dis[N], pre[N], flo[N];
bool inq[N];
bool spfa()
{
memset(dis, 0x7f, sizeof(dis));
memset(flo, 0x7f, sizeof(flo));
que.push(s); dis[s] = 0; inq[s] = 1;
while (!que.empty()){
int u = que.front();
que.pop(); inq[u] = 0;
for (int i = g.h[u]; i; i = g.nxt[i]){
int v = g.v[i], f = g.f[i], w = g.w[i];
if (f && dis[v] > dis[u]+w){
dis[v] = dis[u]+w;
pre[v] = i;
flo[v] = min(f, flo[u]);
if (!inq[v]){inq[v] = 1; que.push(v);}
}
}
}
return dis[t] < inf;
}
int aug()
{
int u = t;
while (u != s){
int i = pre[u];
g.f[i] -= flo[t];
g.f[i^1] += flo[t];
u = g.v[i^1];
}
return dis[t]*flo[t];
}
int min_cost_max_flow()
{
int ret = 0;
while (spfa())
ret += aug();
return ret;
}
int main()
{
get_input();
printf("%d\n", min_cost_max_flow());
return 0;
}
平面图网络流
p.s. 对偶图的方法只适用于无向图,但是在需要手算简单的网络流图的场景中,用对偶图来算会比手推增广路方便很多,即使结果不一定正确也可以比较方便地检查。
- 把源汇连边,找 对偶图 (面作为点,面之间相邻就连边,正向保留边权,反向设为 0,走一条有向边表示把边左边的点放进 s 的集合中,右边的点放进 t 的集合中)
- 求最短路(本质上来说对偶图中的环相当于一个割。为了保证源汇在不同的集合中,强制选取了源汇之间连的虚拟边)
上下界网络流
讲的很明白的博客:liu runda’s blog
在最大流板子上魔改即可。
无源汇有上下界可行流
先假设已经把所有下界流量流掉,然后建虚拟源点和汇点流补偿流来使原图满足流量守恒。
有源汇有上下界可行流
与上面的唯一不同在于源点和汇点不需要满足流量守恒。
汇点向源点连一条[0, inf]的边,转化成无源汇。
有源汇有上下界最大流
在有源汇可行流的基础上从源点到汇点跑一遍最大流。
有源汇有上下界最小流
在有源汇可行流的基础上,去掉汇点到源点连的边再从汇点到源点跑一遍最大流。
例题
网络流的话,在这里放代码就没什么意思了。主要是建模能够建好剩下的就是背板子了。
那么下面是做到的网络流的题,做一道算一道啦,大概会一直更新的。