• 网络流、最大流、费用流


    网络流、最大流、费用流

    (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) 为边数

  • 相关阅读:
    Calling Convention的总结
    形参传递关键点
    linux input输入子系统分析《四》:input子系统整体流程全面分析
    22.Linux-块设备驱动之框架详细分析(详解)
    spring: 使用profiles选择数据源(结合嵌入式数据源EmbeddedDatabaseBuilder)
    spring: 使用嵌入式数据源 EmbeddedDatabaseBuilder
    jsp:jstl标签之控制流程
    jsp: jstl标签库
    jsp:tld标签
    spring boot: 组合注解与元注解
  • 原文地址:https://www.cnblogs.com/ChenyangXu/p/12730994.html
Copyright © 2020-2023  润新知