浅谈网络流

发布时间 2023-12-29 21:25:30作者: FAKUMARER

浅谈网络流

最近网络流做了一堆,感觉有微弱的进步!

记录一些 好的套路,好的错误,以便以后再错

板子

根据地方法律法规,最大流\(Dinic\) 以及 费用流\(EK\) 不应当被卡,望周知

下面并没有出现 \(HLPP\) 的任何板子

因为这个东西 十分的难调 并 理论时间复杂度很对(一定不是指 上界极紧

导致我根本不会写(根本原因)

最大流

多用各种 增广路算法,下面给出 两种常用算法实现

容易被卡,而且 绝绝绝大多数情况下 能被 \(Dinic\) 平替的 \(EK\) 就直接跳了

最大流增广路 大体思路 就是每次找一条 可行通路,增流到 被塞满了!

然后有一层的边 都被塞满了!

于是 进不去,怎么想也进不去吧!(指流量)

就结束了

最大流的正确性很显然 —— \(\textsf {Meatherm}\)


\(Dinic\)

namespace Dinic {
	const int MAXN = 100005;
	const long long INF  = 1e16;
	
	struct Node {
		int to, nxt;
		long long f;
	} E[MAXN << 1];
	
	int H[MAXN], tot = 1;
	
	inline void Add_Edge (const int u, const int v, const long long f) {
		E[++ tot] = {v, H[u], f}, H[u] = tot;
		E[++ tot] = {u, H[v], 0}, H[v] = tot;
	}
	
	int S, T;
	int Cur[MAXN], Dep[MAXN];
	
	inline bool BFS () {
		fill (Dep, Dep + MAXN, -1);
		queue <int> q;
		q.push (S), Dep[S] = 0, Cur[S] = H[S];
		int u, v, f;
		while (!q.empty ()) {
			u = q.front (), q.pop ();
			for (int i = H[u]; i; i = E[i].nxt) {
				v = E[i].to, f = E[i].f;
				if (Dep[v] == -1 && f) {
					Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v);
					if (v == T) return 1;
				}
			}
		}
		return 0;
	}
	
	inline long long DFS (const int x, const long long MAXF) {
		if (x == T || MAXF == 0) return MAXF;
		long long F = 0;
		for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
			Cur[x] = i;
			if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
				long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
				if (!TmpF) Dep[E[i].to] = -1;
				F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
			}
		}
		return F;
	}
	
	inline long long dinic () {
		long long F = 0;
		while (BFS ()) F += DFS (S, INF);
		return F;
	}
}

注意 当前弧优化 的实现!

考虑这个东西 我曾经写过 以下 3 * 3 种方式,感觉把 能错的都错了

在初始化上

inline bool BFS () {
    fill (Dep, Dep + MAXN, -1);
    queue <int> q;
    q.push (S), Dep[S] = 0, Cur[S] = H[S]; // 开头特判源点
    int u, v, f;
    while (!q.empty ()) {
        u = q.front (), q.pop ();
        for (int i = H[u]; i; i = E[i].nxt) {
            v = E[i].to, f = E[i].f;
            if (Dep[v] == -1 && f) { // 每次初始化 边终点 (E[i].to)
                Dep[v] = Dep[u] + 1, Cur[v] = H[v], q.push (v);
                if (v == T) return 1;
            }
        }
    }
    return 0;
}

这个第一种写法是对的,考虑每个点 \(Dep\) 都将被更新到,后面 \(DFS\) 可以正常执行多路增广。

这个写法就是要注意 特判源点但不需要额外辅助变量,时间也很对,十分规范!


inline bool BFS () {
    fill (Dep, Dep + MAXN, -1);
    for (int i = 1; i <= N; ++ i) Cur[i] = H[i];
    // or memcpy (Cur, H, sizeof H); 在开头统一初始化
    queue <int> q;
    q.push (S), Dep[S] = 0;
    int u, v, f;
    while (!q.empty ()) {
        u = q.front (), q.pop ();
        for (int i = H[u]; i; i = E[i].nxt) {
            v = E[i].to, f = E[i].f;
            if (Dep[v] == -1 && f) {
                Dep[v] = Dep[u] + 1, q.push (v);
                if (v == T) return 1;
            }
        }
    }
    return 0;
}

