最大流

2022-03-01 15:58:27 浏览数 (1)

1. 简介

最大流算法主要分为两大类,一类为增广路算法,一类为预流推进算法。最大流算法解决的是在有向网路图 G=(V,E) 中计算从源点 s 到汇点 t的最大流量问题,以及最小割容量问题。

  • 最小割最大流定理

s−t 最大流的值等于 s−t的最小割容量。

2. 增广路算法

  • 剩余容量

剩余容量 cf(u,v)=c(u,v)−f(u,v) 表示边 (u,v) 的容量与流量之差。

  • 增广路

对于一个网络图 G=(V,E),从图 G 中找到一条从源节点 s 到目标节点 t 的路径,且路径上所有边的剩余容量都大于 0 ,则这条路径称为增广路

  • 残量网络

对于网络图 G,残量网络定义为网络 G 中所有节点和剩余容量大于 0 的边构成的子图,即

begin{array}{c} G_f = (V_f = V, E_f = {(u,v) in E, c_f(u,v) gt 0}) end{array}

2.1 EK 算法

  • BFS 寻找增广路,一次 BFS 一次增广
  • 每一条有向边都需要构造反向边,因为当前增广路可能不是最优的,后续增广可能需要修改流量流向。

EK 算法复杂度为O(nm^2)(nnn 为节点数,mmm 为边数)。

代码语言:javascript复制
#include <bits/stdc  .h>
using namespace std;

#ifndef _EK_
#define _EK_
#define ll long long 
#define MAXN 505
#define MAXM 240005
#define INF 1e12

// EK 算法
struct EK {
    ll cnt_edge;    // 边数
    ll head[MAXN];  // 节点的第一条边的编号
    ll flag[MAXN];  // 节点标记数组
    // 链式前向星
    struct edge {
        ll to, next, flow; // 分别代表:边的终点、同起点的下一条边的编号、边的流量
        edge() {}
        edge(ll _to, ll _next, ll _flow): to(_to), next(_next), flow(_flow) {}
    }e[MAXM];       // 边数组 
    // 增广路径
    struct path {
        ll from, index; // 分别代表:边的起点,边的编号
    }p[MAXN];
    EK() {}
    // 初始化前向星结构
    void init(ll n) {
        cnt_edge = 0;
        memset(head, -1, sizeof(ll)*(n 1));
    }
    // 添加边(正反向两条边)
    void addEdge(ll u, ll v, ll w) {
        e[cnt_edge].to = v;
        e[cnt_edge].next = head[u];
        e[cnt_edge].flow = w;
        head[u] = cnt_edge  ;
        e[cnt_edge].to = u;
        e[cnt_edge].next = head[v];
        e[cnt_edge].flow = 0;
        head[v] = cnt_edge  ;
    }
    // BFS 寻找增广路
    bool bfs(ll s, ll t, ll n) {
        memset(flag, 0, sizeof(ll)*(n 1));
        memset(p, -1, sizeof(ll)*(n 1));
        queue <ll> bfsq;
        bfsq.push(s);
        while(!bfsq.empty()) {
            ll u = bfsq.front();
            bfsq.pop();
            for(ll i = head[u]; i != -1; i = e[i].next) {
                ll v = e[i].to, w = e[i].flow;
                if(!flag[v] && w) {
                    flag[v] = 1;
                    p[v].from = u;
                    p[v].index = i;
                    if(v == t) {
                        return true;
                    }
                    bfsq.push(v);
                }
            }
        }
        // 没有找到增广路
        return false;
    }
    // 求最大流
    ll maxFlow(ll s, ll t, ll n) {
        ll ans = 0;
        while(bfs(s,t,n)) {
            ll maxflow = INF;
            for(ll i = t; i != s; i = p[i].from) {
                maxflow = min(maxflow, e[p[i].index].flow);
            }
            for(ll i = t; i != s; i = p[i].from) {
                e[p[i].index].flow -= maxflow;
                e[p[i].index ^ 1].flow  = maxflow;
            }
            ans  = maxflow;
        }
        return ans;
    }
};
#endif

2.2 Dinic 算法

可以看出,EK 算法存在以下明显不足:

  • 一次 BFS 只增广一次,如果残量网络中还存在增广路则被浪费了
  • 对于已经没有剩余容量的边,EK 算法仍然进行增广判断从而导致时间上的浪费
层次深度

当前节点到源点的最短路径长度(边权值视为 1 )。

Dinic 算法则巧妙解决了 EK 算法的不足之处:

  • 一次 BFS 后使用 DFS 进行多次增广
  • DFS 多次增广过程中,对于已经没有容量的边,采用弧优化的方法巧妙避免重复增广判断

