网络流、最大流、费用流

网络流、最大流、费用流

(1.) 网络流概念

(1.1) 网络

网络指的是有向图 (G(V, E))

每条边 ((u, v)in E) 都有一个权值 (w(u, v)),称之为容量

两个特殊的点为源点 (s) 和汇点 (t)

(1.2)

定义 (f(u, v)) 为流经边 ((u, v)) 的流量

流量满足

  • 正反流量互为相反数,即 (f(u, v) = -f(v, u))
  • 流量限制,即 (f(u, v)leq w(u, v))
  • 流量守恒,即流出点的流量和流入该点的流量相等
  • 若边 ((u, v)) 流过一个大小为 (f(u, v)) 的流,那么该边剩余的容量为 (w(u, v) - f(u, v))

可行流指的就是能从源点 (s) 汇入汇点 (t) 的一个流量

最大流指的就是可行流的最大值

(1.3) 残余网络

网络上所有边的容量减去可行流的大小,得到的新的网络被称为残余网络

(1.4) 增广路

网络中能使可行流增加的一条路径被称为增广路

(1.5) 反悔边

经典图如下

走红线标注的路显然不是流量最大

考虑引入反悔边

反悔可以认为是决策的重构,边 ((a, b)) 的流量对答案的贡献并没有变化,所以这是正确的

不得不说,反悔边真是一个天才的想法

(1.6) 最大流算法

不断寻找增广路,同时建立反悔边

当无法找到增广路时,流量达到最大值

(2. Ford-Fulkerson Algorithm)

(2.1) (FF) 算法概括

  • (dfs) 寻找增广路
  • 建立反悔边
  • 重复上述过程

(2.2) (FF code)

/*FF Algorithm*/
#include <bits/stdc++.h>
#define fi first
#define se second
#define pii pair<int, int>
#define arrayDebug(a, l, r) for(int i = l; i <= r; ++i) printf("%d%c", a[i], " 
"[i == r])
typedef long long LL;
typedef unsigned long long ULL;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int inf = 0x3f3f3f3f;
const int DX[] = {0, -1, 0, 1, 0, -1, -1, 1, 1};
const int DY[] = {0, 0, 1, 0, -1, -1, 1, 1, -1};
const int MOD = 1e9 + 7;
const int N = 2e5 + 7;
const double PI = acos(-1);
const double EPS = 1e-6;
using namespace std;
inline int read()
{
    char c = getchar();
    int ans = 0, f = 1;
    while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
    while(isdigit(c)) {ans = ans * 10 + c - '0'; c = getchar();}
    return ans * f;
}

int n, m, s, t, cnt, head[N], vis[N];
struct edge
{
    int fr, to, next, w;
}e[N];

void addedge(int a, int b, int c)
{
    e[cnt].fr = a;//第一条边标为 1
    e[cnt].to = b;
    e[cnt].w = c;
    e[cnt].next = head[a];
    head[a] = cnt++;
}

int dfs(int u = s, int flow = inf)
{
    if(u == t) return flow;
    vis[u] = 1;
    for(int i = head[u]; ~i; i = e[i].next){
        int v = e[i].to;
        if(e[i].w == 0 || vis[v]) continue;
        int res = dfs(v, min(flow, e[i].w));
        if(res > 0) {
            e[i].w -= res;
            e[i ^ 1].w += res;
            return res;
        }
    }
    return 0;
}

int FF()
{
    int res = dfs(), ans = 0;
    while(res > 0) {
        memset(vis, 0, sizeof(vis));
        ans += res;
        res = dfs();
    }
    return ans;
}

int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d %d %d %d", &n, &m, &s, &t);
    for(int i = 1; i <= m; ++i) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        addedge(a, b, c);
        addedge(b, a, 0);
    }
    printf("%d
", FF());
    return 0;
}

(2.3) (FF) 时间复杂度及瓶颈

每次增广,至少增加 (1) 的流量

每次增广,最多流经 (m) 条边

所以理论复杂度为 (O(CM)),其中 (C) 为流量,(M) 为边数

如果流量特别大,那么这样的复杂度显然是不够优秀的

如下所示

