网络最大流

Posted natsuka

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了网络最大流相关的知识,希望对你有一定的参考价值。

(quad) 网络流(Network-Flows)指的是一张联通无向图 (G),每条边具有两个属性:容量、流量。每条边的流量常用 (f_{i,j}) 表示,容量常用 (c_{i,j}) 表示。网络流有容量限制(Capacity Constraints),一条边的流量不可以超过它的容量;网络流流守恒(Flow Conservation),对于每个除了源点、汇点之外的点,都有:(f_{in} = f_{out}),即流入的流量始终等于流出的流量。
(quad) (ullet) 源点(Source Point)
(qquad) 即所有流量的源头。性质:只出不进。
(quad) (ullet) 汇点(Sink Point)
(qquad) 即所有流量的终点。性质:只进不出。
(quad) (ullet) 容量(Capacity)
(qquad) 每条有向边固有的属性。容量指这条边可以容纳的最大流量。
(quad) (ullet) 残量(Residual Capacity)
(qquad) 容量与流量的差。
(quad) (ullet) (Cut)
(qquad) 割又叫割集(Cutset)。在网络流中,割是一些边的集合,满足当整张图去除割集中所有边后不再联通。割所允许通过的最大流量就是割的容量。
(quad) (ullet) 最大流最小割定理(Maxium Flow Minium Cut Theorem)
(qquad) 所有割的容量都比所有可行流的流量大;特别的,容量最小的割的容量等于最大流的流量。如果有一个可行流的流量与一个割的容量相等,那么它们分别为最大流和最小割。
(quad) 如果每条边的流量均不大于它们的容量,则称之为可行流(Feasible Flow),其流量为汇点的流量。特别地,当所有边的流量均为 (0) 时,称为零流,其流量为 (0)。对于一个可行流,如果存在一条从源点 (s) 至汇点 (t) 的路径,路径上所有边残量的最小值 (e > 0),则这条路径称为一条增广路(Augmenting Path),这条增广路的容量为 (e)。当一个可行流不存在增广路时,这个可行流称为最大流(Maxium Flow)。
(quad) 那么,欲求最大流,只需求出一个零流中的所有增广路并增广即可。然而,一条不恰当的增广路可能会阻塞其他的增广路,使得最终的总流量变小。
技术图片
(quad) 如上图,每条边上所标数字代表每条边的残量,初始时为零流,残量等于容量,(1) 为源点,(4) 为汇点。
技术图片
(quad) 猴子找到了一条增广路:(1 o 2 o 3 o 4),流量为 (1)。增广后不存在其他增广路,得出最大流为 (1)。然而,显然 (1 o 2 o 4,, 1 o 3 o 4) 才是真正的最大流,流量为 (2)
技术图片
(quad) 问题出在了哪儿?我们没有给予程序一个“反悔”的机会。只需要在选择 (1 o 2 o 3 o 4) 的同时添加相应的反向边即可。接下来,程序得以找到第二条大小为 (1) 的增广路 (1 o 3 o 2 o 4),得到正确的最大流 (2)。选择 (3 o 2) 这条反向边增广相当于反悔之前选择 (2 o 3) 的操作。
(quad) 这就是 Edmonds-Karp 算法,增广路可以用宽度优先搜索查找。其时间复杂度上限为 (O(n cdot m^2))

#include <stdio.h>
#include <string.h>

#include <queue>

const int MAXN = 2e2 + 19;

inline int min(const int& a, const int& b){
    return a < b ? a : b;
}

int n, m, s, t, c[MAXN][MAXN], pre[MAXN];

int bfs(){
    int stream = 0x3f3f3f3f; memset(pre, 0, sizeof(pre));
    std::queue<int>q;
    q.push(s);
    while(!q.empty()){
        int node = q.front();
        q.pop();
        if(node == t)
            break;
        for(int i = 1; i <= n; ++i)
            if(!pre[i] && c[node][i]){
                pre[i] = node;
                stream = min(stream, c[node][i]);
                q.push(i);
            }
    }
    pre[s] = 0;
    if(pre[t])
        return stream;
    else
        return 0;
}

int edmonds_karp(void){
    int stream, flow = 0;
    while(stream = bfs()){
        for(int u = t; pre[u]; u = pre[u])
            c[pre[u]][u] -= stream, c[u][pre[u]] += stream;
        flow += stream;
    }
    return flow;
}