Dinic 算法的复杂度为 O(n^2m)(nnn 为节点数,mmm 为边数)。在稀疏图上,Dinic 算法和 EK 算法相差不大,但在稠密图上(二分匹配之类的)Dinic的优势很明显。

代码语言:javascript复制
#include <bits/stdc  .h>
using namespace std;

#ifndef _DINIC_
#define _DINIC_
#define ll long long 
#define MAXN 505
#define MAXM 250005
#define INF 1e12

// Dinic 算法
struct Dinic {
    ll cnt_edge;    // 边数
    ll head[MAXN];  // 节点的第一条边的编号
    ll shead[MAXN]; // shead 用来临时保存 head 的副本
    ll level[MAXN]; // 节点所在层次
    // 链式前向星
    struct edge {
        ll to, next, flow; // 分别代表:边的终点、同起点的下一条边的编号、边的流量
        edge() {}
        edge(ll _to, ll _next, ll _flow): to(_to), next(_next), flow(_flow) {}
    }e[MAXM];       // 边数组 
    Dinic() {}
    // 初始化前向星结构
    void init(ll n) {
        cnt_edge = 0;
        memset(head, -1, sizeof(ll)*(n 1));
    }
    // 添加边(正反向两条边)
    void addEdge(ll u, ll v, ll w) {
        e[cnt_edge].to = v;
        e[cnt_edge].next = head[u];
        e[cnt_edge].flow = w;
        head[u] = cnt_edge  ;
        e[cnt_edge].to = u;
        e[cnt_edge].next = head[v];
        e[cnt_edge].flow = 0;
        head[v] = cnt_edge  ;
    }
    // BFS 寻找增广路,构建层次网络
    bool bfs(ll s, ll t, ll n) {
        memset(level, 0, sizeof(ll)*(n 1));
        queue <ll> bfsq;
        bfsq.push(s);
        level[s] = 1;   // 0 用来表示没有 bfs 到,故源点深度从 1 开始
        while(!bfsq.empty()) {
            ll u = bfsq.front();
            bfsq.pop();
            for(ll i = head[u]; i != -1; i = e[i].next) {
                ll v = e[i].to, w = e[i].flow;
                if(!level[v] && w) {
                    level[v] = level[u]   1;
                    bfsq.push(v);
                }
            }
        }
        if(level[t])    return true;
        return false;
    }
    // DFS 进行路径增广
    ll dfs(ll u, ll curflow, ll t) {
        if(u == t) {
            return curflow;
        }
        for(ll & i = shead[u]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(level[v] == level[u]   1 && w) {
                ll maxflow = dfs(v, min(curflow, w), t);
                if(maxflow) {
                    e[i].flow -= maxflow;
                    e[i^1].flow  = maxflow;
                    return maxflow;
                }
            }
        }
        return 0;
    }
    // 求最大流
    ll maxFlow(ll s, ll t, ll n) {
        ll ans = 0;
        while(bfs(s,t,n)) {
            ll maxflow = INF, curflow;
            memcpy(shead, head, sizeof(head));
            while((curflow = dfs(s, maxflow, t))) {
                ans  = curflow;
            }
        }
        return ans;
    }
};
#endif

2.3 ISAP 算法

SAP 算法引入了断层的优化方法:

  • 距离

当前节点到汇点的最短路径长度(边权值视为 1 )。类似于 Dinic 算法中的层次深度的计算,不过是在反图上从汇点到源点进行 BFS 。

  • 断层

gap[i] 数组用来统计距离为 i 的节点个数。如果增广到某一距离发现节点数为零,需要重新计算所有节点的距离并计算 gap ;如果重新计算后仍然无法增广,则说明源点和汇点之间出现了断层,此时算法结束。

SAP 算法复杂度为O(n^2m)(nnn 为节点数,mmm 为边数)。

  • ISAP 算法即为 SAP 算法的优化版本,在 SAP 算法基础上加上了当前弧优化和分层优化(即 DFS 后不需要重跑 BFS 来进行分层)。

ISAP 算法复杂度为 O(n^2m)(nnn 为节点数,mmm 为边数)。

代码语言:javascript复制
#include <bits/stdc  .h>
using namespace std;

#ifndef _ISAP_
#define _ISAP_
#define ll long long 
#define MAXN 1205
#define MAXM 240005
#define INF 1e12