这个第二种写法也是对的,在 开头全部复制 这个一听就不可能有问题。

但是你要么需要把 \(N\) 写在函数前面(本人习惯板子单独放前面,所以 \(N\) 一般在后面)

要么 \(memset\) 一整个数组(当你数组大小和实际点数差异大时,会浪费很多时间

总之不很完美


inline bool BFS () {
    fill (Dep, Dep + MAXN, -1);
    queue <int> q;
    q.push (S), Dep[S] = 0;
    int u, v, f;
    while (!q.empty ()) {
        u = q.front (), q.pop (), Cur[u] = H[u]; // 每次初始化 边起点
        for (int i = H[u]; i; i = E[i].nxt) {
            v = E[i].to, f = E[i].f;
            if (Dep[v] == -1 && f) {
                Dep[v] = Dep[u] + 1, q.push (v);
                if (v == T) return 1;
            }
        }
    }
    return 0;
}

这个第三种写法就很错了,而我甚至在 相当长一段时间 都是这么写的

因为他确实比前面俩简单

仔细思考会发现,它只更新到 第一个能到达汇点的点,然后就 退出了

于是 十分逆天,这玩意儿直接退化成了 \(EK\)(在 \(DFS\) 的时候每次只有 一路可用


讲一个 题 外 话

费用流\(SPFA\) 实现中,第三种写法并无问题

原因显然,\(SPFA\) 的实现中 并不允许到汇点就提前跳出


在使用上

也可以看这个

for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
	Cur[x] = i;
	if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
		long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
		if (!TmpF) Dep[E[i].to] = -1;
			F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
	}
	// if (F >= MAXF) break;
}

这个第一种写法是对的,不取地址,手动记录

然后你 \(F < MAXF\) 的判断就可以放在循环里(虽然很容易忘

相当的规范,我十分喜欢!


for (int &i = Cur[x]; i; i = E[i].nxt) {
	// Cur[x] = i;
	if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
		long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
		if (!TmpF) Dep[E[i].to] = -1;
			F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
	}
	if (F >= MAXF) break;
}

这个第二种写法也是对的,可取地址,不用写 \(Cur[x] = i\) 这个东西

但是 \(F\)\(MAXF\) 但判断要 在循环末尾写出来,不很美观


for (int &i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
	// Cur[x] = i;
	if (Dep[E[i].to] == Dep[x] + 1 && E[i].f) {
		long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
		if (!TmpF) Dep[E[i].to] = -1;
			F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
	}
	// if (F >= MAXF) break;
}

这个第三种写法就很错了,感觉上就是把前两种结合起来吧?

但事实上这个十分的假,每次都会增广不完效率低下

根据考证,问题还是出在 循环条件执行顺序 的理解上

考虑原来理解循环的顺序是 先执行判断,再执行赋值

但事实上,完整的 循环执行步骤 是这样的:


(第一次执行时)1.执行 for 的 第一语句
(第一次执行时)2.判断 for 的 第二语句
(后续循环){
	1.循环第一行
	2.循环最后一行
	3.执行 for 的第三语句
	4.判断 for 的第二语句
}

比较神秘,带进去就可以很快判断出三种写法 对为什么对,错为什么错


一些 容易忘 的地方

1.建边

  • 单向边反边 流量为 0,双向边反边 流量为 \(f\)
  •  \(tot\) 初始值为 1 !!!!!
  • 边集数组 \(E\) 时,大概率 \(<< 1\) 是不够的
  • 注意 源汇 及 其它点 编号是否 大于 \(MAXN\)

2.BFS

  • 特判 源点当前弧当前弧当前弧!!!
  • \(q.pop () ~~ q.push(v)\) 一生之敌
  • 不要写成 \(SPFA\) 还加个 \(Vis\)这显得很傻

3.DFS

  • 开局是 \(return ~ MAXF\) 不是 \(return ~ 0\)
  • 记得判断 \(F < MAXF\)
  • 记得最后 \(return ~ F\)

感觉上述问题每次总会 随机一个 出现,火大

yny の 神 秘 优 化
namespace Dinic {
	struct Edge {
		int u, v;
		long long w;
		
		inline bool operator < (const Edge &a) const {
			return w > a.w;
		}
	};
	
