网络流建模小记

前言

牢人言:

网络流能把一个看似是 NPC 的问题转换为多项式问题.

网络流题目的一大难点就是想到要想到建模. 通常如果你看到一个题目满足数据范围比较小,不太能做指数级算法,并且第一想法是贪心但是贪心是假的,这个时候你就可以思考网络流了.

基础模型

板子

主要讲一下如何看出是那种板子,因为过于板所以不会细讲.

最大流

就是直接建出以流量代表贡献的网络,跑最大流得到最大贡献.

容易发现涉及最小贡献的几乎不可能是最大流,要及时排除这种想法.

Luogu P2756 飞行员配对方案问题

Hint:发现每个英国飞行员仅能匹配一个外籍飞行员,这明显是二分图最大匹配,考虑最大流.

考虑刻画每个飞行员只能匹配一次的性质,那么源点流向外籍飞行员,外籍飞行员流向英国飞行员,英国飞行员流向汇点都连一条流量为 \(1\) 的边. 一条流量为 \(1\) 的增广路就代表了一个匹配,故最大流即为最大匹配.

Luogu P3254 圆桌问题

Hint:可行性问题,考虑最大流.

重要的限制是每个单位的人不能在同一桌吃饭,所以每个单位向每个餐桌连一条流量为 \(1\) 的边. 然后源点和汇点分别向单位和餐桌连代表数 \(r_i\) 和 餐桌容量 \(c_i\). 最大流与 \(\sum_ir_i\) 相等即存在方案.

考虑输出方案,每个代表分配到一个餐桌当且仅当单位与餐桌的正向边流量为 \(0\),反向边流量为 \(1\),依次输出即可.

最小割

最小割求得的是割去流量最小的一组边使得网络不连通,比如典型的价值二选一就可以很好的刻画.

最虽然在结果上与最大流的值相等,但是不能把两者直接划等号. 它们的建模思路常常大相径庭.

Luogu P1361 小M的作物

Hint:要么单独种要么合种,只能获得一种收益,考虑最小割.

考虑怎么刻画合种的额外收益,如果不割合种的收益那么合种的所有作物也不会割;如果割掉一种作物那么合种一定也会割掉. 所以合种的收益应该单独建点流向分开种的收益,流量为 \(inf\). 源点流向所有分开种和在 A 合种的点,流量为 A 地的收益;分开种的点和在 B 合种的点流向汇点流量为 B 地的收益. 总收益减去最小割即为最大收益.

Luogu P4313 文理分科

Hint:与前一个题基本一样.

相当于“上下左右中” \(5\) 个人都选相同的科才获得额外收益,稍微判一边界即可.

费用流

建出最小/最大费用最大流代表贡献的网络,求出最大流顺便得到最小/最大贡献(最大费用最大流只需要把所有费用取相反数,再把结果取相反数即可).

虽然费用流费用与流量挂钩,但是大多数费用流建模的题目都不会流大于 \(1\) 流量的费用,此时的流量作为费用的某种限制而不太有它本身在费用流板子里面的意义,所以要仔细分析到底是用最大流还是费用流. 其他情况判断出使用费用流是容易的.

下文没有特别说明的 \((w, c)\) 表示连一条流量为 \(w\),费用为 \(c\) 的边.

Luogu P4016 负载平衡问题

Hint:每个仓库可以向左右运货物,要求最后每个仓库货物数都相等的最小花费,考虑费用流.

不妨流量代表货物,每个仓库向左右连边,费用代表搬运的货物数量.
每个仓库初始货物 \(a_i\),平均货物 \(s\),考虑源点向每个仓库连 \((a_i, 0)\) 的边,每个仓库向汇点连 \((s, 0)\) 的边,这样就确保了得到的最小费用满足题意. 然后把仓库看成一个环,每个仓库向左右连 \((inf, 1)\) 的边即可. 费用流会自己找到最优情况.

Luogu P4012 深海机器人问题

Hint:显然是费用流板子.

每条输入的价值连 \((1, w)\) 的边,源点向每个起点连 \((k, 0)\) 的边,每个终点向汇点连 \((r, 0)\) 的边,跑最大费用最大流即可.

技巧

在板子的基础上有一些很厉害的建图技巧,应用更加灵活.

最大权闭合子图

最小割的拓展模型. 即在点权有正负的一般有向图找到一个点权和最大的闭合子图(即子图每个点的后继都包含在子图内).

钦定原来点权为正的为左部点,边权为负的为右部点(边权为 \(0\) 的在哪一边无所谓). 源点向左部点连流量为点权的边,右部点向汇点连流量为点权绝对值的边,然后连上原图的边流量为 \(inf\). 答案即为正点权之和减去这个网络的最小割.

