Skip to content

網路流

性質和例子

網路流的性質:有起點和終點,有流量限制。

例子:水流、網速、高速公路。

定義

給定一張圖 ,每條邊 有容量\ 和流量 ,剩餘流量 。

給定 和匯點 ,我們稱網路流為「 網路流」, 網路流的流量 源點 流出的流量和, 流進匯點 的流量和。

可行流

給定一個 網路流,如果符合以下條件,稱為「 可行流」,也就是合法的 網路流

  • 流量限制:
  • 流量對稱:
  • 流量守衡
    • 非源點 和匯點 ,流入的流量和等於流出的流量和 流入的流量和等於流出的流量和
    • 源點 流出的流量和等於流進匯點 的流量和

最大流演算法

如上文所述,一個 可行流( 網路流)的流量 源點 流出的流量和, 流進匯點 的流量和。在現實例子中,我們希望 越大越好。

基本想法:重複尋找從 到 的路徑,計算這條路徑最大流量 ,,並將這條路徑所有邊扣除 ,無法找到一條路徑使 增加就停止。

但這樣的演算法不一定能找到最大流量,如下圖的最大流量是 ,但只找到 單位的流量。

為了解決這個問題,我們讓流量可以逆行,根據流量限制和流量對稱,一條邊 ,反向邊 的容量 ,流量 ,所有剩餘流量 的邊形成的圖稱為剩餘網路 。

藉由逆行的機制,選擇路徑 ,可以再找到 單位的流量。

增廣路徑 (Augmenting Path)

  • 假設 是可行流, 是 的剩餘網路
  • 是 在一條從 到 的路徑,稱為擴充路徑。
  • 一條邊 增加流量 ,代表
增廣路徑

假設 是可行流中的剩餘網路, 是 在一條從 到 的路徑,且每一條邊的剩餘流量 ,稱為擴充路徑。

令 為擴充路徑的瓶頸流量,也就是為可行流再找到 的流量。假設 為 上往匯點 的邊。

  • 所有前往匯點的邊流量加上 :
  • 所有離開匯點的邊流量扣除 :

藉由加入反向邊機制和定義增廣路徑,找最大流的演算法雛形誕生:在剩餘網路上找到增廣路徑來擴充流量,直到剩餘網路沒有從 的路徑,即獲得最大流。

根據增廣路徑的找法,下列簡述三個演算法:

  • Ford-fulkerson:利用 DFS 枚舉增廣路徑,如果流量為實數域無法保證程式會結束,流量為整數域時間複雜度為 。
  • Edmonds-karp:利用最短路枚舉增廣路徑,時間複雜度
  • Dinic:先做 BFS 將點分層(有向無環圖),再用 DFS 遍歷搜尋增廣路徑,一次找到多條增廣路徑,時間複雜度為 。

Dinic 演算法實作

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
using LL = long long;
struct Dinic
{
    int n, s, t, level[MXV], now[MXV];
    struct Edge
    {
        int v;
        LL rf; // rf: residual flow
        int re;
    };
    vector<Edge> e[MXV];
    void init(int _n, int _s, int _t)
    {
        n = _n;
        s = _s;
        t = _t;
        for (int i = 0; i <= n; i++)
            e[i].clear();
    }
    void add_edge(int u, int v, LL f)
    {
        e[u].push_back({v, f, (int)e[v].size()});
        e[v].push_back({u, f, (int)e[u].size() - 1});
        // for directional graph
        // e[v].push_back({u, 0, (int)e[u].size() - 1});
    }
    bool bfs()
    {
        fill(level, level + n + 1, -1);
        queue<int> q;
        q.push(s);
        level[s] = 0;
        while (!q.empty())
        {
            int u = q.front();
            q.pop();
            for (auto it : e[u])
            {
                if (it.rf > 0 && level[it.v] == -1)
                {
                    level[it.v] = level[u] + 1;
                    q.push(it.v);
                }
            }
        }
        return level[t] != -1;
    }
    LL dfs(int u, LL limit)
    {
        if (u == t)
            return limit;
        LL res = 0;
        while (now[u] < (int)e[u].size())
        {
            Edge &it = e[u][now[u]];
            if (it.rf > 0 && level[it.v] == level[u] + 1)
            {
                LL f = dfs(it.v, min(limit, it.rf));
                res += f;
                limit -= f;
                it.rf -= f;
                e[it.v][it.re].rf += f;
                if (limit == 0)
                    return res;
            }
            else
                ++now[u];
        }
        if (!res)
            level[u] = -1;
        return res;
    }
    LL flow(LL res = 0)
    {
        while (bfs())
        {
            memset(now, 0, sizeof(now));
            res += dfs(s, INF);
        }
        return res;
    }
};

