zoukankan      html  css  js  c++  java
  • 网络流学习笔记-最大流问题的四种算法

    最大流问题

    最大流问题满足以下三个条件:

    • 容量限制:(f(u,v)≤c(u,v))
    • 斜对称性:(f(u,v)=-f(v,u))
    • 流量平衡:(sum_ {(u,v)∈E}f(u,v)=0)(除(s),(t)外的图中任意两点)

    原图中不存在的边也假设存在,视为容量为0.

    残量网络:计算出每条边上容量(c)与流量(f)之差后所得到的图。由于上述的原因,边数可能是原图的两倍。对于原图中一条(c=16,f=11)的单向边,在残量网络中对应边权为(5(16-11))(11(0-(-11)))的两条边。

    增广路算法

    当且仅当残量网络中不存在(s→t)的有向道路(增广路)时,此时的流是从(s)(t)的最大流。

    Edmonds-Karp算法

    核心流程

    • 对图进行(BFS),直到形成(s->t)的通路(即当找到(t)时),同时维护这条通路上的最小残量(minflow=e[i].cap-e[i].flow),并保存通路上每一个节点的入边。只要能到达(t),这条通路就是增广路,否则图中已经没有增广路。
    • 形成通路之后,按照先前保留的各节点的入边,从终点(t)开始沿着通路倒退回去,并更新路上每一条边的正向边的流量e[i].flow与反向边的流量e[i^1].flow
    • 整条通路的流量更新完毕后,更新答案(maxflow)
    • 重复以上步骤,直到图中没有增广路。

    优化方法

    • 重边处理。即将正向边(u→v)的多条管道合并为一条,只需将(cap)属性累加即可。而反向边不需要特殊处理。

    (链式前向星+重边处理)

    int n, m, s, t;
    int num = 1;//让边的编号从2开始
    int head[maxn], pre[maxn];
    LL flow = 0;
    LL d[maxn];
    int flag[300][300];
    //记录重边,重边的容量累加到一条边上
    
    struct Edge {
        int next, to;
        LL cap;
        LL flow;
    }e[maxn*4];
    
    void addedge(int from,int to,LL cap){
        //存正向边
        num++;
        e[num].next = head[from];
        e[num].to = to;
        e[num].cap = cap;
        e[num].flow = 0;
        head[from] = num;
        //存反向边
        num++;
        e[num].next = head[to];
        e[num].to = from;
        e[num].cap = 0;
        e[num].flow = 0;
        head[to] = num;
    }
    
    void update() {
        //从终点开始退回去,沿路修改边权
        //通路的最小值m即d[t]
        for (int x = t; x != s; x = e[pre[x]^1].to) {
            e[pre[x]].flow += d[t];
            e[pre[x] ^ 1].flow -= d[t];
        }
        flow += d[t];
    }
    
    void ek(int s, int t) {
        for (;;) {
            mem(d, 0);
            //d[i]记录s->i路径上的最小残量
            //由于总是正数,可以同时起到记录是否访问过的作用
            queue<int> q;
            q.push(s);
            d[s] = INF;
            while (q.size()) {
                int u = q.front(); q.pop();
                for (int i = head[u]; i; i = e[i].next) {
                    if (!d[e[i].to] && e[i].cap > e[i].flow) {
                        pre[e[i].to] = i;
                        //记录e[[i].to是从哪条边过来的
                        d[e[i].to] = min(d[u], e[i].cap - e[i].flow);
                        q.push(e[i].to);
                    }
                }
                if (d[t]) break;
                //已经访问过t,可以跳出了
            }
            if (!d[t]) break;
            //整张图找遍依然到达不了t,说明已经没有增广路了
            update();
        }
    }
    
    int main() {
        cin >> n >> m >> s >> t;
        mem(flag, 0);
        f(i, 1, m) {
            int u, v, w;
            cin >> u >> v >> w;
            if (!flag[u][v]) {
                addedge(u, v, w);
                flag[u][v] = num - 1;
                //num是v->u的编号
                //num-1才是u->v的编号
            }
            else e[flag[u][v]].cap += w;
            //如果是重边,容量加到已存在的那条边上
        }
        ek(s,t);
        cout << flow;
        return 0;
    }
    

    Dinic算法

    EK算法中,每一次BFS都只能找到一条增广路,效率不高。而Dinic算法则能通过BFS同时找到多条最短增广路,再通过DFS对找出的所有最短增广路同时增广。

    核心流程

    • 对图进行BFS,直到形成(s→ t)的通路(即找到终点(t))。同时用数组(d[i])记录每个节点(i)所在层级,这类似于经典BFS题目中常记录的步数。BFS的目的在于找到图中的最短增广路。其中最短定义为路上经过的节点数最少。联想到BFS的性质可以知道,当找到终点(t)时,找到的最短增广路有多条

    • 在BFS成功形成(s→ t)通路的基础上,从(s)开始进行DFS。搜索路径所经过的节点层级严格递增,以保证走的是最短增广路。同时维护路径上的最小残量(minflow)。当找到终点(t)时,带着整条通路上的最小残量(minflow)回溯,这相当于(EK)算法中“从终点开始倒退回去沿路更新”的操作。

      • 在回溯过程中,同(EK)算法一样,会更新路上每一条边的正向边的流量e[i].flow与反向边的流量e[i^1].flow。但还要更新额外的变量(nowflow),表示将当前节点(now)视为起点,以此出发后所经过的所有最短增广路的总流量。以及将当前节点(now)视为终点时的通路最小残量(minflow)。故前者加,后者减。
      • 当节点(now)的各数据更新完毕后,带着以(now)为起点的总流量(nowflow)回溯。
    • 一次DFS最终得到的值,即以(s)为起点的多条最短增广路的总流量(nowflow),用它更新答案(maxflow)

    • 重复以上所有步骤,直到BFS找不到增广路。

    优化方法

    • 剪枝。注意到当前节点(now)的各数据更新是基于更深一层(DFS)回溯上来的值(k),当(k)(0)时,它对节点的更新是无任何贡献的,所以可以把(k)的来源节点(v)从层级网络中剥离,即令d[v]=INF,这样之后的DFS就不会再访问以(v)为起点的最短增广路了。

    • 当前弧优化。(这里的弧其实就是边)

      对于一个节点(u),当它在DFS中走到了第(i)条弧时,前(i-1)条弧到汇点的流一定已经被流满而没有可行的路线了。那么当下一次再访问(u)节点的时候,前(i-1)条弧就可以被删掉而没有任何意义了。所以我们可以在每次枚举节点(now)所连的弧时,改变枚举的起点,这样就可以删除起点以前的所有弧以达到优化的效果。

    typedef long long LL;
    const long long INF = 2005020600;
    const int maxn = 5e4 + 100;
    
    int n, m, s, t;
    int num = 1;//让边的编号从2开始
    int head[maxn];
    bool vis[maxn];
    LL d[maxn], Maxflow;
    int cur[maxn];
    int flag[300][300];
    int cnt[maxn];
    //记录重边,重边的容量累加到一条边上
    
    struct Edge {
        int next, to;
        LL cap;
        LL flow;
    
    }e[maxn*4];
    
    void addedge(int from,int to,LL cap){
        //存正向边
        num++;
        e[num].next = head[from];
        e[num].to = to;
        e[num].cap = cap;
        e[num].flow = 0;
        head[from] = num;
        //存反向边
        num++;
        e[num].next = head[to];
        e[num].to = from;
        e[num].cap = 0;
        e[num].flow = 0;
        head[to] = num;
    }
    
    bool bfs() {
        for (int i = 0; i <= n; i++) d[i] = INF;
        queue<int>q;
        q.push(s);
        d[s] = 0;
        cnt[s] = head[s];
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (int i = head[u]; i; i = e[i].next) {
                if (d[e[i].to]==INF && e[i].cap > e[i].flow) {
                    d[e[i].to] = d[u] + 1;
                    cnt[e[i].to] = head[e[i].to];
                    q.push(e[i].to);
                    if (e[i].to == t) return true;
                }
            }
        }
        return false;
    }
    
    //bfs将整张图跑一遍,将所有点分层,这样就可以一次性找出s->t的所有最短增广路
    //最短指经过最少的节点
    
    //dfs 从s开始递归搜索残量网络,且递归路线是层级逐渐加深的,这样就能保证走的是最短路
    //并且在递归过程中维护通路上的最小残量
    LL dfs(int now, LL minflow) {
        if (now == t) return minflow;
        //minflow维护以now为终点的最小残量
        LL nowflow = 0;
        for (int i = cnt[now]; i && minflow; i = e[i].next) {
            cnt[now] = i;
            //当前弧优化
            if (d[e[i].to] == d[now] + 1 && e[i].cap > e[i].flow) {
                LL k = dfs(e[i].to, min(e[i].cap - e[i].flow, minflow));
                if (!k) d[e[i].to] = INF;
                //剪枝
                e[i].flow += k;
                e[i ^ 1].flow -= k;
                nowflow += k;
                minflow -= k;
                //因为通路上的所有路径的残量都少了k
                //所以以now为终点的所有路径的最小残量也要减k
            }
        }
        return nowflow;
    }
    
    void dinic(){
        while (bfs()) {
            Maxflow += dfs(s, INF);
        }
    }
    
    int main() {
        cin >> n >> m >> s >> t;
        f(i, 1, m) {
            int u, v, w;
            cin >> u >> v >> w;
            addedge(u, v, w);
        }
        dinic();
        cout << Maxflow;
        return 0;
    }
    

    注意:通常(cap)(flow)这两个属性可以简化为一个属性(val),意为边(i)的残量
    这样做的话,在增广处理时,(正向边.val-流量)(反向边.val+流量)

  • 相关阅读:
    341. Flatten Nested List Iterator
    667. Beautiful Arrangement II
    953. Verifying an Alien Dictionary
    1704. Determine if String Halves Are Alike
    MySQL数据库的数据类型详解【转】
    Dart之数组(List)的相关方法总结【转】
    Flutter解决点击非输入框时关闭键盘问题及TextFormField焦点切换问题【转】
    Flutter Switch开关【转】
    Image Picker
    Flutter按钮添加背景图片及文字【转】
  • 原文地址:https://www.cnblogs.com/streamazure/p/13733227.html
Copyright © 2011-2022 走看看