	struct Node {
		int to, id;
		long long f;
	};
	
	vector <Edge> Tmp;
	vector <Node> E[MAXN];
	
	int Now[MAXN];
	
	inline void Add_Edge (const int u, const int v, const long long w) {
		Tmp.push_back ({u, v, w});
	}
	
	inline void add_edge (const int x) {
		E[Tmp[x].u].push_back ({Tmp[x].v, Now[Tmp[x].v] ++, Tmp[x].w});
		E[Tmp[x].v].push_back ({Tmp[x].u, Now[Tmp[x].u] ++, 0});
	}
	
	int Cur[MAXN], Dep[MAXN];
	int S, T;
	
	inline bool BFS () {
		memset (Cur, 0, sizeof Cur);
		memset (Dep, 0, sizeof Dep);
		queue <int> q; 
		q.push (S), Dep[S] = 1;
		int u;
		while (!q.empty ()) {
			u = q.front (), q.pop ();
			for (auto i : E[u]) 
				if (!Dep[i.to] && i.f) {
					Dep[i.to] = Dep[u] + 1, q.push (i.to);
					if (i.to == T) return 1;
				}
		}
		return 0;
	}
	
	inline long long DFS (const int x, const long long MAXF) {
		if (x == T || MAXF == 0) return MAXF;
		long long f = 0;
		for (int i = Cur[x]; i < (int) E[x].size () && f < MAXF; ++ i) {
			Cur[x] = i;
			if (Dep[E[x][i].to] == Dep[x] + 1 && E[x][i].f) {
				long long TmpF = DFS (E[x][i].to, min (MAXF - f, E[x][i].f));
				if (!TmpF) Dep[E[x][i].to] = 0;
				f += TmpF, E[x][i].f -= TmpF, E[E[x][i].to][E[x][i].id].f += TmpF;
			}
		}
		return f;
	}
	
	inline long long Solve () {
		long long f = 0;
		while (BFS ()) f += DFS (S, INF);
		return f;
	}
	
	inline long long dinic () {
		sort (Tmp.begin(), Tmp.end());
		long long Ans = 0;
		for (int i = 1e9, j = 0; j < (int) Tmp.size(); i /= 20) {
			while (Tmp[j].w >= i && j < (int) Tmp.size()) add_edge (j), ++ j;
			Ans += Solve ();
		}
		return Ans;
	}
}

本质上是 按值域分块加边,把 相近流量 的边 放在一起

块长 这个东西每次 \(\div 20\) 十分合适,也有 \(<< 4\) 之类的,动态调整就好

需要用 \(vector\) 来存边,初始加边只是存入,真正跑之前再 排序加边

能轻松跑过 Luogu P4722 【模板】最大流 加强版 / 预流推进 什么实力不用多说

