网络流学习笔记

本文详细介绍了网络流的基础概念,包括符号约定、网络流模型以及各种算法,如Ford-Fulkerson、Edmond-Karp、Dinic和ISAP,还涉及最小费用最大流问题。通过对这些算法的描述和分析,展示了它们在网络流问题中的应用和解决策略。
摘要由CSDN通过智能技术生成

目录

前言

Part1 符号及约定

Part2 网络流算法

Part2.1 Ford-Fulkerson 算法

Part2.2 Edmond-Karp 算法

Part2.3 Dinic 算法

Part2.4 ISAP 算法

Part2.5 HLPP 算法

Part2.6 最小(大)费用最大流

Part3 网络流建模和例题

后记

前言

我最近在刷网络流的题,结果啥都不会做……

把一些学习中的心得放到这里,很垃圾,请巨佬们不要吐槽。

本文中添加了一些作者本人自己的见解,如果有误,烦请巨佬们帮忙指出。

本文写作时间:2023.3.30 22.16 ~ ?

本文发布在洛谷博客博客园

Part1 符号及约定

V \textrm{V} V 表示点集, E \textrm{E} E 表示边集。

G = ⟨ V , E ⟩ \textrm{G} = \lang \textrm{V}, \textrm{E} \rang G=V,E 表示图。

s s s 表示源点, t t t 表示汇点。

u → v u \rarr v uv 表示存在一条从 u u u 连向 v v v 的边。

u ↛ v u \not \rarr v uv 表示不存在从 u u u 连向 v v v 的边。

u ⇒ v u \rArr v uv 表示存在一条从 u u u v v v 的路径。

u ⇏ v u \not \rArr v uv 表示不存在一条从 u u u v v v 的路径。

( u , v ) (u, v) (u,v) 表示一条从 u u u 连向 v v v 的边。

( u , v , w ) (u, v, w) (u,v,w) 表示一条从 u u u 连向 v v v,且容量为 w w w 的边。

在最小(大)费用最大流中, ( u , v , w , k ) (u, v, w, k) (u,v,w,k) 表示一条从 u u u 连向 v v v,且容量为 w w w,费用为 k k k 的边。

容量函数 c ( u , v ) \textrm{c}(u, v) c(u,v) 的定义如下:

