最大流 / 最小割 / 费用流

最大流 / 最小割 / 费用流

一些定义

流网络:G=(V,E) 一个连通图满足 |E||V|1 ,其中有源点 S 汇点 T

每一条边 (u,v) 有一个非负容量 c(u,v)0

流:边 (u,v) 的流是一个函数 f(u,v)u,vV ,满足

  • 容量限制: f(u,v)c(u,v)

  • 斜对称性: f(u,v)=f(v,u)

  • 流守恒性:若 u{S,T} ,要求 vf(u,v)=wf(w,u)

    流进 u 的总流量=离开 u 的总流量

网络的流:流 f 定义为 f=vVf(S,v)

  • 即从源点出发的总流表示网络的流。

    在最大流问题中,求 ST 的最大值流

FF 算法

边的残留容量: r(u,v)=c(u,v)f(u,v)

残留网络:流 f 的残留网络 Gf=(V,Ef)

其中 Ef={(u,v)u,vVr(u,v)>0}

  • 0<f(u,v)<c(u,v)(u,v) 在残留网络中,且

    r(v,u)=c(v,u)f(v,u)>0 ,所以 (v,u) 也在残留网络中

增广路径:增广路径 P 是残留网络中 ST 的一条简单路径

增广路径的残留容量: δ(P)=min{r(u,v)(u,v)P}

沿着路径增广 :沿着路径的每一条边发送 δ(P) 的流。使得整个网络的流量增加。

因此,最大流问题,转化为若干次增广得到的流的和。

增广时,根据修改流的值与残留容量。

由于斜对称性,要有退流操作,即当正向边增加,需要对反向边减少同样大小的流。

  • f=f+δ(P)
  • (u,v)Pr(u,v)r(u,v)δ(P)r(v,u)r(v,u)+δ(P)

为什么要有反向边?——给程序一个反悔的机会

退流操作带来的「抵消」效果使得我们无需担心我们按照「错误」的顺序选择了增广路。

上界是 O(|E||f|) ,这是最最坏的复杂度。

单次增广 O(|E|) ,增广会使流量增加,增广轮数不超过 |f|

常规的方法都是基于 FF 找增广路的思路。

根据不同的实现方式。有 ekdinic ,和 sap

正确性

需要最大流最小割定理。。

EK

找增广路最自然的方法: bfs 。

类似最短路的思路在残留网络 Gf 上找增广路。

δ(P) 为路径上 r(u,v) 的最小值。

沿着路径增广,得到 Gf ,继续在上面操作,直到找不到增广路。

int EK()
{
	f = 0;
	创建残留网络 G(f);
	while (通过 bfs 能找到 G(f) 中存在从 s 到 t 的有向路径) 
	{
    		令 P 是在G(f)中从 s 到 t 的一条路径
    		delta = delta(P)
    		沿着 P 发送 delta 单位的流
    		更新 P 上的边的残留容量
    		f = f + delta;
	}
	return f;	//f是最大流
}

单轮 BFS 增广的时间复杂度是 O(|E|)

增广总轮数的上界是 O(|V||E|) ,这个在 最大流 - OI Wiki 上有严格的分析。

总: O(nm2)

dinic

先对 Gfbfs 分层,根据点 u 到源点 S 的距离 d(u) 把图分成若干层。

对于在 Gf=(v,Ef) 得到分层图 GL=(V,EL)

其中 EL={(u,v)(u,v)Ef,dv=du+1}

  1. 在残量网络上 BFS ,构造分层图。

  2. 在分层图上 DFS 找增广路,在回溯时实时更新剩余容量。

分层的作用:给定一个固定的搜索顺序,防止在环中反复流动,减少不必要边的搜索。

这个算法有一个最典型的优化:当前弧优化。

当边 (u,v) 已经增广到极限(边 (u,v) 已无剩余容量或 u 的后侧已不能继续增广)