容易感性理解:原图的答案为 \(正权和最大 + 负权和最小\),也就是 \(所有正权和-(正权和最小+负权和最小的绝对值)\). 严谨一点来说,网络流上割去汇点的边代表要选择这个点,因为这条增广路来自正点权的流量与其进行了抵消,相当于正负点权加起来抵消;割去源点的边代表不选择这个点,因为如果网络连通,说明选的正权点还有负权的后继需要选,就要割去这个边才满足闭合子图的定义.

遇到类似有正负点权或者可以转换为这种问题的就可以考虑建网络用最小割求解.

Luogu P2762 太空飞行计划问题

Hint:选择一个实验要求选择所有需要的仪器,每个实验向所需仪器连边,这就是闭合子图的限制.

相当于仪器都是负点权,实验都是正点权,求最大收益就与最大权闭合子图模型一模一样了.

黑白染色

建模的技巧.

建网络时针对奇偶性之类的性质,把原图按像棋盘一样的黑白染色分成两类点,转换为类似二分图求解.

Luogu P3355 骑士共存问题

Hint:黄点只能攻击红点,考虑黑白染色. 彼此互不攻击考虑最小割.

对于坐标 \((x,y)\),不妨以 \(x+y\) 的奇偶性黑白染色. 钦定先在所有非障碍点放骑士,黑点的骑士一定不会相互攻击. 黑点再向所有可以攻击到的白点连流量为 \(inf\) 的边. 然后源点向所有黑点连流量为 \(1\) 的边,所有白点向汇点连流量为 \(1\) 的边. 有骑士相互攻击当且仅当网络连通,最小割割去源点的边代表不选这个黑点,割去汇点的边代表不选这个白点. 最后所有非障碍点数减去最小割即为答案.

Luogu P2774 方格取数问题

Hint:取一个格子上下左右四连通都不能取,考虑黑白染色. 不能同时取考虑最小割.

对于坐标 \((x,y)\),不妨以 \(x+y\) 的奇偶性黑白染色. 钦定先选所有格子,所有黑色格子一定符合要求. 黑色格子再向所有不能同时选到的白色格子连流量为 \(inf\) 的边. 然后源点向所有黑色格子连流量为 \(a_{i,j}\) 的边,所有白色格子向汇点连流量为 \(a_{i,j}\) 的边. 有选择格子不符合要求当且仅当网络连通,最小割割去源点的边代表不选这个黑色格子,割去汇点的边代表不选这个白色格子. 最后所有权值和减去最小割即为答案.

拆点

建模的技巧.

把原题中的一个点拆成多个点,可以处理很多特殊限制,比如每个点只能走一次.

Luogu P2153 [SDOI2009] 晨跑

Hint:天数和路程两个限制直接考虑费用流.

考虑如何保证每个点只能走一次,每个点 \(u\) 拆为 \(u_1,u_2\) 表示出点和入点,\(u_1\)\(u_2\) 连边 \((1,0)\),原图 \(u,v\) 的边如果路程为 \(d\) 就在网络上连 \(u_1\)\(v_2\)\((1, d)\) 边.

但是起点和终点可以走多次,所以拆点单独建 \((inf, 0)\) 的边.

Luogu P2764 最小路径覆盖问题

Hint:增广路视为合并两点,总点数减去最大流就是最小路径覆盖.

每个点只能经过一次,拆点,入点和出点连一条流量为 \(1\) 的边. 然后源点到入点连流量为 \(1\) 的边,出点到汇点连流量为 \(1\) 的边,直接跑最大流即可.

如果路径可以经过重复的点就先跑一遍 Floyd,再按照上面的方法求解.

Luogu P2766 最长不下降子序列问题

Hint:第一问暴力 DP,二三问根据 DP 转移建图跑最大流.

每个元素只能用一次就拆成入点和出点并且连一条流量为 \(1\) 的边. 最大化子序列个数肯定是最大流来保障,所以每个子序列长度都为 \(s\) 就需要在建图上想办法. 先考虑 DP,设 \(f_i\) 表示以 \(i\) 结尾的最长不下降子序列长度,\(n^2\) 暴力跑. 现在枚举 \(j<i\) 考虑怎么连边,首先必须有 \(a_j\le a_i\),其次构成最长长度的连边一定是在 \(f\) 产生转移的时候,所以另一个条件是 \(f_j+1=f_i\). 相当于用 DP 转移给网络分了层,非常的厉害. 然后源点向每一个 \(f_i=1\) 的入点连边,每一个 \(f_i=s\) 的出点向汇点连边. 第二问所有连边流量都为 \(1\).