c ( u , v ) = { 边 ( u , v ) 的容量 u → v 0 u ↛ v \textrm{c}(u, v) = \begin{cases} 边 (u, v) 的容量 & u \rarr v \\ 0 & u \not \rarr v \end{cases} c(u,v)={(u,v)的容量0uvuv

注: 为了方便,假定对于每个顶点 u ∈ V u \in \textrm{V} uV 都有 s ⇒ u ⇒ t s \rArr u \rArr t sut。如果存在点 u u u 使得 s ⇏ u s \not \rArr u su,那么就增加边 ( s , u , 0 ) (s, u, 0) (s,u,0)。如果存在点 u u u 使得 u ⇏ t u \not \rArr t ut,那么就增加边 ( u , t , 0 ) (u, t, 0) (u,t,0)

注: 为了方便,假定图中没有边 ( u , v , w 1 ) (u, v, w_1) (u,v,w1) ( v , u , w 2 ) (v, u, w_2) (v,u,w2) ( w 1 ≥ w 2 ∧ w 1 > 0 ∧ w 2 > 0 ) (w_1 \ge w_2 \land w_1 > 0 \land w_2 > 0) (w1w2w1>0w2>0)同时存在,如果存在,那么删去这两条边,且如果 w 1 > w 2 w_1 > w_2 w1>w2 ,就添加一条边 ( u , v , w 1 − w 2 ) (u, v, w_1 - w_2) (u,v,w1w2)

流函数 f ( u , v ) \textrm{f}(u, v) f(u,v) 的定义如下:

f ( u , v ) = { 边 ( u , v ) 的流量 u → v − f ( v , u ) v → u 0 u ↛ v ∧ v ↛ u \textrm{f}(u, v) = \begin{cases} 边 (u, v) 的流量 & u \rarr v \\ -\textrm{f}(v, u) & v \rarr u \\ 0 & u \not \rarr v \land v \not \rarr u \end{cases} f(u,v)= (u,v)的流量f(v,u)0uvvuuvvu

流函数满足以下性质:

  • 容量限制: ∀ u , v ∈ V , f ( u , v ) ≤ c ( u , v ) \forall u, v \in \textrm{V}, \textrm{f}(u, v) \le \textrm{c}(u, v) u,vV,f(u,v)c(u,v)
  • 斜对称性: ∀ u , v ∈ V , f ( u , v ) = − f ( v , u ) \forall u, v \in \textrm{V}, \textrm{f}(u, v) = -\textrm{f}(v, u) u,vV,f(u,v)=f(v,u)
  • 流守恒性: ∀ u ∈ ∁ V { s , t } , ∑ x ∈ V f ( u , x ) = 0 \forall u \in \complement_{\textrm{V}}{\{s, t\}}, \sum_{x \in \textrm{V}}{\textrm{f}(u, x)} = 0 uV{s,t},xVf(u,x)=0。(简单来说就是除源点和汇点以外,这个点流进的流量等于它流出的流量)

当流函数 f \textrm{f} f 满足以上三点性质时,我们称它为合法的。

流指一个合法的流函数。

一个流的流量定义如下:

∣ f ∣ = ∑ x ∈ V f ( s , x ) |\textrm{f}| = \sum_{x \in \textrm{V}}\textrm{f}(s, x) f=xVf(s,x)

最大流问题:给出一张图 G = ⟨ V , E ⟩ \textrm{G} = \lang \textrm{V}, \textrm{E} \rang G=V,E,求出 ∣ f ∣ |\textrm{f}| f 的最大值。

残存容量函数的定义如下:

c f ( u , v ) = { c ( u , v ) − f ( u , v ) u → v 0 u ↛ v \textrm{c}_\textrm{f}(u, v) = \begin{cases} \textrm{c}(u, v) - \textrm{f}(u, v) & u \rarr v \\ 0 & u \not \rarr v \end{cases} cf(u,v)={c(u,v)f(u,v)0uvuv

在实际处理中,对于每一条边 ( u , v ) (u, v) (u,v),设它的权值为 c f ( u , v ) \textrm{c}_\textrm{f}(u, v) cf(u,v),并增加一条边 ( v , u , f ( u , v ) ) (v, u, \textrm{f}(u, v)) (v,u,f(u,v))。记这样处理过后的图为 G ′ = ⟨ V , E ′ ⟩ \textrm{G}' = \lang \textrm{V}, \textrm{E}' \rang G=V,E,其中 E ′ = { ( u , v , c f ( u , v ) ) ∣ ( u , v ) ∈ E } ∪ { ( v , u , f ( u , v ) ) ∣ ( u , v ) ∈ E } \textrm{E}' = \{(u, v, \textrm{c}_\textrm{f}(u, v)) | (u, v) \in \textrm{E} \} \cup \{(v, u, \textrm{f}(u, v)) | (u, v) \in \textrm{E} \} E={(u,v,cf(u,v))(u,v)E}{(v,u,f(u,v))(u,v)E}。(在 Part1.1 中我会说明为什么要这样处理)

残存网络 G f \textrm{G}_\textrm{f} Gf 是在 G ′ \textrm{G}' G 中只保留权值大于 0 0 0 的边后产生的图,其中每条边的权值为其对应边的权值。即: G f = ⟨ V , E f ⟩ \textrm{G}_\textrm{f} = \lang \textrm{V}, \textrm{E}_\textrm{f} \rang Gf=V,Ef,其中 E f = { ( u , v , w ) ∣ ( u , v , w ) ∈ E ′ ∧ w > 0 } \textrm{E}_\textrm{f} = \{(u, v, w) | (u, v, w) \in \textrm{E}' \land w > 0 \} Ef={(u,v,w)(u,v,w)Ew>0}

增广路径(简称增广路)是在 G ′ \textrm{G}' G 中的一条从 s s s t t t 的简单路径,该路径上每一条边的权值都不为 0 0 0

增广路定理:当在 G ′ \textrm{G}' G 中不存在增广路时,即当 G f \textrm{G}_\textrm{f} Gf s ⇏ t s \not \rArr t st 时,此时的 ∣ f ∣ |\textrm{f}| f 为最大流。

maxflow \textrm{maxflow} maxflow 表示最大流, mincut \textrm{mincut} mincut 表示最小割, mincost \textrm{mincost} mincost 表示最小费用最大流中的最小费用, maxcost \textrm{maxcost} maxcost 表示最大费用最大流中的最大费用。

在建模时,如果 G \textrm{G} G 是一张二分图或者类似于二分图,那么把它的左半部分(不包括 s s s)叫做 X \textrm{X} X,把它的右半部分(不包括 t t t)叫做 Y \textrm{Y} Y

在最小割中,把 s s s 能够到达的节点组成的集合叫做 S \textrm{S} S s s s 无法到达的节点组成的集合叫做 T \textrm{T} T

Part2 网络流算法

Part2.1 Ford-Fulkerson 算法

Ford-Fulkerson 算法(简称 FF)是最原始的网络流算法,时间复杂度上限 O ( m f ) O(mf) O(mf)(其中 m m m 是边数, f f f 是最大流的大小)。

Solution

FF 算法的核心思想是不断寻找增广路并计入答案,直到找不到了为止。

但是直接在原图 G \textrm{G} G 里求最大流会出现问题,如下图。

如果我们先找出一条增广路 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s12t,变成下图:

img

此时就找不到增广路了,但是实际上该图的最大流是 2 2 2 s → 1 → t s \rarr 1 \rarr t s1t s → 2 → t s \rarr 2 \rarr t s2t 两条路径)。

所以我们要在 G ′ \textrm{G}' G 里求最大流,即给每一条边都建反边,反边的权值是 0 0 0

( u , v ) (u, v) (u,v) 流过 w w w 的流量,应当将 ( u , v ) (u, v) (u,v) 的权值减去 w w w,把 ( v , u ) (v, u) (v,u) 的权值加上 w w w

反边的作用可以理解为撤销流量,例如在上面这个例子中,找出增广路 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s12t 后,还有一条增广路 s → 2 → 1 → t s \rarr 2 \rarr 1 \rarr t s21t。处理增广路 s → 2 → 1 → t s \rarr 2 \rarr 1 \rarr t s21t 后, ( 1 , 2 ) (1, 2) (1,2) 这条边的权值是 1 1 1,和原来一样,相当于撤销了 s → 1 → 2 → t s \rarr 1 \rarr 2 \rarr t s12t 中它流过的流量。

FF 使用 dfs 实现,每次从 s s s 出发,寻找一条增广路并将流量计入答案,每访问一个节点就把这个节点打上标记,下次就不能再进入打过标记的节点,保证 dfs 的时间复杂度是 O ( n + m ) O(n + m) O(n+m)。当不存在从 s s s t t t 的增广路的时候,算法退出并输出答案。

时间复杂度证明:

由于 FF 每执行一次 dfs,答案至少会增加 1 1 1,所以最多会执行 f f f 次最大流。

每一次 dfs 由于标记了是否到达过这个点,时间复杂度是 O ( n + m ) O(n + m) O(n+m) 的。

所以总的时间复杂度是 O ( ( n + m ) ⋅ f ) O((n + m) \cdot f) O((n+m)f),也就是 O ( m f ) O(mf) O(mf)

证毕。

要注意网络流算法的时间复杂度是玄学,往往跑不满,所以 FF 实际上耗时比 O ( m f ) O(mf) O(mf) 小很多,但是由于它的复杂度和最大流有关,所以还是很慢。

Tip:

如何快速求出一条边的反边的编号呢?

如果我们在建边之前把 num(即边的总数)设为 1 1 1,且把正边和反边一起建,那么编号为 2 2 2 和编号为 3 3 3 的边就是一对反边,编号为 4 4 4 和编号为 5 5 5 的边也是一对反边……

设这条边的编号为 i,那么这条边的反边的编号就是 i ^ 1

注意: 这样做的前提是一定要在建边之前把 num(即边的总数)设为 1 1 1,不要漏掉!

Code

下面是P3376 【模板】网络最大流的代码,但是由于太慢而 T 了 2 个点……

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1000, maxm = 40000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N];
bool vis[N];
struct edg{
    ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
    ++num;
    E[num].t = y;
    E[num].nxt = H[x];
    E[num].len = w;
    H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
    add_edg(x, y, w);
    add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
    if (x == t) {
        return y;
    }
    vis[x] = 1;
    for (ll i = H[x]; i > 0; i = E[i].nxt) {
        ll v = E[i].t, w = E[i].len;
        if (w > 0 && (!vis[v])) {
            ll k = dfs(v, min(w, y));
            if (k != -1) {
                E[i].len -= k;
                E[(i ^ 1)].len += k;
                return k;
            }
        }
    }
    return -1;
}
void FF() {
    ans = 0;
    for (ll i = 1; i <= S; ++i) {
        vis[i] = 0;
    }
    ll k = dfs(s, INF);
    while (k != -1) {
        ans += k;
        for (ll i = 1; i <= S; ++i) {
            vis[i] = 0;
        }
        k = dfs(s, INF);
    }
}
int main() {
    scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
    num = 1;
    S = n;
    for (ll i = 1; i <= m; ++i) {
        ll u, v, w;
        scanf("%lld%lld%lld", &u, &v, &w);
        add_edges(u, v, w);
    }
    FF();
    printf("%lld", ans);
    return 0;
}

Part2.2 Edmond-Karp 算法

Edmond-Karp 算法(简称 EK)是 FF 的 bfs 版,时间复杂度上限 O ( n m 2 ) O(nm^2) O(nm2),但是往往比 FF 跑得更快。

(未完待续)

Part2.3 Dinic 算法

Dinic 算法应该可以算最常用的网络流算法,既好写,时间复杂度上限又比 FF 和 EK 低很多,是 O ( n 2 m ) O(n^2 m) O(n2m),跑得比较快。

(未完待续)

Code

下面是P3376 【模板】网络最大流的代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 1000, maxm = 40000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N], C[N], de[N];
struct edg{
    ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
    ++num;
    E[num].t = y;
    E[num].nxt = H[x];
    E[num].len = w;
    H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
    add_edg(x, y, w);
    add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
    if (x == t) {
        return y;
    }
    ll z = y, res = 0;
    for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
        C[x] = i;
        ll v = E[i].t, w = E[i].len;
        if (w > 0 && de[v] != -1 && de[v] == de[x] + 1) {
            ll k = dfs(v, min(w, z));
            E[i].len -= k;
            E[(i ^ 1)].len += k;
            z -= k;
            res += k;
        }
    }
    return res;
}
bool bfs() {
    for (ll i = 1; i <= S; ++i) {
        de[i] = -1;
        C[i] = H[i];
    }
    de[s] = 1;
    queue<ll> q;
    q.push(s);
    while (!q.empty()) {
        ll x = q.front();
        q.pop();
        for (ll i = H[x]; i > 0; i = E[i].nxt) {
            ll v = E[i].t, w = E[i].len;
            if (w > 0 && de[v] == -1) {
                de[v] = de[x] + 1;
                q.push(v);
            }
        }
    }
    return (de[t] != -1);
}
void Dinic() {
    ans = 0;
    while (bfs()) {
        ll k = dfs(s, INF);
        ans += k;
    }
}
int main() {
    scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
    num = 1;
    S = n;
    for (ll i = 1; i <= m; ++i) {
        ll u, v, w;
        scanf("%lld%lld%lld", &u, &v, &w);
        add_edges(u, v, w);
    }
    Dinic();
    printf("%lld", ans);
    return 0;
}

