冈络瘤

最大流

EK

  • 增广路:源点至汇点的一条路径使得新增流量不为 \(0\)

那么找最大流有一个很显然的思路就是每一次都找增广路,直到找不到为止,找到之后把增广路上的边权全部减去该增广路新增的流量,找增广路可以使用 BFS(注意要记录路径)。可惜这样是没有正确性的。

问题在于之前的 flow 会对之后的 flow 产生影响,考虑撤销。我们对每条边建一个反向边,最开始每条反向边的流量均是 \(0\),在给正向边减流量的同时给反向边加流量,这样就可以达到撤销的效果(通过反向边让之前的 flow 流向更优的边,为当前 flow 留下通道)。

\(O(nm^2)\)

Dinic

  • 核心:EK 劣在一次 BFS 仅找一条增广路,考虑一次找多条增广路。

首先,在 BFS 的时候标号,求出每一个点的层数 dis(最短增广路长度,图中蓝色数字),这是为了接下来 DFS 的时候可以一遍标记所有最短增广路。

接着 DFS,每一个点记录从源点到当前点的流量 flow,枚举每一条出边,若 dis[v] = dis[u] + 1 那么就去 v 递归找增广路,flow 取 min,然后给当前点的可用流量减去 v 这条增广路的新增流量,然后去下一个儿子。

\(O(n^2m)\)

ISAP

  • 核心:Dinic 劣在多次 BFS,考虑优化。

BFS 直接预处理出 T 到每个点的距离(这是为了可以使 dep[S] > n 之后直接结束算法)。

DFS 类似 Dinic,多判一下如果遍历完 u 的所有儿子流量仍然有剩余,那么就把 u 的层数增加 1。为了剪枝,可以维护每一层的点数,如果某一层点数变成了 0,那么可以直接结束 ISAP 算法(因为出现断层,永远无法从 S 到达 T 了)。

更快的 \(O(n^2m)\)

最大流模板
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long
#define REP(i, l, r) for (int i = l; i <= r; ++i)
#define PER(i, l, r) for (int i = l; i >= r; --i)
#define rep(i, l, r) for (int i = l; i < r ; ++i)
#define per(i, l, r) for (int i = l; i > r ; --i)
#define ld cin
#define jyt cout
// #define int long long
const int N = 2e5 + 7;
const int inf = 1e9 + 7;
const ll linf = 1e18 + 7;
const int P = 998244353;
namespace MG42 {
    int n, m, S, T, dep[N], gap[N], Q[N], head, tail; ll Ans = 0;
    struct Star_W {
        struct Node {int to, nxt, w;} e[N << 1]; int h[N], ec = 1; // 下标从 1 开始,方便取反边。
        inline void AddEdge(int u, int v, int w) {e[++ec].to = v, e[ec].nxt = h[u], e[ec].w = w, h[u] = ec;}
    } E;
    inline void Bfs() { // 从 T 到 S 更新 dep,从而方便我们判是否出现断层。
        Q[head = tail = 1] = T, gap[dep[T] = 1] = 1;
        for (int x = Q[head]; head <= tail; x = Q[++head]) for (int i = E.h[x], v = E.e[i].to; i; i = E.e[i].nxt, v = E.e[i].to) if (!dep[v]) Q[++tail] = v, ++gap[dep[v] = dep[x] + 1];
    }
    inline int Dfs(int x, int flow) {
        if (x == T) return Ans += flow, flow; int used = 0;
        for (int i = E.h[x], v = E.e[i].to, flowing_boat = 0; i; i = E.e[i].nxt, v = E.e[i].to) if (E.e[i].w && dep[v] == dep[x] - 1 && (flowing_boat = Dfs(v, min(flow - used, E.e[i].w)), E.e[i].w -= flowing_boat, E.e[i ^ 1].w += flowing_boat, used += flowing_boat) == flow) return used; // 用完流直接 return
        if (!(--gap[dep[x]++])) dep[S] = n + 1; ++gap[dep[x]]; return used;
    }
    signed main() { int u, v, w;
        ld >> n >> m >> S >> T;
        REP(i, 1, m) ld >> u >> v >> w, E.AddEdge(u, v, w), E.AddEdge(v, u, 0);
        Bfs(); 
        while (dep[S] <= n) Dfs(S, inf); // 大于 S,出现断层,直接退出。
        jyt << Ans << '\n';
        return 0; 
    }
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    MG42::main(); return 0;
}