u 没必要再向边 (u,v) 增广了。

所以,对于每个结点我们维护器第一条还有用的出边。这就是当前弧优化。

多路增广

我们找到了 ST 的一条增广路 P ,没必要重新从 S 开始找。

可能在 P 上某一点的岔路也能继续增广。

回溯是不必直接 return ,每个点可以流向多条出边,这是基于 dfs 一个很自然的实现。

多路增广只是常数优化,而当前弧优化是保证复杂度的一部分。

一次增广 O(nm) ,最多 O(n) 次。

时间复杂度: O(n2m)

如果直接按照 O(n2m) 估计复杂度是不科学的。

往往对于 n105 的题可能也有网络流做法。

可以相信大力出奇迹,更多是要在做题中学会估计复杂度。

ISAP

dinic 还有弊端:每次 dfs 后都要重新 bfs 分层。

第一步同样分层,但略有不同,我们选择在反图上,从 t 点向 s 点进行 bfs

之后按照层次 dfs 并增广。

不同的是,不用重跑 bfs 来对图上的点重新分层,而是在增广中就完成重分层

当我们发现点 u 的流量有剩余,修改 du=minvdv+1

特别地,若残量网络上 u 无出边,则 du=n

dSn 时,图上不存在增广路,可终止算法。

  • gap 优化:记录 gapi 表示深度为 i 的点的数量。

    在更新 du 是顺便更新 gap 。若出现 gapi=0 则图出现断层。

    无法再找到增广路,直接终止算法,实现时直接将 dS 标为 n

理论上初始距离标号要用先预处理求得,实践中可以全部设为 0

可以证明:这样做不改变时间复杂度,相当于用一次增广来给 dep 赋值。

推荐 isap :实现很短,速度很快。

  • 补充:正常情况下 isap 是可以用当前弧优化的。

  • 但在这一种实现下,需要枚举所有的出边寻找 mindepv

    这就变得繁琐了。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200 + 5, M = 5005;
int n, m;
struct Net {
    int St, Ed;
    int lst[N], Ecnt;
    struct Edge { int to, nxt; LL qz; } e[M << 1];
    Net() {
        memset(lst, 0, sizeof(lst));
        Ecnt = 1;
    }
    void Ae(int fr, int go, LL vl) {
        e[++Ecnt] = (Edge){ go, lst[fr], vl }, lst[fr] = Ecnt;
    }
    void lk(int u, int v, LL w) {
        Ae(u, v, w), Ae(v, u, 0);
    }
    int dep[N], gap[N];
    LL dfs(int u, LL low) {
        if (u == Ed) return low;
        LL use = 0, rl;
        int mn = n - 1;
        for (int i = lst[u], v; i; i = e[i].nxt) {
            if (!e[i].qz) continue;
            if (dep[u] == dep[v = e[i].to] + 1) {
                rl = dfs(v, min(e[i].qz, low - use));
                e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
                if (low == use) return use;
            }
            mn = min(mn, dep[v]);
        }
        // use < low, or it won't come here.
        --gap[dep[u]];
        if (!gap[dep[u]]) dep[St] = n;
        ++gap[dep[u] = mn + 1];
        return use;
    }
    LL mxfl() {
        LL res = 0;
        gap[0] = n;
        while (dep[St] < n) res += dfs(St, 1e10);
        return res;
    }
} nt;
int main() {
    scanf("%d%d%d%d", &n, &m, &nt.St, &nt.Ed);
    for (int i = 1, u, v, w; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        nt.lk(u, v, 1ll * w);
    }
    printf("%lld", nt.mxfl());
}