// ISAP 算法
struct ISAP {
    ll cnt_edge;    // 边数
    ll head[MAXN];  // 节点的第一条边的编号
    ll shead[MAXN]; // shead 用来临时保存 head 的副本
    ll level[MAXN]; // 节点所在层次
    ll gap[MAXN 1];   // 层次节点数
    // 链式前向星
    struct edge {
        ll to, next, flow; // 分别代表:边的终点、同起点的下一条边的编号、边的流量
        edge() {}
        edge(ll _to, ll _next, ll _flow): to(_to), next(_next), flow(_flow) {}
    }e[MAXM];       // 边数组 
    ISAP() {}
    // 初始化前向星结构
    void init(ll n) {
        cnt_edge = 0;
        memset(head, -1, sizeof(ll)*(n 1));
    }
    // 添加边(正反向两条边)
    void addEdge(ll u, ll v, ll w) {
        e[cnt_edge].to = v;
        e[cnt_edge].next = head[u];
        e[cnt_edge].flow = w;
        head[u] = cnt_edge  ;
        e[cnt_edge].to = u;
        e[cnt_edge].next = head[v];
        e[cnt_edge].flow = 0;
        head[v] = cnt_edge  ;
    }
    // BFS 寻找增广路,构建层次网络
    bool bfs(ll s, ll t, ll n) {
        memset(level, 0, sizeof(ll)*(n 1));
        memset(gap, 0, sizeof(ll)*(n 2));   // 最大 level 可达 n 1(见下文)
        queue <ll> bfsq;
        level[t] = 1;   // 0 用来表示没有 bfs 到,故到汇点的距离从 1 开始
        gap[1] = 1;    
        bfsq.push(t); 
        while(!bfsq.empty()) {
            ll u = bfsq.front();
            bfsq.pop();
            for(ll i = head[u]; i != -1; i = e[i].next) {
                // 在反图中构建层次网络,故需反向判断路径容量
                ll v = e[i].to;
                if(!level[v] && e[i^1].flow) {
                    level[v] = level[u]   1;
                      gap[level[v]];
                    bfsq.push(v);
                }
            }
        }
        // 判断 s 到 t 是否有增广路
        if(level[s])    return true;
        return false;
    }
    // 重贴标签(更新层次深度)
    void relabel(ll u, ll n) {
        level[u] = n   1;
        for(ll i = head[u]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(level[v] < level[u] - 1 && w) {
                level[u] = level[v]   1;
            }
        }
    }
    // DFS 进行路径增广
    ll dfs(ll u, ll curflow, ll s, ll t, ll n) {
        if(u == t) {
            return curflow;
        }
        for(ll & i = shead[u]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(level[v] == level[u] - 1 && w) {
                ll maxflow = dfs(v, min(curflow, w), s, t, n);
                if(maxflow) {
                    e[i].flow -= maxflow;
                    e[i^1].flow  = maxflow;
                    return maxflow;
                }
            }
        }
        // 程序执行到此说明该点出去的所有点都已经流过了
        // 此时需要更改层次深度
        // 同样要更新对应点的 shead
        shead[u] = head[u];
        --gap[level[u]];
        if(gap[level[u]] == 0) {
            level[s] = n   1;
        }
        // 提高当前层次深度(因为可能存在路径更长的增广)
        // 方案一:直接更新为出边的最小层次深度 1
        relabel(u, n);
        // 方案二:直接更新为原层次深度 1
        //   level[u];
          gap[level[u]];
        return 0;
    }
    // 求最大流
    ll maxFlow(ll s, ll t, ll n) {
        ll ans = 0;
        if(bfs(s,t,n)) {
            ll maxflow = INF;
            memcpy(shead, head, sizeof(head));
            while(level[s] <= n) {
                ans  = dfs(s, maxflow, s, t, n);
            }
        }
        return ans;
    }
};
#endif

3. 预流推进算法

预流推进算法的主要思想是允许水在非源汇点的节点中暂时存储,然后每个节点都尽可能将自身的超额流推送出去,并且保证在算法结束后所有非源汇点的超额流都为0,那这种方案就是合法的。

  • 超额流

存储在非源汇点中的流称作这个点的超额流。

3.1 HLPP 算法

HLPP 算法的主要思想如下:

  • 对于非源汇点的层次深度,每次选择层次深度最大的节点进行推流
  • 如果节点推流后还有余流,则对该节点重贴标签后抬高其高度,回到上一步骤
  • 直到所有非汇源点的余流都等于零,程序结束
GAP 优化
  • 当出现断层后,直接将断层以上深度的所有节点的层次深度设为 n 1,使得这些再也无法推流到汇点的节点的余流尽快推送回源点,从而减少重贴标签的操作。

HLPP 算法复杂度为 O(n^2 sqrt{m})(nnn 为节点数,mmm 为边数)。

代码语言:javascript复制
#include <bits/stdc  .h>
using namespace std;

#ifndef _HLPP_
#define _HLPP_
#define ll long long 
#define MAXN 1205
#define MAXM 240005
#define INF 1e12