费用流

EK & Dinic 费用流

把 Dinic 的 BFS 换成 SPFA 就可以了,判的时候用 dep[v] = dep[u] + w 即可。

注意判断一条边费用为 \(0\) 的情况,此时会反复递归导致死循环,在 DFS 的时候通过 vis 让每一个点都只去一次就可以了。

小优化:如果去一个点的流为 \(0\),以后就都不尝试去这个点更新增广路了。

费用流模板
#include<bits/stdc++.h>
using namespace std;
#define ld cin
#define jyt cout
#define int long long
#define ll long long
#define ull unsigned long long
#define REP(i, l, r) for (int i = l; i <= r; ++i)
#define PER(i, l, r) for (int i = l; i >= r; --i)
const int N = 1e5 + 7;
const int inf = 1e9 + 7;
const ll linf = 1e18 + 7;
const int P = 998244353;
namespace SS {
	int n, m, S, T, Q[N], vis[N], Dfs_vis[N], dis[N], now[N], Ans1 = 0, Ans2 = 0;
	struct Node {int to, nxt, w, cost;} e[N << 1]; int h[N], ec = 1;
	inline void AddEdge(int u, int v, int w, int cost) {e[++ec].to = v, e[ec].nxt = h[u], e[ec].w = w, e[ec].cost = cost, h[u] = ec;}
	inline void SpanEdge(int u, int v, int w, int cost) {AddEdge(u, v, w, cost), AddEdge(v, u, 0, -cost);}
#define nxt(x) (x = (x + 1 > n ? 1 : x + 1))
#define FR(x) for (int i = x, v = e[i].to, w = e[i].w, cost = e[i].cost; i; i = e[i].nxt, v = e[i].to, w = e[i].w, cost = e[i].cost)
	inline bool Spfa() { int head = 1, tail = 1, sz = 1; REP(i, 1, n) now[i] = h[i], dis[i] = linf, Dfs_vis[i] = 0; dis[Q[head] = S] = 0;
		for (int x = Q[head]; sz; x = Q[nxt(head)], --sz, vis[x] = 0) 
			FR(h[x]) if (w && dis[v] > dis[x] + cost) dis[v] = dis[x] + cost, (!vis[v] && (Q[nxt(tail)] = v, ++sz, vis[v] = 1));
		return dis[T] != linf;
	}
	inline int Dfs(int x, int flow) {
		if (x == T) return Ans1 += flow, flow; int used = 0, flowing_boat = 0; Dfs_vis[x] = 1;
		FR(now[x]) if ((now[x] = i) && !Dfs_vis[v] && w && dis[v] == dis[x] + cost && (flowing_boat = Dfs(v, min(w, flow - used)), Ans2 += cost * flowing_boat, e[i].w -= flowing_boat, e[i ^ 1].w += flowing_boat, (!flowing_boat && (dis[v] = linf)), used += flowing_boat) == flow) return used; 
		return used;
	}
	signed main() { int u, v, w, cost;
		ld >> n >> m, S = 1, T = n;
		REP(i, 1, m) ld >> u >> v >> w >> cost, SpanEdge(u, v, w, cost);
		while (Spfa()) Dfs(S, linf);
		jyt << Ans1 << ' ' << Ans2 << '\n';
		return 0;
	}
}
signed main() {
	ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
	SS::main(); return 0;
}

zkw 费用流

咕咕咕。

建模技巧 & 例题

基础最大流/多源多汇建模

核心思想:超级原点 & 超级汇点

P1402 酒店之王(一般图模型)

题目给出若干条较为复杂关系限制,考虑图论建模。