放一个有弧优化的版本。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 200 + 5, M = 5005;
int n, m;
struct Net {
    int St, Ed;
    int lst[N], cur[N], Ecnt;
    struct Edge { int to, nxt; LL qz; } e[M << 1];
    Net() {
        memset(lst, 0, sizeof(lst));
        Ecnt = 1;
    }
    void Ae(int fr, int go, LL vl) {
        e[++Ecnt] = (Edge){ go, lst[fr], vl }, lst[fr] = Ecnt;
    }
    void lk(int u, int v, LL w) {
        Ae(u, v, w), Ae(v, u, 0);
    }
    int dep[N], gap[N];
    LL dfs(int u, LL low) {
        if (u == Ed) return low;
        LL use = 0, rl;
        int mn = n - 1;
        for (int &i = cur[u], v; i; i = e[i].nxt)
            if (e[i].qz && dep[u] == dep[v = e[i].to] + 1) {
                rl = dfs(v, min(e[i].qz, low - use));
                e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
                if (low == use) return use;
            }
        for (int i = lst[u]; i; i = e[i].nxt)
            if (e[i].qz) mn = min(mn, dep[e[i].to]);
        if (!(--gap[dep[u]])) dep[St] = n;
        ++gap[dep[u] = mn + 1];
        return use;
    }
    LL mxfl() {
        LL res = 0;
        gap[0] = n;
        while (dep[St] < n) {
            memcpy(cur, lst, sizeof(cur));
            res += dfs(St, 1e10);
        }
        return res;
    }
} nt;
int main() {
    scanf("%d%d%d%d", &n, &m, &nt.St, &nt.Ed);
    for (int i = 1, u, v, w; i <= m; i++) {
        scanf("%d%d%d", &u, &v, &w);
        nt.lk(u, v, 1ll * w);
    }
    printf("%lld", nt.mxfl());
}

最小割

一些定义

割:把流网络划分成两个部分 S,T ,满足源点 sS ,汇点 tT

割的容量: c(S,T)=uSvTc(u,v) ,可以用 c(s,t) 代表 C(S,T)

即所有从 ST 的边的容量之和。

最大流最小割定理

f(s,t)max=c(s,t)min

对于 f(s,t) 的一个割 c(s,t)

f(s,t)=uSvTf(u,v)uSvTf(v,u)uSvTf(u,v)=c(s,t)

既然 f(s,t) 是最大流,残量网络中不存在从 ST 的增广路,

S 的出边一定满流。 S 的入边一定是零流。即 uSvTf(v,u)=0

f(s,t)=uSvTf(u,v)=c(s,t)

其实有三个可以互相推导的结论。

  1. 存在一个割满足 f(s,t)=c(s,t)
  2. f 是最大流
  3. 残量网络上没有增广路径

最小割的方案

求出最小割,将没有满流的边流量设置为

满流的边流量设置为 1

再跑一遍最小割。


之后再学最小割模型的应用。

费用流

定义

引入单位费用 w(u,v) 满足斜对称性: w(u,v)=w(v,u)

当边 (u,v) 的流量为 f(u,v) 需要花费 f(u,v)×w(u,v)

花费最小的最大流是最小费用最大流。

SSP 算法

名字好像有点陌生,实际上就是一个贪心的思路:沿着单位费用最小的路径增广。

不能直接处理有负环的图。

正确性:归纳法?设图上没有负环,流量为 i 的最小费用为 fi ,可得 f0=0

fi 得到的残量网络上找到最短路增广,计算得到 fi+1 ,则 fi+1fi 是最短路长度

假设存在 fi+1<fi+1 ,显然是需要经过负环增广。

那既然有负环,我们向负环中增加流量,可不增加 s 流出流量让 fi 更小,与假设矛盾

所以正确性有保证

复杂度上界是 O(nm|f|) ,基于最大流的算法改进实现。

改进 EK

直接找最短路上的增广路径即可。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N], vis[N], pre[N];
LL dis[N], mxfl, Cost, low[N];
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
    e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
