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

最大流问题

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

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

原文地址:https://www.cnblogs.com/streamazure/p/13733227.html