Part2.4 ISAP 算法

ISAP 算法的时间复杂度上限和 Dinic 一样,都是 O ( n 2 m ) O(n^2 m) O(n2m) 的,但是往往比 Dinic 跑得更快。

(未完待续)

注: 我的模板因为使用每次通过寻找出边中深度的最小值并更新该点的深度的写法,有点问题,被我 hack 了 2 次,下面是两个 hack 样例:

第 1 个:

input:

7 8 1 7
1 2 1
1 3 1
2 4 1
3 4 1
4 7 1
4 5 1
5 7 1
4 6 2147483647

output:

2

wrong output:

1

第 2 个:

input:

8 9 6 7
6 1 2
1 3 2
3 8 2
8 7 2
1 2 2147483647
2 7 1
6 4 1
4 5 1
5 2 1

output:

3

wrong output:

2

注意:因此,最好写直接把深度加 1 1 1 的写法,既正确性高,又常数小!

Code

下面是P3376 【模板】网络最大流的代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 400, maxm = 20000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans, H[N], C[N], de[N], gp[N];
bool fl;
struct edg{
    ll t, nxt, len;
}E[M * 2];
void add_edg(ll x, ll y, ll w) {
    ++num;
    E[num].t = y;
    E[num].nxt = H[x];
    E[num].len = w;
    H[x] = num;
}
void add_edges(ll x, ll y, ll w) {
    add_edg(x, y, w);
    add_edg(y, x, 0);
}
ll dfs(ll x, ll y) {
    if (x == t) {
        return y;
    }
    ll z = y, res = 0;
    for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
        C[x] = i;
        ll v = E[i].t, w = E[i].len;
        if (w > 0 && de[v] != -1 && de[v] + 1 == de[x]) {
            ll k = dfs(v, min(w, z));
            E[i].len -= k;
            E[(i ^ 1)].len += k;
            z -= k;
            res += k;
            if (z == 0) {
                return res;
            }
        }
    }
    --gp[de[x]];
    if (gp[de[x]] == 0) {
        fl = 0;
    }
    ++de[x];
    ++gp[de[x]];
    return res;
}
void bfs() {
    for (ll i = 1; i <= S; ++i) {
        de[i] = -1;
    }
    de[t] = 1;
    for (ll i = 1; i <= S + 1; ++i) {
        gp[i] = 0;
    }
    queue<ll> q;
    q.push(t);
    while (!q.empty()) {
        ll x = q.front();
        q.pop();
        ++gp[de[x]];
        for (ll i = H[x]; i > 0; i = E[i].nxt) {
            ll v = E[i].t;
            if (i % 2 == 1 && de[v] == -1) {
                de[v] = de[x] + 1;
                q.push(v);
            }
        }
    }
}
void ISAP() {
    bfs();
    ans = 0;
    fl = 1;
    while (de[s] <= S && fl) {
        for (ll i = 1; i <= S; ++i) {
            C[i] = H[i];
        }
        fl = 1;
        ll k = dfs(s, INF);
        ans += k;
    }
}
int main() {
    scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
    num = 1;
    S = n;
    for (ll i = 1; i <= m; ++i) {
        ll u, v, w;
        scanf("%lld%lld%lld", &u, &v, &w);
        add_edges(u, v, w);
    }
    ISAP();
    printf("%lld", ans);
    return 0;
}