第三问就在第二问的建图基础上,在 \(1\) 的出点到入点还有如果 \(f_n=s\)\(n\) 的出点到入点连流量 \(inf\) 的边就好了.

Luogu P2469 [SDOI2010] 星际竞速

Hint:拆点,每颗星球恰好访问一次.

这个题为什么要拆点?考虑空间跳跃和高速航行只能有一个来算贡献,但是一个点直接连边这两个贡献可能同时统计到. 所以每个点拆入点和出点,空间跳跃由源点直接连向出点 \((1, w)\) ,高速航行由入点连向出点 \((1, w)\). 源点连向入点 \((1, 0)\),出点连向汇点 \((1, 0)\). 跑最小费用最大流即可,此时最大流一定为 \(n\).

拆贡献

建模的技巧.

Luogu P2053 [SCOI2007] 修车

Hint:把总时间拆成每个人修倒数第 \(k\) 辆车的总等待时间之和.

把第 \(i\) 个人拆成 \(n\) 个点表示修倒数第 \(k\) 辆车的人,每辆车分别向每个拆开的人连 \((1, k\times w)\) 的边. 其他边均为 \((1, 0)\). 源点必须连车而不是连拆开的人,因为车一定要流过但人不一定会用完,否则贡献可能出现不合法的情况.

然后你会发现这很对,因为车一定是用完被拆开倒数第一个人后才用后面的人.

进阶应用

综合性比较强的网络流题目,长期更新.

最小乘积模型

sccpc 2025 A 考到板子,队伍里无一人写题单里的这道题,遂暴毙

咕咕咕

模拟最小割

遇到可以直接用最大流解决的,但是数据范围不够存图或者建图的,可以考虑转化成最小割,根据最小割的性质用 DP 来求. 这种 DP 还是非常考验直觉的,对注意力有较高的要求.

CF724E Goods transportation

Hint:模拟最小割,考虑使每个城市都不连通要割的边.

比较经典的模拟最小割板子. 考虑把所有 \(i\rightarrow j\) 连流量为 \(c\) 的边就可以直接跑最大流,但是 \(O(n^2)\) 条边肯定会超时. 最大流数值上等于最小割而且最小割显然更容易用非网络流算法解决.

考虑网络的形态. 要使图不连通对于每个城市,要么割 \(i\rightarrow t\) 的边,要么把一些小于 \(i\)\(i'\) 连向 \(i\) 的边和 \(s\rightarrow i\) 的边都割了. \(i'\) 连向 \(i\) 的边必须割当且仅当小于 \(i\) 的城市没有割 \(s\rightarrow i'\) 的边,因为这意味着存在一条 \(s\rightarrow i'\rightarrow i\rightarrow t\) 的通路. 于是设 \(f_{i,j}\) 表示考虑了前 \(i\) 条边,割了 \(j\)\(i\rightarrow s\) 的边. 显然有转移方程:

\[f_{i,j}=\min(f_{i-1,j}+c\times j+p_i, f_{i-1,j-1}+s_i) \]

需要滚动数组优化空间. 时间复杂度 \(O(n^2)\).

ABC332G Not Too Many Balls

Hint:模拟最小割,\(n\) 很小考虑用背包预处理,剩下两部分推式子,化成分段函数.

中间 \(n\times m\) 条边肯定不能直接连,考虑模拟最小割. 于是要在 \(s\rightarrow i,i\rightarrow j, j\rightarrow t\) 三部分中选一部分来割. 直接做相当困难,考虑化式子.

考虑枚举左部点边权 \(a_i\in A\) 要割的子集 \(P\),右部点边权 \(b_i\in B\) 要割的子集 \(Q\),中间的边权为 \(i\times j\),有:

\[\begin{aligned} ans&=\min_{P\subseteq A}\min_{Q\subseteq B}\left(\sum_{a_i\in P}a_i+\sum_{a_j\in Q}b_j+\sum_{a_i\notin P}\sum_{a_j\notin Q}ij\right)\\ &=\min_{P\subseteq A}\min_{Q\subseteq B}\left(\sum_{a_i\in P}a_i+\sum_{a_j\in Q}b_j+\sum_{a_i\notin P}i\sum_{a_j\notin Q}j\right)\\ \end{aligned} \]

注意到 \(n\) 远小于 \(m\),不妨枚举 \(k=\sum_{a_i\notin P}i\). 有:

\[\begin{aligned} &=\min_{k=0}^{n(n+1)\over2}\min_{P\subseteq A\wedge\sum_{a_i\notin P}i=k}\min_{Q\subseteq B}\left(\sum_{a_i\in P}a_i+\sum_{a_j\in Q}b_j+\sum_{a_j\notin Q}kj\right)\\ &=\min_{k=0}^{n(n+1)\over2}\min_{P\subseteq A\wedge\sum_{a_i\notin P}i=k}\left(\sum_{a_i\in P}a_i+\min_{Q\subseteq B}\Big(\sum_{a_j\in Q}b_j+\sum_{a_j\notin Q}kj\Big)\right)\\ &=\min_{k=0}^{n(n+1)\over2}\min_{P\subseteq A\wedge\sum_{a_i\notin P}i=k}\left(\sum_{a_i\in P}a_i+\sum_{j=1}^m\min(b_j,kj)\right)\\ \end{aligned} \]

发现右边最小值的取值取决于 \(k'=\lceil {b_j\over j}\rceil\),当 \(k>k'\) 时取 \(b_j\)\(k\le k'\) 时取 \(k_j\),于是考虑维护两个差分数组求得任意 \(k\) 处的取值.

对于任意的 \(k\),要求把剩余不选的 \(i\) 换成 \(a_i\),发现这是一个 \(0/1\) 背包. 预处理出所有剩余容量可以换的 \(\min\Big(\sum_{a_i}\Big)\) 即可.

于是直接枚举 \(k\) 取最小值,复杂度 \(O(n^3+m)\).

模拟费用流

咕咕咕

杂题

Luogu P11531 [THUPC 2025 初赛] 检查站

Hint:分部拆点,最小割.

这个题可以检验最小割学懂没有.

要求选出的最少的分部覆盖所有的 \(1\sim n\) 的铁路,也就是选出最少的分部使得 \(1\sim n\) 不连通,这不就是最小割的定义? 考虑分部单独建点,拆成入点和出点,连一条流量为 \(1\) 的边. 由于每个分部的边都经过分部办公室,加边 \((u,v,r)\) 时,判一下哪个端点火车站是分部的办公室:\(u\) 是办公室就分部出点向 \(v\) 连,\(v\) 是办公室就 \(u\) 向分部出点连. 然后再把办公室和对应分部出点入点连成一个环,这样就能新建的分部不影响图的连通性.

除了分部出点入点之间流量为 \(1\),其他所有边流量都为 \(inf\),因为只能割分部.

(小声:这个题解之前在 vsc 写完没上传被 hfu 拉闸了,所以这是写的第二遍)

Luogu P9879 [EC Final 2021] Check Pattern is Good

Hint:奇偶颜色取反.

逮捕,被击溃了.

Luogu P4003 无限之环

Hint:注意到旋转的水管只会改变一个接口的方向,考虑费用连边刻画旋转次数,跑费用流.

为什么直线型水管不能旋转?一个惊人的注意是非直线型水管的旋转只会改变一个接口的方向,而直线型会改变两个. 考虑拆点,一个水管拆成 \(5\) 个点分别表示中心和上右下左的接口.

考虑用费用表示旋转次数,接下来分类讨论不同类型的水管:

  • 形如 \(↑\ →\ ↓\ ←\) 的,向左向右旋转一次到达箭头方向接口的左、右接口,连 \((1, 1)\) 的边;旋转两次到达箭头方向对面的接口,连 \((1, 2)\) 的边.
  • 形如 \(└\ ┌\ ┐\ ┘\) 的,以 \(└\) 为例,向右旋转一次相当于向上的接口到向下的接口连 \((1, 1)\) 的边,向左旋转一次相当于向右的接口到向左的接口连 \((1, 1)\) 的边,旋转两次相当于前面两条边同时流,不需要额外考虑. 其它情况同理.
  • 形如 \(┴\ ├\ ┬\ ┤\) 的,以 \(┴\) 为例,向右旋转一次相当于向左的接口到向下的接口连 \((1, 1)\) 的边,向左旋转一次相当于向右的接口到向下的接口连 \((1, 1)\) 的边,旋转两次相当于前面两条边同时流,不需要额外考虑. 其它情况同理.

考虑水怎么在水管里流,发现一个点要么由上右下左流来,要么流向上右下左,也就是上右下左和这个点是互斥的. 于是黑白染色,钦定原点向黑点流,黑点向白点流,白点向汇点流. 跑最小费用最大流,就可以得到最小旋转次数.

具体的连边我把我的代码放在这里,仅供参考.

点击查看代码
inline void add(int u, int v, int w, int c) {e[++tot].v = v, e[tot].w = w, e[tot].c = c, e[tot].nxt = head[u], head[u] = tot; return;}
inline void add2(int u, int v, int w, int c, bool col) {if(!col) swap(u, v); add(u, v, w, c), add(v, u, 0, -c); return;}

inline int id0(int x, int y) {return (x - 1) * m + y + 0 * d;} 
inline int id1(int x, int y) {return (x - 1) * m + y + 1 * d;} 
inline int id2(int x, int y) {return (x - 1) * m + y + 2 * d;} 
inline int id3(int x, int y) {return (x - 1) * m + y + 3 * d;} 
inline int id4(int x, int y) {return (x - 1) * m + y + 4 * d;} 
inline bool ok(int x, int y) {return ((x >= 1) && (x <= n) && (y >= 1) && (y <= m));}
inline void add3(int x, int y, bool col) {
	if(ok(x + 1, y) && col) add2(id3(x, y), id1(x + 1, y), 1, 0, col);
	if(ok(x - 1, y) && col) add2(id1(x, y), id3(x - 1, y), 1, 0, col);
	if(ok(x, y + 1) && col) add2(id2(x, y), id4(x, y + 1), 1, 0, col);
	if(ok(x, y - 1) && col) add2(id4(x, y), id2(x, y - 1), 1, 0, col);
	return;
}

int main() {
	ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);

	cin >> n >> m; d = n * m, s = 5 * d + 1, t = s + 1;
	
	int cnt = 0;
	for(int i = 1; i <= n; i++) for(int j = 1, op; j <= m; j++) {
		cin >> op; for(int k = op; k; k >>= 1) cnt += (k & 1);

		bool col = (i + j) & 1; 
        if(col & 1) add2(s, id0(i, j), inff, 0, 1);
		else add2(id0(i, j), t, inff, 0, 1);
		
        if(op & 1) add2(id0(i, j), id1(i, j), 1, 0, col);
        if(op & 2) add2(id0(i, j), id2(i, j), 1, 0, col);
        if(op & 4) add2(id0(i, j), id3(i, j), 1, 0, col);
        if(op & 8) add2(id0(i, j), id4(i, j), 1, 0, col);

        if(op == 1) add2(id1(i, j), id2(i, j), 1, 1, col), add2(id1(i, j), id4(i, j), 1, 1, col), add2(id1(i, j), id3(i, j), 1, 2, col);
        if(op == 2) add2(id2(i, j), id3(i, j), 1, 1, col), add2(id2(i, j), id1(i, j), 1, 1, col), add2(id2(i, j), id4(i, j), 1, 2, col);
        if(op == 3) add2(id1(i, j), id3(i, j), 1, 1, col), add2(id2(i, j), id4(i, j), 1, 1, col);
        if(op == 4) add2(id3(i, j), id2(i, j), 1, 1, col), add2(id3(i, j), id4(i, j), 1, 1, col), add2(id3(i, j), id1(i, j), 1, 2, col);
        if(op == 6) add2(id3(i, j), id1(i, j), 1, 1, col), add2(id2(i, j), id4(i, j), 1, 1, col);
        if(op == 7) add2(id1(i, j), id4(i, j), 1, 1, col), add2(id3(i, j), id4(i, j), 1, 1, col), add2(id2(i, j), id4(i, j), 1, 2, col);
        if(op == 8) add2(id4(i, j), id1(i, j), 1, 1, col), add2(id4(i, j), id3(i, j), 1, 1, col), add2(id4(i, j), id2(i, j), 1, 2, col);
        if(op == 9) add2(id1(i, j), id3(i, j), 1, 1, col), add2(id4(i, j), id2(i, j), 1, 1, col);
        if(op == 11) add2(id2(i, j), id3(i, j), 1, 1, col), add2(id4(i, j), id3(i, j), 1, 1, col), add2(id1(i, j), id3(i, j), 1, 2, col);
        if(op == 12) add2(id3(i, j), id1(i, j), 1, 1, col), add2(id4(i, j), id2(i, j), 1, 1, col);
        if(op == 13) add2(id1(i, j), id2(i, j), 1, 1, col), add2(id3(i, j), id2(i, j), 1, 1, col), add2(id4(i, j), id2(i, j), 1, 2, col);
        if(op == 14) add2(id2(i, j), id1(i, j), 1, 1, col), add2(id4(i, j), id1(i, j), 1, 1, col), add2(id3(i, j), id1(i, j), 1, 2, col);

		add3(i, j, col);
    } mcmf();

	if(cnt == (ans << 1)) cout << cost << endl;
	else cout << -1 << endl;
	return 0;
} 

posted @ 2025-06-15 20:49  Ydoc770  阅读(21)  评论(0)    收藏  举报