(3. Edmond-Karp Algorithm)

(3.1) (EK) 算法概括

  • (bfs) 找最近的增广路
  • 建立反边
  • 重复上述过程

(3.2) (EK code)

/*EK Algorithm*/
#include <bits/stdc++.h>
#define fi first
#define se second
#define pii pair<int, int>
#define arrayDebug(a, l, r) for(int i = l; i <= r; ++i) printf("%d%c", a[i], " 
"[i == r])
typedef long long LL;
typedef unsigned long long ULL;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int inf = 0x3f3f3f3f;
const int DX[] = {0, -1, 0, 1, 0, -1, -1, 1, 1};
const int DY[] = {0, 0, 1, 0, -1, -1, 1, 1, -1};
const int MOD = 1e9 + 7;
const int N = 2e5 + 7;
const double PI = acos(-1);
const double EPS = 1e-6;
using namespace std;
inline int read()
{
    char c = getchar();
    int ans = 0, f = 1;
    while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
    while(isdigit(c)) {ans = ans * 10 + c - '0'; c = getchar();}
    return ans * f;
}

int n, m, s, t, cnt, head[N], vis[N], pre[N];
struct edge
{
    int fr, to, next, w;
}e[N];

void addedge(int a, int b, int c)
{
    e[cnt].fr = a;//第一条边标为 1
    e[cnt].to = b;
    e[cnt].w = c;
    e[cnt].next = head[a];
    head[a] = cnt++;
}

int bfs(int u = s)
{
    memset(vis, 0, sizeof(vis));
    memset(pre, -1, sizeof(pre));
    vis[u] = 1;
    queue<int> q;
    q.push(u);
    while(!q.empty()) {
        int temp = q.front(); q.pop();
        if(temp == t) break;
        for(int i = head[temp]; ~i; i = e[i].next) {
            if(vis[e[i].to] || e[i].w == 0) continue;
            vis[e[i].to] = 1;
            pre[e[i].to] = i; //记录点 x 的前驱边的序号
            q.push(e[i].to);
        }
    }
    int flow = inf;
    for(int i = pre[t]; ~i; i = pre[e[i].fr]) //找出流量
        flow = min(flow, e[i].w);
    if(flow == inf) return 0;
    for(int i = pre[t]; ~i; i = pre[e[i].fr]) { //建立反边
        e[i].w -= flow;
        e[i ^ 1].w += flow;
    }
    return flow;
}