不妨用一条流来表示有一个人满意的搭配。因为一个人要同时满足两个条件,所以可以考虑把人放在中间,S 向房间连边,房间向人连边,人向菜品连边,菜品向 T 连边,每条边流量为 1。

但是这样仍有问题:菜,房间确实只会在一个流中出现,但人由于被放置在中间,可能会被多个流同时流过。不妨把一个人拆成一个入点和一个出点,入点和出点间连一条流量为 1 的边,这正是网络流中常用的拆点思想

P2756 飞行员配对方案问题(二分图最大匹配)

  • 二分图最大匹配 = 二分图多源多汇最大流

建一个超级原点 S 和一个超级汇点 T,点集 A 和 S 相连,点集 B 和 T 相连,跑 S 到 T 的最大流即可。

UVA11419 SAM I AM(二分图最小顶点集覆盖 König 定理)

  • König 定理:二分图最小顶点集覆盖 = 二分图最大匹配数 (= 二分图最大流)。

证明:

先证:二分图最小顶点集覆盖 \(\ge\) 二分图最大匹配数,这个很显然,因为每一条匹配边必须被覆盖,且端点两两不重。

然后第二步需要匈牙利算法辅助证明,感性理解一下。

这种网格图且行列操作有关联的题,经常通过关键点给行列连边表示关系,和 [ARC112D] Skate 是一个套路。

然后这题连边以后就是个裸的最小顶点集覆盖。

难点:输出方案

暂无题目(二分图最大独立集)

  • 二分图最大独立集 = 二分图最小点覆盖

[CQOI2014] 危桥[hard]

最大流毕业题。首先不难发现往返走同一条路径一定最优(这样可以把更多的边留给后面的人)。如果我们按照原网络建图,会遇到两个问题:

  • 两个源点到汇点的流量会交叉。
  • 危桥可能会被 Alice,Bob 正反各走一个往返,走 \(4\) 遍。

此时,只要知道 \(f_1,f_2\) 就可以解出 \(f_A,f_B\),考虑 \(f_1,f_2\) 的组合意义。注意到 \(f_1\) 就是从 \(A_1,B_1\)\(A_2,B_2\) 的流,\(f_2\) 就是从 \(A_1,B_2\)\(A_2,B_1\) 的流,证明如下:

问题二在这种情况下也被完美解决。

所以跑两遍最大流,一遍从 \(A_1,B_1\)\(A_2,B_2\),另一遍从 \(A_1,B_2\)\(A_2,B_1\),同时满流即是合法情况。

最大流最小割定理

最大流最小割定理:任意图,最小割 = 最大流,且最小割就是最大流中满流的边的集合。

始终牢记最小割的组合意义就是“二选一”。

拆点/拆边思想

核心思想:对点有限制,将点拆成多个点,转化为边的限制,使用网络流。对于费用流,拆边可以做到“阶梯水价”的效果。

P2053 [SCOI2007] 修车【Imp】

启发点:有的题网络流可以直接表示贡献,但有的题网络流只是限制,费用流才是计算合法方案的最小贡献。

时 间 拆 点,状态 \(\delta(i,j)\) 表示第 \(i\) 个维修人员,后面还要修 \(j\) 辆车。

每一个车只能被修一次,入度为 \(1\),每个状态至多被一个车占,出度为 1。

费用容易计算,跑费用流就行了。

点击查看代码
signed main() {
	ld >> W >> H, S = 1, T = 2, n = 2;
	REP(i, 1, H) idk[i] = ++n, Span(S, idk[i], 1, 0);
	REP(i, 1, H) REP(j, 1, W) ld >> a[i][j], g[j][i] = ++n, Span(g[j][i], T, 1, 0);
	REP(i, 1, H) 
		REP(j, 1, W) 
			REP(k, 1, H) 
				Span(idk[i], g[j][k], 1, a[i][j] * k);
	while (Spfa()) Dfs(S, inf); 
	printf("%.2lf\n", (1.0 * Ans2 / H));
	return 0; 
}