inline bool spfa() {
    for (int i = 1; i <= n; i++) dis[i] = Inf, inq[i] = 0, pre[i] = 0;
    dis[St] = 0, low[St] = Inf, Q.push(St);
    for (int u; !Q.empty(); ) {
        u = Q.front(), Q.pop(), inq[u] = 0;
        for (int i = lst[u], v; i; i = e[i].nxt) {
            if (!e[i].qz) continue;
            v = e[i].to;
            if (dis[u] + e[i].cs < dis[v]) {
                dis[v] = dis[u] + e[i].cs, pre[v] = i;
                low[v] = min(low[u], e[i].qz);
                if (!inq[v]) Q.push(v), inq[v] = 1;
            }
        }
    }
    return dis[Ed] ^ Inf;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &St, &Ed);
    for (int i = 1, u, v, w, q; i <= m; i++) {
        scanf("%d%d%d%d", &u, &v, &w, &q);
        Ae(u, v, w, q);
        Ae(v, u, 0,-q);
    }
    while (spfa()) {
        LL rl = low[Ed];
        for (int u = Ed; u ^ St; u = e[pre[u] ^ 1].to)
            e[pre[u]].qz -= rl, e[pre[u] ^ 1].qz += rl;
        mxfl += rl, Cost += rl * dis[Ed];
    }
    printf("%lld %lld", mxfl, Cost);
}

改进 dinic

把原本按照深度分成改为按照最短路分层。

其实 zkw 也提出了这一种做法。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N], vis[N];
LL dis[N], mxfl, Cost;
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
    e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
inline bool spfa() {
    for (int i = 1; i <= n; i++) dis[i] = Inf, inq[i] = 0;
    dis[St] = 0, Q.push(St);
    for (int u; !Q.empty(); ) {
        u = Q.front(), Q.pop(), inq[u] = 0;
        for (int i = lst[u], v; i; i = e[i].nxt) {
            if (!e[i].qz) continue;
            v = e[i].to;
            if (dis[u] + e[i].cs < dis[v]) {
                dis[v] = dis[u] + e[i].cs;
                if (!inq[v]) Q.push(v), inq[v] = 1;
            }
        }
    }
    return dis[Ed] ^ Inf;
}
LL dfs(int u, LL low) {
    if (u == Ed) return Cost += dis[Ed] * low, low;
    register LL use = 0, rl;
    for (int i = lst[u], v; i; i = e[i].nxt)
        if (!vis[v = e[i].to] && e[i].qz)
            if (dis[u] + e[i].cs == dis[v]) {
                vis[v] = 1, rl = dfs(v, min(e[i].qz, low - use)), vis[v] = 0;
                e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
                if (use == low) return use;
            }
    return use;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &St, &Ed);
    for (int i = 1, u, v, w, q; i <= m; i++) {
        scanf("%d%d%d%d", &u, &v, &w, &q);
        Ae(u, v, w, q);
        Ae(v, u, 0,-q);
    }
    while (spfa()) {
        for (int i = 1; i <= n; i++) vis[i] = 0;
        vis[St] = 1, mxfl += dfs(St, Inf);
    }
    printf("%lld %lld", mxfl, Cost);
}

zkw?费用流

问号:应该叫什么。

这是 zkw 提供的一种实现方式,有别于普通的重新跑最短路。

这种方法采用二分图 KM 的重标号思想,如果不知道 KM 是什么也没有关系。

我们考虑跑完最短路后会发生什么:

  1. 所有点 u 满足 dudv+w(v,u)
  2. 对于每一个 u 存在一点 v 使得 du=dv+w(v,u)

而增广之后会破坏什么?

不会是 1 ,只有可能是满流后导致 2 不满足,使得不能找到最短路上的增广路。

找增广后,当前流对应割的边集 {(u,v)uS,vT} ,表示到 u 增广失败了。

找到 d=minvdv+w(v,u)du

所有访问过的点距离标号增加 d 。这样不会破坏性质 1,

而且至少有一条新的边进入了 du=dv+w(v,u) 的子图


使用范围:不可直接处理有负权的边。