int main(){
    scanf("%d%d%d%d", &n, &m, &s, &t);
    for(int i = 1, u, v, w; i <= m; ++i)
        scanf("%d%d%d", &u, &v, &w), c[u][v] += w;
    printf("%d
", edmonds_karp());
    return 0;
}

(quad) 一般的 EK 算法使用邻接矩阵存图,占用空间太多,效率太低。用链式前向星可以一定程度上优化它。

#include <stdio.h>
#include <string.h>

#include <queue>

const int MAXN = 1e4 + 19, MAXM = 1e5 + 19;

struct Edge{
    int to, next, c;
}edge[MAXM];

int cnt, head[MAXN];

inline void add(int from, int to, int c){
    edge[cnt].to = to;
    edge[cnt].c = c;
    edge[cnt].next = head[from];
    head[from] = cnt++;
}

inline int min(const int& a, const int& b){
    return a < b ? a : b;
}

int n, m, s, t, pre[MAXN];

int bfs(){
    int stream = 0x3f3f3f3f; memset(pre, -1, sizeof(pre));
    std::queue<int>q;
    q.push(s);
    pre[s] = 0;
    while(!q.empty()){
        int node = q.front();
        q.pop();
        if(node == t)
            break;
        for(int i = head[node]; i != -1; i = edge[i].next)
            if(pre[edge[i].to] == -1 && edge[i].c){
                stream = min(stream, edge[i].c);
                pre[edge[i].to] = i;
                q.push(edge[i].to);
            }
    }
    pre[s] = -1;
    if(pre[t] != -1)
        return stream;
    else
        return 0;
}

int edmonds_karp(void){
    int stream = 0, flow = 0;
    while(stream = bfs()){
        for(int u = pre[t]; u != -1; u = pre[edge[u ^ 1].to])
            edge[u].c -= stream, edge[u ^ 1].c += stream;
        flow += stream;
    }
    return flow;
}

int main(){
    memset(head, -1, sizeof(head));
    scanf("%d%d%d%d", &n, &m, &s, &t);
    for(int i = 1, u, v, w; i <= m; ++i)
        scanf("%d%d%d", &u, &v, &w), add(u, v, w), add(v, u, 0);
    printf("%d
", edmonds_karp());
    return 0;
}

(quad) 慢是 Edmonds-Karp 算法的硬伤。Dinic 算法减少了搜索的次数,一定程度上解决了这个问题。
(quad) Dicnic 算法要求先将整张图分为若干层:(G_0,G_1,G_2,G_3,...G_k)。其中,源点在 (G_0) 中,与源点最短距离为 (k) 的在 (G_k) 中。显然,(forall G_i,G_j),若 (|i - j| > 1),则 (forall x in G_i,y in G_j) 都不存在一条连接 (x, y) 的边。这样,使用深度优先搜索寻找增广路,要求每次都进入更深的一层图,即可大大减少搜索次数。
技术图片
(quad)P3376 【模板】网络最大流 中的样例为例。
技术图片
(quad) 为了方便,定义 (dep_s = 1)。一次宽度优先搜索即可获得所有点的深度。

int bfs(void){
    memset(dep, 0, sizeof(dep)); dep[s] = 1;
    std::queue<int>q; q.push(s);
    while(!q.empty()){
        int node = q.front();
        for(int i = head[node]; i != -1; i = edge[i].next)
            if(!dep[edge[i].to] && edge[i].capacity)
                dep[edge[i].to] = dep[node] + 1, q.push(edge[i].to);
        q.pop();
    }
    return dep[t];
}

技术图片
(quad) 深搜发现了一条流量为 (20) 的增广路,增广并建立反向边,回溯至源点并退出。
技术图片
(quad) 重新标记各点深度(这里没有变化是个巧合),再次深搜,又发现一条 (20) 的增广路,建立反向边,回溯至 (2)
技术图片
(quad) 搜索下一个深度为 (3) 的节点,发现无法到达,回溯至 (2)。此时 (2) 已无其他出边,回溯至上一个节点,由于是源点,退出。
技术图片
(quad) 重新标记各点深度。如法炮制,找到一条 (10) 的增广路。至此已无其他增广路,一路回溯,返回结果 (50)
(quad) 按照以上的思路,深搜代码为:

int dfs(int node, int flow){
    if(node == t || !flow)
        return flow;
    int stream = 0, f;
    for(int i = head[node]; i != -1; i = edge[i].next)
        if(dep[edge[i].to] == dep[node] + 1 && (f = dfs(edge[i].to, min(flow, edge[i].capacity)))){
            flow -= f, stream += f;
            edge[i].capacity -= f, edge[i ^ 1].capacity += f;
            if(!flow)
                break;
        }
    return stream;
}

(quad) Dinic 代码为:

int dinic(void){
    int flow = 0;
    while(bfs())
        flow += dfs(s, INF);
    return flow;
}

(quad) 这样的代码已经能够通过 P3376 【模板】网络最大流 了。ISAP 算法将在下一篇博客介绍。

以上是关于网络最大流的主要内容,如果未能解决你的问题,请参考以下文章

模板网络最大流

试题库问题(最大流Isap) 网络流

网络流24题-飞行员配对方案问题-二分图最大匹配

网络流总结费用流

P3376 模板网络最大流——————Q - Marriage Match IV(最短路&最大流)

解题报告 『酒店之王(网络最大流 + 拆点)』