但是这个优化很玄学,就...(一直不知道在理论复杂度上真的有优化吗?

然后显然,这东西并不能在 动态加边 的过程中 保持良好的复杂度

因为本身就是 先把边集离线了 然后特定方式加边

有一定局限性,但还是很值得学

费用流 似乎也能应用 这个东西?没有仔细研究过 正确性 上的证明?

我实现过一份,但是板子题 \(60~pts ~~ TLE\) 了,有种卡死的感觉

其它点倒没有 \(WA\)

先鸽了,会的 \(DALAO\) 教教!!!

\(ISAP\)

注意,以下板子 有可能 是假的!

主要是因为按理说 这玩意儿 要比 上面那玩意儿 快一些

但我写 这篇随笔 之前专门去测了一下...(Luogu P3376 | P4722

嘶,效果并不理想(至少在 板子题上

namespace _ISAP {
	struct Edge {
		int to, nxt;
		long long f;
	} E[MAXN << 1];
	
	int H[MAXN], tot = 1, N; // FUCK
	
	inline void Add_Edge (const int u, const int v, const int f) {
		E[++ tot] = {v, H[u], f}, H[u] = tot;
		E[++ tot] = {u, H[v], 0}, H[v] = tot;
	}
	
	int Dep[MAXN], Gap[MAXN], Cur[MAXN];
	int S = 11451, T = 19198;
	
	inline void BFS () {
		memset (Dep, -1, sizeof Dep);
		memset (Gap, 0, sizeof Gap);
		queue <int> q;
		q.push (T), Gap[0] = 1, Dep[T] = 0;
		int u, v;
		while (!q.empty ()) {
			u = q.front (), q.pop ();
			for (int i = H[u]; i; i = E[i].nxt) {
				v = E[i].to;
				if (Dep[v] != -1) continue;
				q.push (v), Dep[v] = Dep[u] + 1, ++ Gap[Dep[v]];
			}
		}
		return;
	}
	
	inline long long DFS (const int x, const long long MAXF) {
		if (x == T) return MAXF;
		long long F = 0;
		for (int i = H[x]; i; i = E[i].nxt) {
			if (E[i].f && Dep[E[i].to] + 1 == Dep[x]) {
				long long TmpF = DFS (E[i].to, min (E[i].f, MAXF - F));
				if (TmpF) F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF;
				if (F == MAXF) return F;
			}
		}
		-- Gap[Dep[x]];
		if (Gap[Dep[x]] == 0) Dep[S] = N;
		Gap[++ Dep[x]] ++;
		return F;
	}
	
	inline long long ISAP () {
		long long F = 0;
		BFS ();
		while (Dep[S] < N) F += DFS (S, INF);
		return F;
	}
}

这个东西的优化 讲人话就是 省掉了多次 \(BFS\)

通过 \(Gap\) 数组记录每个 \(Dep\) 的点数,由于 \(BFS\) 是反向跑的

然后每次把当前点 增广完之后就更新当前点 \(Dep\)(当前点不可用了)

那么显然 如果源点 \(S\)\(Dep\) 被推到 \(N\),或者说出现了 断层(没法联通了)

那就完了!返回最大流就行

问题是这东西丑就丑在你需要 \(N\) 放在前面!!!

没有一点好!!!!!

但不然的话 第一个结束条件 就没法判,时间会很有问题

 其他的各种算法 还是 看板子题题解 来的好,我不了解的也就不写了

费用流

最小费用最大流,但是 板子可以用到各种地方

不排除 当你懒得改板子的时候 甚至可以拿来写 最大流

下面将给出一种 垃圾的增广路实现使得它变强的办法

以及一种 本身就非常强的 单纯形实现

\(Dinic\)

实现

先讲实现,本质就是从 最大流 \(Dinic\) 扩展而来

\(BFS\) 部分改成 您喜欢的 最短路算法(\(SPFA, Dijkstra...\) 都行)

边权就是 边上费用\(DFS\) 增流时 统计费用 即可

注意由于 反边大多情况下是带负权的,所以此处的 \(Dijkstra\) 实现参考 \(Johnson\) 全源最短路,需要先跑一遍 \(Bellman-Ford ~ / ~ SPFA\) 来 处理负边权问题(设计势能)。

考虑到上面的问题,显然 \(Dijkstra\) 实现 并不支持动态加边

(不然你加一次,跑一次 \(SPFA\) 处理,不如直接 \(SPFA\)

并且 常数堆的实现 有很大关系

(这导致在 \(C++\) 语言下是否打开 \(O2\) 对其影响是 致命的

有一定局限性,就不写了

\(SPFA + Dinic\) 实现

namespace MCMF {
	struct Node {
		int to, nxt;
		long long f, w;
	} E[MAXN << 1];
	
	int H[MAXN], tot = 1;
	
	inline void Add_Edge (const int u, const int v, const long long f, const long long w) {
		E[++ tot] = {v, H[u], f, + w}, H[u] = tot;
		E[++ tot] = {u, H[v], 0, - w}, H[v] = tot;
	}
	
	bool Vis[MAXN];
	int S, T;
	int Dis[MAXN], Cur[MAXN];
	
	inline bool SPFA () {
		memset (Dis, 63, sizeof Dis);
		deque <int> q;
		q.push_front (S), Dis[S] = 0;
		int u, v, w, f;
		while (!q.empty ()) {
			u = q.front (), q.pop_front (), Vis[u] = 0, Cur[u] = H[u];
			for (int i = H[u]; i; i = E[i].nxt) {
				v = E[i].to, w = E[i].w, f = E[i].f;
				if (Dis[v] > Dis[u] + w && f) {
					Dis[v] = Dis[u] + w;
					if (!Vis[v]) {
						if (q.empty () || Dis[v] > Dis[q.front ()]) q.push_back (v), Vis[v] = 1;
						else q.push_front (v), Vis[v] = 1;
					}
				}
			}
		}
		if (Dis[T] < 1e9) return 1;
		return 0;
	} 
	
	long long MinC = 0;
	
	inline long long DFS (const int x, const long long MAXF) {
		if (x == T || MAXF == 0) return MAXF;
		Vis[x] = 1;
		long long F = 0;
		for (int i = Cur[x]; i && F < MAXF; i = E[i].nxt) {
			Cur[x] = i;
			if (!Vis[E[i].to] && Dis[E[i].to] == Dis[x] + E[i].w && E[i].f) {
				long long TmpF = DFS (E[i].to, min (MAXF - F, E[i].f));
				if (!TmpF) Dis[E[i].to] = -1;
				F += TmpF, E[i].f -= TmpF, E[i ^ 1].f += TmpF, MinC += TmpF * E[i].w;
			}
		}
		Vis[x] = 0;
		return F;
	}
	
	inline long long Dinic () {
		long long F = 0;
		while (SPFA ()) F += DFS (S, INF);
		return F;
	}
}

一些优化点
  1. 如果 建双向边 那么一般 只需要改动 \(Add\_Edge\) 使反边 流量 / 费用 等于正边即可
  2. 万恶的 当前弧
  3. \(SPFA\) 可以应用 双向队列优化
  4. 注意到其实 最后 \(while\) 里的 \(DFS\) 不需要再套 \(while\),不知道以前在写啥(最大流 一样)
  5. yny の 神秘优化?

正确性?

前日与 \(\textsf {Meatherm}\) 先生讨论了这个实现的 正确性问题,豁然开朗!

首先十分重要的一点,基础的增广路算法 不能处理任何时候的 负圈

\(\textsf {Meatherm}\) 先生 那天提出一种情况

如果我现有边 \(A, B\)\(Cost = 2, Flow = 1\)

再加入边 \(C\)\(Cost = 1, Flow = 1\) 再跑一次

最小费用是几喃?几费喃?三费!

然后可以尝试自己跑跑 平凡的增广路费用流 板子,\(MinCost = 4\) !!!


S = 1, T = 3;
Add_Edge (1, 2, 1, 2);
Add_Edge (2, 3, 1, 2);
MCMF();
Add_Edge (1, 2, 1, 1); // Add_Edge (1, 2, 1, 3) is Right
MCMF();
cout << MINC << endl;
// 流程

(当然也很有可能直接 死循环

怎么回事 呢?考虑第一次增广的时候,\(A, B\) 满流,于是反边有 \(1\) 的可用流量

加入 \(C\) 之后,显然 \(A\) 的反边和 \(C\) 形成了 负环!(隐藏负环 \(+1\)

(把 \(C\) 边权改成 \(> 2\) 就不会出事)

由此也启示出一个需要注意的点

基础的增广路算法 动态加边 的时候需注意 不能产生负环(不应出现 同流同起始边费用更小 之类的情况)

(但无需担心是否会在跑的时候产生 额外负环,考虑每次选的是 最短路,容易证明)

然后考虑为什么 最短增广路 就可以得到 最小费用最大流

我们似乎得出了一些 共同认同的 感性理解 以及 理性证明 如下

首先你的 最短路算法 显然是从 最大流 \(Dinic\)\(BFS\) 扩展而来

保证有 可行流增广路不会停止 的特性

那么在 这一部分

然后只需证明它能 取到最小费用 就行

感性理解 就是

我每次增广,走了 当前的最优解,留下了 给以后反悔用的边

每一次对 原图的影响 实质上相当于 选了一条边 并 反悔了一些边(就像 反悔贪心

反边消除了 可能的后效性影响,于是直接做,就很对

理性一点的话

考虑我们 增流的过程,每次找 最短路 增流

\(F_i\) 在这个图上表示 流量为 \(i\) 时的最小费用

然后假使你 当前要增加 \(1\) 的流量,并且 无后效性

显然直接 贪心在最短路上增流 就行,于是一定有 \(F_{i + 1} = F_i + Dis_{S-T} * MinFlow\)

\(MinFlow\)最短路上的可行流量最小值

到这里,正确性的问题差不多解决了(可能写的不是很清楚?)

\(Simplex\)

网络单纯形 法,由 线性规划问题单纯形算法 变形而来

循环流 可以规约成 线性规划问题的标准型,而 有源汇费用流 加边 就能变成循环流)

关于 线性规划 与 网络流 基本原理这一部分可以看 这个(感觉挺能懂的一个 课件

这里只讲 算法步骤,实现细节 之类的

算法步骤
  1. 为了满足 线性规划标准形,我们先把图转成 循环流,也就是 \(T\)\(S\) 连容量 \(INF\),费用 \(-INF\)

这个也可以理解成为 找负环增流 做准备,因为多数情况下 建出的图没有负环,而加边后就构造出一个

  1. 之后我们找到一个 可行支撑树,也就是 单纯形算法中的

这里 可行支撑树 就是一棵树上都是 未满流边生成树,也即 在支撑树上存在可行流

  1. 之后就开始 判负环,具体就是 枚举边,判断在 支撑树 上 加入该边后是否形成 负环

显然加入一边会形成一个 基环树,然后有一个 均摊十分快 的方法来判断 是否为负环,后面会讲

  1. 加入 这条边(入基边),然后 找到负环上剩余流量最小边,并在这个过程中 存下这个负环

注意还需存储 最小边的绝对位置相对位置(在 入基边 左还是右)

  1. 然后 推流,给这个环上所有边都加上 最小剩余流量,同时 更新费用流量 * 途径边费用

推完流之后就会 把一条边塞满,对应 单纯形算法 中的 转轴变换(的前半部分)

即把 入基变量与离基变量 在单纯形表中交点格 变换成 \(0\)

  1. 推流 之后 重构可行支撑树,可以发现就是 反转入基边到删除边之间的链

这里 反转的具体起止点 需要根据相对位置 简单分讨,依旧 后面随代码讲

这一步就对应 转轴变换 的后一部分,入基变量入基,离基变量离基,重构单纯形表

  1. 返回这一次的 费用,累加到 \(MinCost\),回到 \(Step~3\) 直到 图中没有负环

使得 没有更优的可能,对应使 所有非基本变量系数非负

  1. 现在 最大流 就是 \(Step ~ 1\) 中加上的 \(INF\) 边的流量,\(MinCost\) 要补上这个 最大流 * \(INF\)

考虑 循环流 保证流量平衡,故 \(S_{out} = S_{in}, T_{out} = T_{in}\)

那么显然,\(S_{out} -> T_{in}\) 就是理解的通常 最大流(在 不可增流时\(S\) 流到 \(T\) 的流量)

然后 \(T_{out} -> S_{in}\) 就是初始加入的 \(INF\) 边,显然与 最大流 相等

补费用 这个操作十分容易理解,因为你 \(INF\) 边的有 额外的费用 \(-INF\),不应计入答案

到此结束,可能看上去 有些抽象,下面举一个 特例 帮助理解

真的是特例,只是用于帮助理解的,不应在这里纠结 是否能推广的问题

假设原图是 一条链链上每条边费用容量不等(容量均为 正值

于是先 执行 \(Step~1\),从 \(T\)\(S\) 连了一条 \(INF\) 边,随后 构建支撑树(链的话好像...

显然这里 整个新图构成了一个负环,于是找到 环上流量最小的一条边

由于 \(T -> S\) 的边容量为 \(INF\),显然剩余容量最小边在 原图上

然后 推流,也就是 给整个环加上等于最小边容量的流量,更新费用,重构支撑树

这里的 支撑树 就是 原图 - 容量最小边 + \(INF\)

回到 \(Step ~ 3\) 试图找负环,由于 唯一不在支撑树上的边(刚的容量最小边)满流了

找不到负环了,结束!费用加上 \(INF\) * 容量最小边容量

这时候检查我们得到的答案,最大流 就是 链上容量最小边容量

最小费用 是 最大流量 * 链上边费用和(刚刚 整个链都在负环 中,都被统计了)

很对吧,那么

你已经学会一条链的情况了,快推广到任意图吧!