/*
usage
Dinic dinic; // declare
dinic.init(n, s, t); // initialize, n vertexs, start from s to t
dinic.add_edge(x, y, z); // add edge from x to y, weight is z
dinic.flow() // calculate max flow 
*/

變數解釋

  • :點的個數,源點、匯點。
  • :BFS 裡用來表示這次點 是第幾層。
  • :DFS 裡用來表示這次點 是第幾層。
  • :有向邊,存放終點、剩餘流量,反向邊的編號。反向邊的編號用在 DFS 更新反向邊的剩餘流量。

新增邊

  • 無向邊:拆成兩條方向相反,剩餘流量相同的有向邊。
  • 有向邊:增加正向邊和反向邊。

BFS

  • 設源點 為第 層。
  • 將所有和 相連且剩餘流量 的點設為第 層。
  • 將所有和第 層的點相連且剩餘流量 的點設為第 層。
  • 以此類推,已經標記的點不會再標記第 次。
  • 如果匯點 沒有被標記,代表已經找到最大流了。

DFS

  • 依據 BFS 的分層標記,遍歷完所有滿足 第 層的點 第 層的點 的路徑。
  • 在往下遞迴時計算瓶頸流量,往上時更新相關邊的剩餘流量。

二分圖最大匹配

設二分圖兩個集合為 , 集合的所有點和源點連接一條剩餘流量無限大的邊, 集合的所有點和匯點連接一條剩餘流量無限大的邊,二分圖原本的邊剩餘流量設為 ,最大流即為二分圖最大匹配。

DAG 的最小路徑覆蓋

DAG 的最小路徑覆蓋

給你一張 DAG,請問問最少要幾條路徑才能覆蓋所有點。

分成不相交路徑和相交路徑。

不相交路徑: ,共 條。

相交路徑: ,共 條。

不相交路徑:將一個點拆成兩個點,分別代表指向別人的點和被別人指向的點。

一開始每個點都是獨立的路徑,總共有 條,在二分圖上找到一條匹配邊,就等於把兩條路徑變成一條,路徑數 。答案為 最大匹配數。

相交路徑版本:先做一次 Floyd-warshall,如果 可以藉由其他點抵達 ,建一條 的邊。題目變成頂點不相交的最小路徑覆蓋。

以下圖為例,如果選定了 作為第一條路徑, 可以在不經過 的情況下連到 。

最小(大)花費最大流

最小(大)花費最大流

每條邊除了流量以外還有單位價錢 。對於每條邊,如果流了 的流量就要付 的水道維護費,在最大流量的情況下,要讓花費最小(大)。

