最大流 / 最小割 / 费用流

最大流 / 最小割 / 费用流

一些定义

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

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

流:边 \((u,v)\) 的流是一个函数 \(f(u,v)\) , \(\forall u,v\in V\) ,满足

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

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

  • 流守恒性:若 \(u\notin\{S,T\}\) ,要求 \(\sum_v f(u,v)=\sum_w f(w,u)\)

    流进 \(u\) 的总流量=离开 \(u\) 的总流量

网络的流:流 \(f\) 定义为 \(f=\sum_{v\in V} f(S,v)\)

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

    在最大流问题中,求 \(S\) 到 \(T\) 的最大值流

FF 算法

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

残留网络:流 \(f\) 的残留网络 \(G_f=(V,E_f)\) ,

其中 \(E_f=\{(u,v)\mid u,v\in V\and r(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\) 是残留网络中 \(S\) 到 \(T\) 的一条简单路径

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

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

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

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

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

  • \(f=f+\delta(P)\)
  • \(\forall(u,v)\in P\) ,\(r(u,v)\leftarrow r(u,v)-\delta(P)\) ,\(r(v,u)\leftarrow r(v,u)+\delta(P)\)

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

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

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

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

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

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

正确性

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

EK

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

类似最短路的思路在残留网络 \(G_f\) 上找增广路。

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

沿着路径增广,得到 \(G_f'\) ,继续在上面操作,直到找不到增广路。

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(nm^2)\)

dinic

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

对于在 \(G_f=(v,E_f)\) 得到分层图 \(G_L=(V,E_L)\) ,

其中 \(E_L=\{(u,v)\mid(u,v)\in E_f,d_v=d_u+1\}\)

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

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

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

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

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

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

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

多路增广

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

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

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

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

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

时间复杂度: \(O(n^2 m)\) 。

如果直接按照 \(O(n^2 m)\) 估计复杂度是不科学的。

往往对于 \(n\le 10^5\) 的题可能也有网络流做法。

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

ISAP

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

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

之后按照层次 dfs 并增广。

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

当我们发现点 \(u\) 的流量有剩余,修改 \(d_u=\min_v d_v+1\) 。

特别地,若残量网络上 \(u\) 无出边,则 \(d_u=n\)

当 \(d_S \geq n\) 时,图上不存在增广路,可终止算法。

  • \(gap\) 优化:记录 \(gap_i\) 表示深度为 \(i\) 的点的数量。

    在更新 \(d_u\) 是顺便更新 \(gap\) 。若出现 \(gap_i=0\) 则图出现断层。

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

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

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

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

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

  • 但在这一种实现下,需要枚举所有的出边寻找 \(\min dep_v\) 。

    这就变得繁琐了。

#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\) ,满足源点 \(s\in S\) ,汇点 \(t\in T\) 。

割的容量: \(c(S,T)=\sum_{u\in S}\sum_{v\in T}c(u,v)\) ,可以用 \(c(s,t)\) 代表 \(C(S,T)\) 。

即所有从 \(S\) 到 \(T\) 的边的容量之和。

最大流最小割定理

\(f(s,t)_{max}=c(s,t)_{min}\)

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

有

\[f(s,t)=\sum_{u\in S}\sum_{v\in T}f(u,v)-\sum_{u\in S}\sum_{v\in T}f(v,u)\le\sum_{u\in S}\sum_{v\in T}f(u,v)=c(s,t) \]

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

\(S\) 的出边一定满流。 \(S\) 的入边一定是零流。即 \(\sum_{u\in S}\sum_{v\in T}f(v,u) = 0\)

\[f(s,t)=\sum_{u\in S}\sum_{v\in T}f(u,v)=c(s,t) \]

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

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

最小割的方案

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

满流的边流量设置为 1

再跑一遍最小割。


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

费用流

定义

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

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

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

SSP 算法

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

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

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

在 \(f_i\) 得到的残量网络上找到最短路增广,计算得到 \(f_{i+1}\) ,则 \(f_{i+1}-f_{i}\) 是最短路长度

假设存在 \(f_{i+1}'<f_{i+1}\) ,显然是需要经过负环增广。

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

所以正确性有保证

复杂度上界是 \(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\) 满足 \(d_u\le d_v+w(v,u)\)
  2. 对于每一个 \(u\) 存在一点 \(v\) 使得 \(d_u=d_v+w(v,u)\)

而增广之后会破坏什么?

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

找增广后,当前流对应割的边集 \(\{(u,v)\mid u\in S, v\in T\}\) ,表示到 \(u\) 增广失败了。

找到 \(d=\min_v d_v+w(v,u)-d_u\)

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

而且至少有一条新的边进入了 \(d_u=d_v+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 。

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

在此基础上把边 \((u,v)\) 的长度修改为 \(w'(u,v)=w(u,v)+h_u-h_v\)

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

  1. 在 \(w'\) 上跑最短路和在 \(w\) 上跑是等价的。

    易得固定对于路径 \(p_1,p_2,\cdots,p_m\)

    长度为 \(\sum_{i=2}^m w(p_{i-1},p_i)+(h_{p_{i-1}}-h_{p_i})\) ,拆开之后能消掉 \(h\) ,最后剩下 \(h_{p_1}-h_{p_m}\)

    所以长度等于 \((h_{p_1}-h_{p_m})+\sum_{i=2}^m w(p_{i-1},p_i)\)

    对于固定的起点、终点 \(p_1=S,p_m=T\) ,\((h_{p_1}-h_{p_m})\) 为定值,最短路也自然等价。

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

    \(w'(u,v)=w(u,v)+h_u-h_v\ge 0\) ,整理, \(w(u,v)+h_u\ge h_v\)

    即势能要满足三角不等式。所以先跑一遍 spfa , \(h_u\) 初始化为最初的最短路即可。

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

    修改的结论,假设增广后源点 \(S\) 到 \(i\) 的距离是 \(d_i\) (修改边权后的距离)

    只需给 \(h_i\) 加上 \(d_i\) 即可。

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

    • 原有的边。满足

      \[\begin{aligned} d_u+w'(u,v) &\ge d_v\\ d_u+(w(u,v)+h_u-h_v) &\ge d_v\\ (d_u+h_u)+w(u,v)&\ge (d_v+h_v) \end{aligned} \]

      所以用 \(h_i+d_i\) 作为新的势能对原有的边满足条件。

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

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

      \[\begin{aligned} d_u+w'(u,v) &= d_v\\ d_u+(w(u,v)+h_u-h_v) &= d_v\\ (d_u+h_u)&= (d_v+h_v)-w(u,v)\\ (d_v+h_v)+w(v,u)&=(d_u+h_u) \end{aligned} \]

      因此新增的边 \((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);
}

最后

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

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

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

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

总结

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

posted @ 2023-03-19 13:12  小蒟蒻laf  阅读(226)  评论(0)    收藏  举报