效率问题:在一些图上很快,在一些图上很慢。

  • 优点:减少多次访问节点与队列维护。多路增广。

  • 缺点:最差情况下,真的一次修改只能让 1 条边进入最短路径。

    反复尝试增广而次次不能增广, 陷入弄巧成拙的境地.

适用于:最终流量较大,而费用取值范围不大的图

慎用与:流量不大,费用不小,增广路还较长,就不适合

#include <bits/stdc++.h>

using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const int N = 5005, M = 50005;
const int INF = 0x3f3f3f3f;
struct Edge { int to, nxt, qz, cs; }e[M << 1];
int n, m, S, T, cnt = 1, lst[N], dis[N], Cost, tot;
int vis[N];
inline void Ae(int fr, int go, int vl, int ad) {
    e[++cnt] = (Edge){go, lst[fr], vl, ad}, lst[fr] = cnt;
}
inline bool relabel() {
    int mn = INF;
    for (int u = 1; u <= n; u++) if (vis[u])
        for (int i = lst[u], v; i; i = e[i].nxt)
            if (!vis[v = e[i].to] && e[i].qz)
                mn = min(mn, dis[v] + e[i].cs - dis[u]);
    if (mn == INF) return 0;
    for (int u = 1; u <= n; u++) if (vis[u]) dis[u] += mn;
    return 1;
}
int dfs(int u, int nw) {
    if (u == T) return Cost += dis[S] * nw, tot += nw, nw;
    register int use = 0, rl;
    vis[u] = 1;
    for (int i = lst[u], v; i; i = e[i].nxt)
        if (!vis[v = e[i].to] && e[i].qz)
            if (dis[u] == dis[v] + e[i].cs) {
                rl = dfs(v, min(e[i].qz, nw - use));
                e[i].qz -= rl, e[i ^ 1].qz += rl, use += rl;
                if (use == nw) return use;
            }
    return use;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &S, &T);
    for (int i = 1, u, v, w, q; i <= m; i++) {
        scanf("%d%d%d%d", &u, &v, &w, &q);
        Ae(u, v, w, q), Ae(v, u, 0, -q);
    }
    do {
        do {
            memset(vis, 0, sizeof(vis));
        } while(dfs(S, INF));
    } while(relabel());
    printf("%d %d", tot, Cost);
}

使用 dijkstra

又叫“Primal-Dual 原始对偶算法”??

鉴于 spfa 在某方面的缺点,想用 dijkstra

势:给每一个节点赋予一个新的标号 hu ,叫做“势”只是因为与物理中的势有相似的性质。

在此基础上把边 (u,v) 的长度修改为 w(u,v)=w(u,v)+huhv

证明其可行性,需要三点。

  1. w 上跑最短路和在 w 上跑是等价的。

    易得固定对于路径 p1,p2,,pm

    长度为 i=2mw(pi1,pi)+(hpi1hpi) ,拆开之后能消掉 h ,最后剩下 hp1hpm

    所以长度等于 (hp1hpm)+i=2mw(pi1,pi)

    对于固定的起点、终点 p1=S,pm=T(hp1hpm) 为定值,最短路也自然等价。

  2. 边权 w 保持非负。势的初始化?

    w(u,v)=w(u,v)+huhv0 ,整理, w(u,v)+huhv

    即势能要满足三角不等式。所以先跑一遍 spfahu 初始化为最初的最短路即可。

  3. 如何修改势,正确性如何保证?

    修改的结论,假设增广后源点 Si 的距离是 di (修改边权后的距离)

    只需给 hi 加上 di 即可。

    若能证明修改后边权保持非负,就是正确的。

    • 原有的边。满足

      du+w(u,v)dvdu+(w(u,v)+huhv)dv(du+hu)+w(u,v)(dv+hv)

      所以用 hi+di 作为新的势能对原有的边满足条件。

    • 在一轮增广后,由于一些 (u,v) 边在增广路上。一定会满足 du+w(u,v)=dv

      之后残量网络上会多出一些 (v,u) 边,根据 w(u,v)=w(v,u) 可以得到

      du+w(u,v)=dvdu+(w(u,v)+huhv)=dv(du+hu)=(dv+hv)w(u,v)(dv+hv)+w(v,u)=(du+hu)

      因此新增的边 (v,u) 的边权非负。

  4. 得证,边权全部非负,可以用 dijk