int EK()
{
    int res = bfs(), ans = 0;
    while(res > 0) {
        ans += res;
        res = bfs();
    }
    return ans;
}
int main()
{
    memset(head, -1, sizeof(head));
    scanf("%d %d %d %d", &n, &m, &s, &t);
    for(int i = 1; i <= m; ++i) {
        int a, b, c;
        scanf("%d %d %d", &a, &b, &c);
        addedge(a, b, c);
        addedge(b, a, 0);
    }
    printf("%d
", EK());
    return 0;
}
/*
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 40
*/

(3.3) (EK) 时间复杂度及瓶颈

(O(NM^2)),其中 (N) 为顶点数,(M) 为边数

当边数 (M) 特别大时,这样的复杂度是不够优秀的

(4. Dinic Algorithm)

(4.1) (Dinic) 算法概述

  • 先用 (bfs) 对网络图分层,即预处理出每个点相对于源点的距离
  • 然后用 (dfs) 多路增广,同时建立反边,直到当前图没有流量为止
  • 重复上述过程

多路增广的含义是,不是找到一个流量就返回,而是要穷尽当前网络的所有流量才返回

(4.2) (Dinic) 的特点以及其他优化

(4.2.1) 多路增广

相较于传统 (dfs) 寻找增广路,多路增广实际上找出了一个增广路网

(4.2.2) 分层图

设立分层图的本质就是预处理每个点相对于源点的距离

这样流量在流动时避免“绕路”,而是直接流向更远的点


(4.2.3) 可行性剪枝

若当前点流出的剩余流量为零,那么无法继续向下传输流量

所以直接退出循环,回溯到上一层

(4.2.4) 当前弧优化

多路增广时,已经增广的路径可能会被再次访问

(cur) 数组记录点 (u) 已经被增广的出边,下次直接从未被增广的出边开始增广

for(int &i = cur[u]; ~i; i = e[i].next) {
    ...
}

注意到这里的引用取值

随着 (i) 增加,(cur[i]) 也会增加,于是起到了记录的作用

值得一提的是,当前弧优化的优化效果相当不错

(4.3) (Dinic code)

/*Dinic Algorithm*/
#include <bits/stdc++.h>
#define fi first
#define se second
#define pii pair<int, int>
#define arrayDebug(a, l, r) for(int i = l; i <= r; ++i) printf("%d%c", a[i], " 
"[i == r])
typedef long long LL;
typedef unsigned long long ULL;
const LL INF = 0x3f3f3f3f3f3f3f3f;
const int inf = 0x3f3f3f3f;
const int DX[] = {0, -1, 0, 1, 0, -1, -1, 1, 1};
const int DY[] = {0, 0, 1, 0, -1, -1, 1, 1, -1};
const int MOD = 1e9 + 7;
const int N = 2e5 + 7;
const double PI = acos(-1);
const double EPS = 1e-6;
using namespace std;
inline int read()
{
    char c = getchar();
    int ans = 0, f = 1;
    while(!isdigit(c)) {if(c == '-') f = -1; c = getchar();}
    while(isdigit(c)) {ans = ans * 10 + c - '0'; c = getchar();}
    return ans * f;
}

int n, m, s, t, cnt, head[N], dep[N], cur[N];
struct edge
{
    int fr, to, next, w;
}e[N];

void addedge(int a, int b, int c)
{
    e[cnt].fr = a;//第一条边标为 0,对应反边为 1
    e[cnt].to = b;
    e[cnt].w = c;
    e[cnt].next = head[a];
    head[a] = cnt++;
}

void debug()
{
    for(int i = 1; i <= n; ++i){
        cout<<i<<": ";
        for(int j = head[i]; ~j; j = e[j].next)
            printf("(%d, %d, %d)", e[j].fr, e[j].to, e[j].w);
        puts("");
        cout<<dep[i]<<endl;
    }
    puts("");
}

bool bfs()
{
    memset(dep, 0, sizeof(dep)); //dep 有标记,则可以访问到该点
    queue<int> q; q.push(s);
    dep[s] = 1;
    while(!q.empty()) {
        int u = q.front(); q.pop();
        for(int i = head[u]; ~i; i = e[i].next) {
            int v = e[i].to, cost = e[i].w;
            if(cost && !dep[v]) dep[v] = dep[u] + 1, q.push(v);
        }
    }
    return dep[t];
}

int dfs(int u = s, int flow = inf)
{
    if(u == t) return flow;
    int ans = 0;
    for(int &i = cur[u]; ~i; i = e[i].next) {
        int v = e[i].to, cost = e[i].w;
        if(dep[v] == dep[u] + 1 && cost) {
            int res = dfs(v, min(flow, cost));
            if(res > 0) {
                e[i].w -= res, e[i ^ 1].w += res; //建立反边
                flow -= res; //可以流通的流量减少
                ans += res; //答案的贡献增加
                if(!flow) break; //没有可以流通的流量,直接退出循环
            }
        }
    }
    return ans;
}

int Dcnic()
{
    int ans = 0;
    while(bfs()) {
        memcpy(cur, head, sizeof(head));
        ans += dfs();
    }
    return ans;
}

int main()
{
    memset(head, -1, sizeof(head));
    n = read(), m = read(), s = read(), t = read();
    for(int i = 1; i <= m; ++i) {
        int a, b, c;
        a = read(), b = read(), c = read();
        addedge(a, b, c);
        addedge(b, a, 0);
    }
    printf("%d
", Dcnic());
    return 0;
}
/*
4 5 4 3
4 2 30
4 3 20
2 3 20
2 1 30
1 3 40
*/

(4.4) (Dinic) 时间复杂度

(O(N^2M)),其中 (N) 为顶点数,(M) 为边数

原文地址:https://www.cnblogs.com/ChenyangXu/p/12730994.html