网络流学习笔记

想看详细的网络流讲解请另请高明。这篇文章太过支离。

最大流

最大流算法分为增广路算法和预流推进两种。

最大流最小割定理

零 定义

都没什么博客愿意从定义开始好好讲,然后其实我学了好久连什么是割都搞错了。这篇博客写的比较完整:薄层’s blog

需要注意:

  1. 割边分正向和反向
  2. 割的容量是所有正向割边的容量之和
  3. 对于任意一个割,当前网络的流量等于正向割边的流量之和减去逆向隔边的流量之和
  4. 对于一个最小割,正向割边的流量一定等于容量,逆向割边的流量一定等于0,否则就会有新的增广路(用最小割最大流定理胡一下)

看图(来自上面说的那篇博客):

在这里插入图片描述

一 证明

继续摘抄:orz zhouzhendong

说是严谨证明实际上一波显然就出来了。

对于一个网络流图G=(V,E),其中有源点s和汇点t,那么下面三个条件是等价的:

  1. 流f是图G的最大流
  2. 残留网络Gf不存在增广路
  3. 对于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

  1. 增广路算法核心思路: 引入反向边
    • 反向边小技巧:正向边存在数组的偶数位,反向边存在奇数位,则取反向边只需要 i^1
  2. 常用增广路算法:
    1. Ford-Fulkerson:找到增广路径就更新
    2. Edmonds-Karp (EK 算法) :BFS 找边数最少的增广路径更新, O ( N M 2 ) O(NM^2) O(NM2)
    3. 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(NEk),然后好像上限也比较松吧,这个不是很确定。

模板(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]的边,转化成无源汇。

有源汇有上下界最大流

在有源汇可行流的基础上从源点到汇点跑一遍最大流。

有源汇有上下界最小流

在有源汇可行流的基础上,去掉汇点到源点连的边再从汇点到源点跑一遍最大流。

例题

网络流的话,在这里放代码就没什么意思了。主要是建模能够建好剩下的就是背板子了。

那么下面是做到的网络流的题,做一道算一道啦,大概会一直更新的。

[网络流24题]餐巾计划

[网络流24题]太空飞行计划

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值