// HLPP 算法
struct HLPP {
    ll cnt_edge;    // 边数
    ll head[MAXN];  // 节点的第一条边的编号
    ll rflow[MAXN]; // 每个点对应的余流(所有节点的余流初始化为0)
    ll level[MAXN]; // 节点所在层次
    ll flag[MAXN];  // 标记是否已经在层次深度链表数组中
    ll gap[MAXN<<1];        // 层次节点数,重贴标签后层次深度可能达到 2n
    // 层次深度结构体
    struct depth {
        ll node;    // 节点编号
        ll level;   // 节点层次深度
        depth() {}
        depth(ll _node, ll _level): node(_node), level(_level) {}
        bool operator < (const depth d) const {
            return level < d.level;
        }
    };
    // 层次深度优先队列
    priority_queue <depth> pq;
    // 链式前向星
    struct edge {
        ll to, next, flow; // 分别代表:边的终点、同起点的下一条边的编号、边的流量
        edge() {}
        edge(ll _to, ll _next, ll _flow): to(_to), next(_next), flow(_flow) {}
    }e[MAXM];       // 边数组 
    HLPP() {}
    // 初始化前向星结构
    void init(ll n) {
        cnt_edge = 0;
        memset(head, -1, sizeof(ll)*(n 1));
    }
    // 添加边(正反向两条边)
    void addEdge(ll u, ll v, ll w) {
        e[cnt_edge].to = v;
        e[cnt_edge].next = head[u];
        e[cnt_edge].flow = w;
        head[u] = cnt_edge  ;
        e[cnt_edge].to = u;
        e[cnt_edge].next = head[v];
        e[cnt_edge].flow = 0;
        head[v] = cnt_edge  ;
    }
    // BFS 寻找增广路,构建层次网络(反向构建)
    bool bfs(ll s, ll t, ll n) {
        memset(level, 0, sizeof(ll)*(n 1));
        queue <ll> bfsq;
        level[t] = 1;   // 0 用来表示没有 bfs 到,故到汇点的距离从 1 开始
        bfsq.push(t); 
        while(!bfsq.empty()) {
            ll u = bfsq.front();
            bfsq.pop();
            for(ll i = head[u]; i != -1; i = e[i].next) {
                // 在反图中构建层次网络,故需反向判断路径容量
                ll v = e[i].to;
                if(!level[v] && e[i^1].flow) {
                    level[v] = level[u]   1;
                    bfsq.push(v);
                }
            }
        }
        // 判断 s 到 t 是否有增广路
        if(level[s])    return true;
        return false;
    }
    // 预流推进
    void preflowPush(ll u, ll s, ll t) {
        for(ll i = head[u]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(level[v] == level[u] - 1 && w) {
                ll maxflow = min(rflow[u], e[i].flow);
                e[i].flow -= maxflow;
                e[i^1].flow  = maxflow;
                rflow[u] -= maxflow;
                rflow[v]  = maxflow;
                if(v != s && v != t && !flag[v]) {
                    pq.push(depth{v, level[v]});
                    flag[v] = 1;
                }
                if(rflow[u] == 0) {
                    // 全部流量推送完毕
                    break;
                }
            }
        }
    }
    // 重贴标签(更新层次深度)
    void relabel(ll u, ll n) {
        level[u] = 2*n;
        for(ll i = head[u]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(level[v] < level[u] - 1 && w) {
                level[u] = level[v]   1;
            }
        }
    }
    // 求最大流
    ll maxFlow(ll s, ll t, ll n) {
        if(!bfs(s,t,n)) {
            return 0;
        }
        // 防止s的临近节点过早的推流回s节点
        level[s] = n   1;
        // 初始化 gap 数组
        memset(gap, 0, sizeof(ll)*((n 1)<<1));
        for(ll i = 1; i <= n;   i) {
            if(level[i]) {
                  gap[level[i]];
            }
        }
        // 设定源点 s 的出边节点初始余流
        for(ll i = head[s]; i != -1; i = e[i].next) {
            ll v = e[i].to, w = e[i].flow;
            if(w) {
                e[i].flow -= w;
                e[i^1].flow  = w;
                rflow[s] -= w;
                rflow[v]  = w;
                if(v != s && v != t && !flag[v]) {
                    pq.push(depth{v, level[v]});
                    flag[v] = 1;
                }
            }
        }
        // 开始从最高高度节点预推流
        while(!pq.empty()) {
            ll u = pq.top().node;
            pq.pop();
            flag[u] = 0;
            preflowPush(u, s, t);
            if(rflow[u]) {
                // 如果当前节点还有余流
                --gap[level[u]];
                if(!gap[level[u]]) {
                    // 断层
                    for(ll v = 1; v <= n;   v) {
                        if(v != s && v != t && level[v] > level[u] && level[v] < n   1) {
                            level[v] = n   1;
                        }
                    }
                }
                relabel(u, n); // 重贴标签,提升高度
                  gap[level[u]];
                pq.push(depth{u, level[u]});
                flag[u] = 1;
            }
        }
        return rflow[t];
    }
};
#endif

0 人点赞