网格图模型/染色模型/划分模型(流的串并联)

使用边表示题目中的条件/贡献,表示关系而无限制/贡献的边通常将流量置为无穷。

流的串联:将两个或多个条件串联,边被加入最小割则表示不选,从而表示出“多选一”的效果。

流的并联:将多个条件并联,这条边未被加入最小割,这条边指向的点也会被选,从而表示出大家必须一起选的效果。

流的混联:通过并联和串联+极值边就可以表示出大多数题目中的限制条件。

P3355 骑士共存问题 / P2774 方格取数问题

网格图,黑白交替染色,黑点内部和白点内部无限制,拆点,从黑到白跑二分图最小割即可。

P5030 长脖子鹿放置

染色的本质是让有限制的点颜色不一样。这题中只要保证攻击到的点和它颜色不一样就行。

不难观察到一行一个颜色就可以了。做网络流切忌思维固化!

P4313 文理分科【Imp】

妙妙题。

先考虑没有 same 限制的情况,用 \(S \overset{\operatorname{art}_x}{\longrightarrow} x \overset{\operatorname{science}_x}{\longrightarrow} T\) 这种方式连边,跑最小割,就可以做到选科的效果。

考虑如何处理 same 的限制(为了方便,这里先只看文科),可以这样建图:

其中 w0 表示这五个人全文科的额外贡献,w1~5 就表示这五个人单选文科的贡献,此时如果没有断 w0,由于不可能断极值边,所以此时一定不会断 w1~5,反之有元素选理科必定会导致所有文科边全部被删掉,就成功表示出了全文科的额外贡献。给后面串联一个理科就可以表示二选一了。

点击查看代码
signed main() { int u, v, Sum = 0;
	ld >> H >> W, S = 1, T = 2, n = 2;
	REP(i, 1, H) REP(j, 1, W) g1[i][j] = c1[i][j] = ++n, g2[i][j] = ++n, c2[i][j] = ++n;
	REP(i, 1, H) REP(j, 1, W) ld >> art1[i][j], Sum += art1[i][j];
	REP(i, 1, H) REP(j, 1, W) ld >> science1[i][j], Sum += science1[i][j];
	REP(i, 1, H) REP(j, 1, W) ld >> art2[i][j], Sum += art2[i][j]; 
	REP(i, 1, H) REP(j, 1, W) ld >> science2[i][j], Sum += science2[i][j];
	REP(i, 1, H) REP(j, 1, W) Span(S, g1[i][j], art1[i][j]), Span(S, g2[i][j], art2[i][j]), Span(c1[i][j], T, science1[i][j]), Span(c2[i][j], T, science2[i][j]);
	REP(i, 1, H) REP(j, 1, W) {
		Span(g2[i][j], g1[i][j], inf), Span(c1[i][j], c2[i][j], inf);
		if (i != 1) Span(g2[i][j], g1[i - 1][j], inf), Span(c1[i - 1][j], c2[i][j], inf);
		if (i != H) Span(g2[i][j], g1[i + 1][j], inf), Span(c1[i + 1][j], c2[i][j], inf);
		if (j != 1) Span(g2[i][j], g1[i][j - 1], inf), Span(c1[i][j - 1], c2[i][j], inf);
		if (j != W) Span(g2[i][j], g1[i][j + 1], inf), Span(c1[i][j + 1], c2[i][j], inf);
	}
	Bfs();
	while (dep[S] <= n) Dfs(S, inf); 
	jyt << Sum - Ans << '\n';
	return 0; 
}

对偶模型

核心思想:平面图最大流 = 对偶图最短路

P4001 [ICPC-Beijing 2006] 狼抓兔子/[NOI2010] 海拔

建立对偶图后,从左下角到右上角跑最短路,这条最短路就可以把 S,T 割开。

最大权闭合子图

最大密度子图

混合图欧拉回路

posted @ 2025-02-21 19:14  flowing_boat  阅读(40)  评论(0)    收藏  举报