最大流 / 最小割 / 费用流

最大流 / 最小割 / 费用流

一些定义

流网络:\(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 找增广路的思路。

根据不同的实现方式。有 ekdinic ,和 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);
}

最后

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

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

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

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

总结

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

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