演算法和 Dinic 相似,改用最短路徑演算法找尋增廣路徑,因為要處裡負邊 (反向邊),要使用 SPFA。如果要計算最大花費,在一開始把花費乘以 ,算出結果後再乘以 。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
using LL = long long;
struct MCMF
{
    struct Edge
    {
        int u, v;
        LL cost, cap;
    };
    int n, pre[MXV], cnt[MXV];
    vector<Edge> edges;
    vector<int> G[MXV];
    LL dis[MXV], ansFlow, ansCost;
    bitset<MXV> inque;
    void init(int _n)
    {
        n = _n;
        edges.clear();
        for (int i = 0; i <= n; ++i)
            G[i].clear();
    }
    void addEdge(int u, int v, LL cost, LL cap)
    {
        G[u].push_back(edges.size());
        edges.push_back({u, v, cost, cap});
        G[v].push_back(edges.size());
        edges.push_back({v, u, -cost, 0});
    }
    bool spfa(int s, int t)
    {
        queue<int> q;
        bool negative = false;
        fill(begin(dis), end(dis), INF);
        fill(begin(pre), end(pre), -1);
        fill(begin(cnt), end(cnt), 0);
        inque.reset();
        dis[s] = 0;
        cnt[s] = 1;
        q.push(s);
        inque[s] = true;
        while (!q.empty() && !negative)
        {
            int u = q.front();
            q.pop();
            inque[u] = false;
            for (int i : G[u])
            {
                Edge &e = edges[i];
                int v = e.v;
                LL cost = e.cost, cap = e.cap;
                if (dis[v] > dis[u] + cost && cap > 0)
                {
                    dis[v] = dis[u] + cost;
                    pre[v] = i;
                    if (inque[v])
                        continue;
                    q.push(v);
                    inque[v] = true;
                    ++cnt[v];
                    if (cnt[v] == n + 2)
                    {
                        negative = true;
                        break;
                    }
                }
            }
        }
        return dis[t] != INF;
    }
    LL update(int u, LL limit)
    {
        if (pre[u] == -1)
            return limit;
        int i = pre[u];
        Edge &e = edges[i];
        LL f = update(e.u, min(limit, e.cap));
        ansCost += f * e.cost;
        edges[i].cap -= f;
        edges[i ^ 1].cap += f;
        return f;
    }
    PLL sol(int s, int t)
    {
        ansFlow = ansCost = 0;
        while (spfa(s, t))
            ansFlow += update(t, INF);
        return make_pair(ansFlow, ansCost);
    }
};

/*
usage
MCMF<int> mcmf; // declare
mcmf.init(n); // initialize, n vertexs
mcmf.add_edge(x, y, z); // add edge from x to y, weight is z
mcmf.sol(s, t, d) // calculate cost flow, start from s to t max flow 
*/

變數解釋

  • :有向邊,存放每條邊的兩端點、費用和剩餘流量。
  • :點的個數
  • :SPFA 中,紀錄 是被哪個點鬆弛,也可以說是在最短路徑中, 的父節點。
  • :SPFA 中,紀錄從源點 流一單位到 的最小(大)成本。
  • :用於 SPFA。
  • :紀錄最大流和最小(大)成本。

新增邊

  • 和 Dinic 新增反向邊的想法類似,差別在於反向邊的成本是原本的成本乘以 。

SPFA

  • 根據成本和流量鬆弛。
  • 如果匯點 的最小成本 ,代表已經找到最小(大)花費最大流。

update

  • 根據 ,可以找到一條從匯點 到源點 的路徑,利用遞迴找到瓶頸流量,再更新最大流和最小(大)花費。

相關題目

LibreOJ 6013

有 個環形倉庫,每個倉庫物品數量不等,貨物只能往相鄰的倉庫移動,問最少要搬運多少貨品讓每個倉庫物品數量相同?

算出 間倉庫的平均貨物數 。

對於第 間倉庫貨物數 ,如果 ,建一條從 到 的容量 花費為 的邊,如果 ,建一條從 到 的容量 花費為 的邊。

為了防止從 容出的容量直接跑到 ,將所有點拆成兩個點 ,對於所有的 ,分別和 建立一條容量無限花費為 的邊。

最大花費即為答案。

LibreOJ 6014

給定 個區間 ,選定一些區間,如果集合內包含點 的區間不超過 個,稱為合法集合,問合法集合的最長長度和為多少。

因為 的範圍過大,須將 離散化,另外要去除重複元素,得到時間段 。

建立一條源點 到 容量為 花費為 的邊,建立一條 到匯點 容量為 花費為 的邊,相鄰兩個時間段建立一條容量無限花費為 的邊。

對於每個區間 ,建立一條從 到 容量為 花費為 的邊。

最大花費即為答案。

例題練習

  • DAG 的最小路徑覆蓋
    • POJ 1422
    • POJ 2594
    • LibreOJ 6002
    • LibreOJ 6003