Part2.5 HLPP 算法

最快的网络流算法,时间复杂度 O ( n 2 m ) O(n^2 \sqrt{m}) O(n2m ),我还不会……

(未完待续)

Part2.6 最小(大)费用最大流

最小(大)费用最大流是网络最大流的拓展,每一条边除了容量之外,还增加了一个费用,整张网络的总费用为每一条边的流量乘费用的和。在保证流量最大的前提下,要求出最小(大)的费用。

最大费用最大流跑最长路即可。

(未完待续)

Code

下面是P3381 【模板】最小费用最大流的代码。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll maxn = 20000, maxm = 400000;
const ll N = maxn * 2 + 10, M = maxm + 10;
const ll INF = 0x3f3f3f3f3f3f3fll;
ll n, m, num, s, t, S, ans1, ans2, H[N], C[N], dis[N];
bool b[N], vis[N];
struct edg{
    ll t, nxt, len, k;
}E[M * 2];
void add_edg(ll x, ll y, ll w, ll k) {
    ++num;
    E[num].t = y;
    E[num].nxt = H[x];
    E[num].len = w;
    E[num].k = k;
    H[x] = num;
}
void add_edges(ll x, ll y, ll w, ll k) {
    add_edg(x, y, w, k);
    add_edg(y, x, 0, -k);
}
ll dfs(ll x, ll y) {
    if (x == t) {
        return y;
    }
    vis[x] = 1;
    ll z = y, res = 0;
    for (ll i = C[x]; i > 0 && z > 0; i = E[i].nxt) {
        C[x] = i;
        ll v = E[i].t, w = E[i].len, k = E[i].k;
        if (w > 0 && (!vis[v]) && dis[v] == dis[x] + k) {
            ll k = dfs(v, min(w, z));
            E[i].len -= k;
            E[(i ^ 1)].len += k;
            z -= k;
            res += k;
        }
    }
    return res;
}
bool bfs() {
    for (ll i = 1; i <= S; ++i) {
        C[i] = H[i];
        dis[i] = INF;
        b[i] = 0;
        vis[i] = 0;
    }
    dis[s] = 0;
    bool z = 0;
    queue<ll> q;
    q.push(s);
    b[s] = 1;
    while (!q.empty()) {
        ll x = q.front();
        q.pop();
        if (x == t) {
            z = 1;
        }
        b[x] = 1;
        for (ll i = H[x]; i > 0; i = E[i].nxt) {
            ll v = E[i].t, w = E[i].len, k = E[i].k;
            if (w > 0 && dis[x] + k < dis[v]) {
                dis[v] = dis[x] + k;
                if (!b[v]) {
                    b[v] = 1;
                    q.push(v);
                }
            }
        }
        b[x] = 0;
    }
    return z;
}
void Dinic() {
    ans1 = 0;
    ans2 = 0;
    while (bfs()) {
        ll k = dfs(s, INF);
        ans1 += k;
        ans2 += (k * dis[t]);
    }
}
int main() {
    scanf("%lld%lld%lld%lld", &n, &m, &s, &t);
    num = 1;
    S = n;
    for (ll i = 1; i <= m; ++i) {
        ll u, v, w, k;
        scanf("%lld%lld%lld%lld", &u, &v, &w, &k);
        add_edges(u, v, w, k);
    }
    Dinic();
    printf("%lld %lld", ans1, ans2);
    return 0;
}

Part3 网络流建模和例题

后记

本文更新记录

1.0 版:2023.3.30

最初版本。

1.1 版:2023.3.31

发现 ISAP 的模板有 2 个错误,进行了改正,并放上了 2 个 hack 样例。

1.2 版:2023.4.5

写了 Part1 和 Part2.1。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值