给出一个单路增广的程序。自然可以改成多路增广。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long uLL;
typedef long double LD;
typedef long long LL;
typedef double db;
const LL Inf = 1e10;
const int N = 5005, M = 50005;
int n, m, St, Ed, lst[N], Ecnt = 1, inq[N];
LL dis[N], mxfl, Cost, h[N];
queue<int> Q;
struct Edge { int to, nxt; LL qz, cs; } e[M << 1];
inline void Ae(int fr, int go, int vl, int ad) {
    e[++Ecnt] = (Edge){ go, lst[fr], 1ll * vl, 1ll * ad }, lst[fr] = Ecnt;
}
void spfa() {
    for (int i = 1; i <= n; i++) h[i] = Inf, inq[i] = 0;
    h[St] = 0, Q.push(St);
    for (int u; !Q.empty(); ) {
        u = Q.front(), Q.pop(), inq[u] = 0;
        for (int i = lst[u], v; i; i = e[i].nxt) {
            v = e[i].to;
            if (e[i].qz && h[u] + e[i].cs < h[v]) {
                h[v] = h[u] + e[i].cs;
                if (!inq[v]) Q.push(v), inq[v] = 1;
            }
        }
    }
}
typedef pair<LL, int> pr;
priority_queue<pr> hp;
int pre[N];
LL low[N];
bool dijk() {
    for (int i = 1; i <= n; i++) dis[i] = low[i] = Inf, pre[i] = 0;
    dis[St] = 0, low[St] = Inf, hp.push(make_pair(0, St));
    while (!hp.empty()) {
        int u = hp.top().second;
        LL now = -hp.top().first;
        hp.pop();
        if (now != dis[u]) continue;
        for (int i = lst[u], v; i; i = e[i].nxt) {
            v = e[i].to;
            LL w = e[i].cs + h[u] - h[v];
            if (e[i].qz && dis[u] + w < dis[v]) {
                dis[v] = dis[u] + w, pre[v] = i;
                low[v] = min(low[u], e[i].qz);
                hp.push(make_pair(-dis[v], v));
            }
        }
    }
    return dis[Ed] != Inf;
}
int main() {
    scanf("%d%d%d%d", &n, &m, &St, &Ed);
    for (int i = 1, u, v, w, q; i <= m; i++) {
        scanf("%d%d%d%d", &u, &v, &w, &q);
        Ae(u, v, w, q);
        Ae(v, u, 0,-q);
    }
    spfa();
    while (dijk()) {
        LL rl = low[Ed];
        for (int u = Ed; u ^ St; u = e[pre[u] ^ 1].to)
            e[pre[u]].qz -= rl, e[pre[u] ^ 1].qz += rl;
        mxfl += rl, Cost += rl * (dis[Ed] + h[Ed] - h[St]);
        for (int i = 1; i <= n; i++) h[i] += dis[i];
    }
    printf("%lld %lld", mxfl, Cost);
}

最后

为什么在费用流这部分放出了 ekdinic 的代码?

因为费用流的时间复杂度很玄学,没有固定哪一种跑得快。

比如流量很小,单路增广可能优于多路增广。

对复杂度的估计是需要练习的。

总结

理解好网络流的基本写法,才能有建模解题的底气。

本文作者:小蒟蒻laf

本文链接:https://www.cnblogs.com/KonjakLAF/p/17232881.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   小蒟蒻laf  阅读(130)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起