网络流学习笔记-最大流问题的四种算法

最大流问题

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

  • 容量限制:\(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+流量\)

posted @ 2020-09-26 00:11  StreamAzure  阅读(986)  评论(0编辑  收藏  举报