【筆記/模板】網路流初步

ThySecret發表於2024-11-04

網路流簡介

基本定義

網路(Network)在圖論中指一個有向圖 \(G=(V,E)\),圖上的每一條邊都有一個值,叫做容量(Capacity),也有兩個特殊點:源點(Source)和匯點(Sink)。

而對於一張網路 \(G\)(Flow)指的是一個函式 \(f\)\(f(u, v)\) 表示邊 \(u \to v\) 經過的流量,一個點 \(u\) 的淨流量可以表示為 \(F(u) = \textstyle{\sum_{x \in V}} f(u, x) - \textstyle{\sum_{x \in V}} f(x, u)\)

每一張網路的流從源點 \(st\) 出發,流向匯點 \(ed\),對於流經的每一條邊,都滿足以下性質:

  1. 對於一條邊 \(u \to v\) 的容量 \(c(u, v)\),始終有:\(0 \le f(u, v) \le c(u, v)\)

  2. 對於除源匯點以外的任意一點,淨流量均為 \(0\)

由上述性質可知,源匯點的淨流量均不為 \(0\),並且 \(\left | F(st) \right | = \left | F(ed) \right |\),我們不妨定義整張網路的流量為 \(st\) 的淨流量 \(F(st)\),這便是網路流

常見型別

  • 最大流:對於網路 \(G = (V, E)\),給每條邊一定的流量,使得網路流盡可能大,此時 \(F\)\(G\) 的最大流。

  • 最小割:對於網路 \(G = (V, E)\),找到 \(st \cdot ed\) 的割 \(\left \{ S, T \right \}\),使得其儘可能小,此時割 \(\left \{ S, T \right \}\)\(G\) 的最小割。

對於網路 \(G = (V,E)\),如果 \(\left \{ S, T \right \}\) 是點集 \(V\) 的劃分(通俗說就是分為了兩部分),即 \(S \cup T = V\) 並且 \(S \cap T = \varnothing\)。如果滿足 \(st \in S,ed \in T\),我們稱 \(\left \{ S, T \right \}\)\(G\) 的一個(Cut)。一個割具有容量,可以表示為:\(\left \| S,T \right \| \gets \textstyle{\sum_{u \in S}} \textstyle{ \sum_{v \in T}} \ c(u, v)\)

最大流

講解演算法之前,先給出增廣路(Augmenting Path)的定義:

對於邊 \(u \to v\),如果它的 \(f(u, v) \lt c(u, v)\),就說明這條邊有剩餘容量,為 \(c_f(u, v)\)。一條增廣路指從源點 \(st\) 到匯點 \(ed\) 的一條路徑,所有經過的邊都有剩餘容量。

不難發現,僅當不存在增廣路時,網路流才可能是最大的。

Ford-Fulkerson 演算法

Ford-Fulkerson 演算法(簡稱 FF 演算法)並不是直接用來求解最大流的,而是尋找並更新增廣路,從而求得。

如果單純的貪心,會發現不同的更新順序會導致不同的結果,然而確定次序十分複雜,FF 演算法便是採取了反悔的方式:

  1. 建圖時對於每條邊 \(u \to v\) 同時建立其反向邊 \(u \gets v\),只不過 \(c(v, u) = c_f(v, u) \gets 0\)

  2. 每一次更新增廣路時,如果邊 \(u \to v\) 流過 \(\Delta f\) 的流量,使:

\[c_f(u, v) \gets c_f(u, v) - \Delta f,c_f(v, u) \gets c_f(v, u) + \Delta f \]

  1. 如上反覆找到圖中增廣路並更新,直到不存在增廣路為止。

像這樣,透過推流操作帶來的抵消效果使得我們在貪心的時候可以反悔,無需正確的選擇也可以得到最優解。這就是 Ford–Fulkerson 演算法的增廣過程。

證明詳見 最大流 - OI Wiki (oi-wiki.org)

Edmonds–Karp 演算法

Edmonds–Karp 演算法(簡稱 EK 演算法)便是 FF 增廣的典型應用。

演算法過程

EK 演算法運用 BFS 的方式,每一次遍歷 \(G\) 時,找到一條新的增廣路,使用 FF 增廣更新流 \(f\) 以及相應的邊,得到新的殘餘容量子圖 \(G'\),不斷更新,直至增廣路不存在。

時間複雜度分析

這似乎是一種最原始的暴力,時間複雜度似乎由於更新的次數難以保證。由於其證明難度極其複雜,這裡不便贅述,詳見 最大流 - OI Wiki (oi-wiki.org)。增廣總次數為 \(O(|V| |E|)\),單次 BFS 增廣的時間上界為 \(O(| E |)\),因此總的時間複雜度為 \(O(|V| |E|^2)\)

程式碼實現

bool bfs()
{
    memset(vis, 0, sizeof vis);
    hh = 0, tt = -1;
    dist[st] = INF, vis[st] = true;
    q[++ tt] = st;

    while (hh <= tt)
    {
        int ver = q[hh ++];
        for (int i = h[ver]; ~i; i = ne[i])
        {
            int to = e[i]; 
            if (vis[to] || !w[i]) continue;
            dist[to] = min(dist[ver], w[i]);
            pre[to] = i;
            q[++ tt] = to, vis[to] = true;
            if (to == ed) return true;
        }
    }
    return false;
}

void update()
{
    int cur = ed;
    while (cur != st)
    {
        int nxt = pre[cur];
        w[nxt] -= dist[ed], w[nxt ^ 1] += dist[ed];
        cur = e[nxt ^ 1];
    }
    ans += dist[ed];
}

signed main()
{
	// ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	memset(h, -1, sizeof h);
    cin >> n >> m >> st >> ed;
    while (m --)
    {
        int a, b, c; cin >> a >> b >> c;
        if (edge[a][b]) w[edge[a][b]] += c;
        else add(a, b, c), add(b, a, 0), edge[a][b] = idx - 1;
    }

    while (bfs()) update();
    cout << ans << '\n';

	return 0;
}

Dinic 演算法

Dinic 演算法與 EK 演算法不同,但本質上都運用到了 FF 增廣的思想。

演算法過程

由於網路流只能單向傳輸,每條邊只能從一邊流經,我們以源點為起點建立與源點距離有關的分層圖,從而忽略掉許多不可能增廣的邊。分層圖可以透過 BFS 求得。

bool bfs()
{
    memset(vis, 0, sizeof vis);
    hh = 0, tt = -1;
    dist[st] = INF, vis[st] = true;
    q[++ tt] = st;

    while (hh <= tt)
    {
        int ver = q[hh ++];
        for (int i = h[ver]; ~i; i = ne[i])
        {
            int to = e[i]; 
            if (vis[to] || !w[i]) continue;
            dist[to] = min(dist[ver], w[i]);
            pre[to] = i;
            q[++ tt] = to, vis[to] = true;
            if (to == ed) return true;
        }
    }
    return false;
}

在求出的分層圖上,不斷的 DFS 求出剩餘的增廣路 \(f'\) 並且並列到原來的流 \(f\) 中,直到 BFS 時不存在從源點到匯點的增廣路,此時的流 \(f\)​ 便是最大流。

時間複雜度分析

透過一系列複雜的證明可知,單次 DFS 的時間複雜度為 \(O(|V| |E|)\),分層圖最多層數為 \(O(|V|)\),因此 Dinic 演算法的時間複雜度上界為 \(O(|V|^2 |E|)\)

值得注意的是,如果整張圖的容量均為 \(1\),Dinic 演算法的時間複雜度可以達到更優。

#include <bits/stdc++.h>

using namespace std;

#define int long long
#define DEBUG
#define x first
#define y second
#define File(a) freopen(a".in", "r", stdin); freopen(a".out", "w", stdout)

typedef long long LL;
typedef pair<int, int> PII;

const int N = 400, M = 10010;
const int INF = 0x3f3f3f3f;

int n, m, st, ed;
int cpy[N], h[N], e[M], ne[M], w[M], idx;
int dist[N], que[N], hh = 0, tt = -1;

inline void add(int a, int b, int c) { e[++ idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx; }

bool bfs()
{
    memset(dist, 0, sizeof dist);
    hh = 0, tt = -1;
    dist[st] = 1, que[++ tt] = st;

    while (hh <= tt)
    {
        int ver = que[hh ++];
        for (int i = h[ver]; ~i; i = ne[i])
        {
            int to = e[i];
            if (!dist[to] && w[i] > 0)
                dist[to] = dist[ver] + 1, que[++ tt] = to;
        }
    }
    return dist[ed];
}

int dfs(int ver, int flow)
{
    if (ver == ed || !flow) return flow;
    for (int &i = cpy[ver]; ~i; i = ne[i])
    {
        int to = e[i];
        if (dist[to] == dist[ver] + 1 && w[i] > 0)
        {
            int temp = dfs(to, min(flow, w[i]));
            if (temp > 0)
            {
                w[i] -= temp, w[i ^ 1] += temp;
                return temp;
            }
        }
    }
    return 0;
}

int dinic()
{
    int ans = 0, temp = 0;
    while (bfs())
    {
        memcpy(cpy, h, sizeof cpy);
        while (temp = dfs(st, INF))
            ans += temp;
    }
    return ans;
}

signed main()
{
    // ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    memset(h, -1, sizeof h), idx = -1;
    cin >> n >> m >> st >> ed;
    while (m --)
    {
        int a, b, c; cin >> a >> b >> c;
        add(a, b, c), add(b, a, 0);
    }

    cout << dinic() << '\n';

    return 0;
}

Reference

P3376 【模板】網路最大流 - 洛谷 | 電腦科學教育新生態 (luogu.com.cn)

網路流簡介 - OI Wiki (oi-wiki.org)

最大流 - OI Wiki (oi-wiki.org)

相關文章