← 返回 Blog

LEETCODE 1349 · 推导式讲解

从座位到水流

本文从座位约束出发,依次比较直接枚举、状态压缩与冲突图建模, 再根据冲突图的二分性质建立最大匹配和网络流模型。

分析对象

如何选择最多的无冲突座位

推导目标

从冲突关系建立二分图与最大流模型

路线

01定义选择
02记录冲突
03直接求解
04寻找结构
05认识二分
06理解覆盖
07匹配定理
08构造水流
09检查边界
主线座位约束 → 冲突图 → 二分图 → 点覆盖 → 最大匹配 → 网络流
支线列数很小时,也可直接使用逐行状态压缩 DP
01

问题定义

从选择开始

题目要求确定一个集合:集合中的每个元素是一个好座位, 集合规模应尽量大,并且不能包含任何一对会作弊的座位。以下使用一个仅有四个好座位的教室 展示合法选择与冲突选择。

实验座位选择演示
列 0列 1列 2
#坏座
#坏座
尚未选择座位。点击可添加或撤销座位。
观察建模所需信息

选择 A 后,后续判断只需确认 B、E 的可选状态。 座位的颜色、面积、离讲台多远,都与判断无关。

A 与 B 不能共存A 与 E 不能共存

因而问题的最小信息单位不是“方向”,而是一对不能同时出现的选择

02

选一种语言

换成冲突

一种实现是在每次选座时重新检查六个方向,另一种实现是保存一个 “谁和谁冲突”的二维表。但二者都在表达同一件事:两个对象之间存在一种关系。 图正是专门保存“对象 + 两两关系”的结构。

方案 A

枚举全部集合

用二进制位表示座位是否被选,再逐个检查集合是否合法。

方案 B

回溯并即时检查

按座位递归执行“选或不选”,一旦出现方向冲突就提前停止分支。

方案 C

预存冲突关系

先计算不能共存的座位对,再用图保存对象与两两关系。

按需展开方案 A、B 具体怎样实现?包含伪代码、剪枝过程和“重复检查”的翻转演示
A
基本方案

枚举全部集合

  1. 给 g 个好座位编号 0 到 g−1。
  2. 用一个 g 位二进制数表示座位集合。
  3. 对集合中的每个座位检查六个冲突方向。
  4. 集合合法时,用选中位数更新答案。
best = 0
for mask in [0, 2^g):
    valid = true
    for seat in mask:
        if 六个方向中存在已选座位:
            valid = false
    if valid:
        best = max(best, popcount(mask))

解决:能够得到正确答案。
痛点:需要检查 2ᵍ 个集合,规模增长时计算量迅速失控。

B
第一次优化

回溯并即时检查

  1. 递归处理第 i 个好座位。
  2. 分为“不选”和“选”两个分支。
  3. 选择前检查六个方向;冲突时立即剪枝。
  4. 到达末尾时统计当前人数。
search(i, chosen):
    if i == g:
        updateAnswer(chosen)
        return
    search(i + 1, chosen)      // 不选
    if seat[i] 与 chosen 不冲突:
        search(i + 1, chosen + seat[i])

解决:非法方案不必继续展开。
剩余痛点:最坏情况仍为 2ᵍ,而且

冲突图

边不是路线,是“禁止同时选择”

A—B 表示二者不能一起坐人。边没有方向,因为不管是谁看见谁,最终限制都是 “这一对不能同时出现”。

  • A—B、E—F:左右冲突
  • A—E、B—F:斜向冲突
  • B 与 E:正前后,不冲突,所以不连边
术语

独立集

图中挑出一些点,若这些点之间一条边也没有,它们就是一个独立集。 在本题里,它就是一份合法座位安排;最大的那份叫最大独立集

03

基础方案

直接求

在尚未利用图结构时,最直接的算法是对每个座位做一次二选一: “选”或者“不选”。选的时候检查它是否与已选点冲突,最后记录人数最大值。

A?
选 A不选 A
选 B弃 B选 B弃 B

接着还要为 E、F 继续分支。冲突检查能剪掉一些枝条,却没有改变最坏情况下的指数增长。

1,048,576

种可能的座位集合,也就是 2^20

4 个 → 1620 个 → 1,048,57664 个 → 18,446,744,073,709,551,616
方案能不能做继续优化的原因
枚举所有集合小数据可以O(2ᵛ),座位一多就爆炸
回溯 + 剪枝实际会快一些最坏情况仍是指数级
逐行状态压缩本题完全可行复杂度依赖列数小;这是另一条优秀路线
研究冲突图还不知道若图有特殊结构,可能从指数级降到多项式
支线详解逐行状态压缩 DP 怎样复用上一行?位掩码、合法状态、两行兼容表与一次完整转移

另一条完整路线

逐行状态压缩 DP

枚举全部座位需要 2ᵐⁿ 个集合。题目中每行只有 n≤8 个座位, 因此可以只记录“当前行怎样坐”,并复用上一行已经计算出的最优结果。 这项优化针对的是大量集合拥有相同“行状态”的重复计算。

usable[row]

表示第 row 行可用座位的 n 位二进制掩码。与第 c 列对应的位为 1,表示 (row,c) 是可用座位 .;对应位为 0,表示该位置是坏座位#。为使位串与座位图从左到右对齐,以下示例规定最高位对应第 0 列。

第 0 行
AB#
usable = 110合法:000、100、010
第 1 行
#EF
usable = 011合法:000、010、001
1不能坐坏座位cur & ~usable[row] == 0~usable 的 1 位对应坏座位;与 cur 相交为 0,说明没有选择坏座位。
2同一行不能相邻cur & (cur << 1) == 0
3相邻行不能斜对角cur & (prev << 1) == 0cur & (prev >> 1) == 0
状态dp[row][cur]

处理到当前行,并使用 cur 排列时的最多学生数。

转移dp[row][cur] = max(dp[row−1][prev]) + popcount(cur)

只从与 cur 没有斜向冲突的 prev 转移。

把“两行两两检查”拆开

检查的是两种“整行坐法”能否上下拼接

上一行的每一种合法状态记为 prev,当前行的每一种合法状态记为cur。从两个集合中各取一个,检查它们之间有没有斜向冲突。 不是把两行中的每个座位重新两两比较。

上一行可选状态 prev000 100(A) 010(B)
逐个配对
当前行可选状态 cur000 010(E) 001(F)
每个格子代表一对完整行状态
prev \ cur000010(E)001(F)
000
100(A)× A—E
010(B)× B—F
为什么 A + E 不行?

A 在 E 的左上方,学生可以互相看到,所以这一对状态不能连接。

为什么 B + E 可以?

B 与 E 正前后相邻;题目明确说看不到正前方或正后方,因此允许。

为什么叫“两两检查”?

3 个 prev 分别尝试搭配 3 个 cur,共检查 3×3=9 个状态对。

例如计算 dp[1][010(E)]

prev=100(A) 冲突,排除prev=000 已有 0 人prev=010(B) 已有 1 人,取最大
dp[1][010] = max(0, 1) + popcount(010) = 2对应合法安排:上一行坐 B,当前行坐 E。
复杂度:O(m · 4ⁿ)

每行最多有 2ⁿ 种状态;上一行至多 2ⁿ 种,当前行也至多 2ⁿ 种, 组合起来是 2ⁿ×2ⁿ=4ⁿ。n≤8 时,每相邻两行最多检查 256×256 个“整行状态对”,因此该方案能够直接解决本题。

04

二分结构验证

找结构

前面已经把每个好座位表示为一个点,把每一对“不能同时坐”的座位表示为一条冲突边。 因此,一份合法座位安排就是冲突图中的独立集。建模完成后,问题变成了 “如何求这张图的最大独立集”。一般图上的最大独立集仍然难以高效求解, 所以这一章不急着套算法,而是回到题目的四种冲突方向,寻找它们共同遵守的规律。

01

现在卡在哪里

换成冲突图,不等于已经变简单

冲突图只是把题意保存得更统一。若它是一张任意的普通图, “每个点选或不选”仍可能产生 2ᵛ 个状态,最大独立集依然难求。

一般图最大独立集 → 仍可能指数级
02

准备寻找什么

用简单标签压缩“谁会与谁冲突”

若能给座位贴一个很简单的标签,并让每条冲突边都连接不同标签, 那么同标签内部便完全没有冲突,整张图的关系只需在标签之间检查。 这还不是答案,但它会把任意连接压缩成更规整的连接模式。

目标不是先规定“两组”,而是先寻找能预测每条边的稳定标签
03

规律自然产生分组

四种方向共同指向列奇偶

左、右、左上、右上的列变化全部是 ±1;加减 1 又必定让奇偶性翻转。 因而标签自然取 col % 2,它恰好只有 0、1 两种值。 两组不是预先指定的目标,而是这个稳定规律产生的结果。

Δcol = ±1 → col % 2 必定从 0 ↔ 1
04

验证成功能得到什么

先验证所有边,再给结构命名

若所有冲突边都从偶数列连到奇数列,组内就没有冲突边。到这一步我们只确认了事实; 这种“顶点能分成两组且边只跨组”的图,下一章再正式命名并研究。

发现规律 → 提出分组 → 审计每类边 → 确认结构
扩展比较什么是棋盘色?为什么它在这里会失败?展开查看 (row + col) % 2 的染色方式

棋盘染色

上下左右换色,斜向保持同色

像国际象棋棋盘一样,让上下左右相邻的格子黑白交替。 坐标为 (row, col) 的格子颜色由 (row + col) 的奇偶决定。

斜向移动会同时改变行和列,行列和改变 0 或 2,因此斜向冲突的两个座位 仍会落在同一颜色组。这正是棋盘色在本题失败的原因。

0,00
0,11
0,20
1,01
1,10
1,21
2,00
2,11
2,20

是否能分成两组仍需验证,不能直接假定成立。下面依次检查按行、 棋盘色和按列三种自然染色,并将留在组内的冲突边标红。

本轮只检查一个条件每一条冲突边都会改变行号奇偶吗?

不会。左右冲突的两个座位行号相同,Δrow = 0,因此仍在同一组。

第 0 组
第 1 组
组内冲突:2 条

同一行的座位被染成同色,因此左右相邻的冲突边会留在组内。

失败:A—B、E—F 都留在同一组。

四种冲突方向Δcol 全部为 ±1列号奇偶必定翻转每条冲突边都跨组
扩展证明审计 A—B、A—E、B—F、E—F 四条冲突边同时说明为什么结论适用于更大的教室

最终证明

不能只看示意图:逐条审计所有冲突边

使用 group = col % 2 后,偶数列进入第 0 组,奇数列进入第 1 组。 下面确认示例中的每一条边都跨组。

冲突边列号变化奇偶变化结论
A—B0 → 1偶 → 奇跨组 ✓
A—E0 → 1偶 → 奇跨组 ✓
B—F1 → 2奇 → 偶跨组 ✓
E—F1 → 2奇 → 偶跨组 ✓
为什么更大的教室也成立?

题目的四种作弊方向分别是(0,-1)(0,+1)(-1,-1)(-1,+1)。 它们的列变化全部是 ±1,所以任意冲突边都会翻转列号奇偶。 证明依赖的是全部冲突方向的共同性质,而不是这个四座位示例碰巧成功。

验证事实所有冲突边都跨越列奇偶两组
因此
图的类型冲突图是二分图
先暂停解题
下一章完整理解这种新结构能带来什么
05

图论基础概念

二分图

上一章不是先猜“二分图”,而是先从 Δcol = ±1 推出了列奇偶分组。 现在才暂停解题,研究这种结构本身。 图经常描述“两类对象之间的关系”:候选人与岗位、飞行员与飞机、用户与商品。 如果关系只会发生在两类对象之间,而不会发生在同一类内部,就应该把这条共同结构保留下来。二分图正是为这种“两岸关系”准备的模型。

天然分成两类对象的角色本来就不同

候选人只连接岗位,岗位只连接候选人。边表示“可以录用”,同类对象之间不需要连边。

候选人 ↔ 岗位
从同类对象中发现两类对象相同,但关系会翻转某个标签

座位都是同一种对象,但每次冲突都会让列号奇偶翻转,因此可以按偶数列、奇数列分岸。

偶数列 ↔ 奇数列

正式定义

不是“画成两列”,而是顶点真的能够这样划分

对图 G = (V,E),如果能找到两个互不相交的集合LR,使全部顶点恰好属于其中一侧, 而每条边都在左右两侧各取一个端点,那么 G 是二分图。

V = L ∪ R全部顶点都被分进去L ∩ R = ∅一个顶点不能同时属于两侧E ⊆ L × R每条边只能跨越两侧

回到座位图验证定义

L 放偶数列,R 放奇数列

图的边仍是 A—B、A—E、F—B、F—E,只是把同组节点搬到一起。 A、F 之间没有边,B、E 之间也没有边;四条边全部跨越两侧。

左右只是名称,可以整体交换;真正不能改变的是“组内无边”。

三种等价语言

分组、二染色、没有奇环,说的是同一件事

做题时不一定直接看见 L、R。更常见的方法是尝试二染色; 一旦染色失败,沿冲突路径会还原出一个奇数长度的环。

二分图的三种等价刻画:边只跨越两个部分、图可以二染色、图中不存在奇数长度的环
偶环绕一圈会回到原颜色;奇环绕一圈却要求起点同时拥有两种颜色,因此奇环是二分性的障碍。
本文只需要这一点组内没有冲突,所有冲突都必须跨越 L 与 R

这并不代表可以把两组分别求解;跨组边仍然把两侧绑在一起。 真正的收益是关系只能在两岸之间交替,后续的匹配与点覆盖定理因此有了成立前提。

扩展性质二分图还具备哪些常见性质?不影响座位题主线,可在建立基本定义后再读
性质 01每一侧本身都是独立集

L 内部、R 内部都没有边。但这不等于可以忽略另一侧:跨组边仍然限制两侧能否同时选择。

性质 02树、森林、偶环和普通网格都是二分图

树没有环;偶环可以交替染色。任何子图也会继续保持二分性。

性质 03连通图的二染色只差一次整体交换

起点染成 0 或 1 会得到两份互换的答案;非连通图的每个连通分量都可以独立交换两色。

性质 04完全二分图 Kₘ,ₙ 把所有跨组边都连满

“二分”只规定哪些边允许出现,并不要求全部出现;全部 m×n 条跨组边都存在时才叫完全二分图。

通用判定不知道分组时,怎样用 BFS / DFS 找出 L、R?逐个连通分量二染色,时间复杂度 O(V + E)
  1. 找到一个尚未染色的点,把它染成 0,作为新连通分量的起点。
  2. 用 BFS 或 DFS 遍历边;每遇到未染色邻点,就染成当前点的相反颜色。
  3. 若一条边的两端已经是同色,立即失败;这条边与搜索树路径共同形成奇环。
  4. 一个分量完成后继续寻找未染色点,直到所有顶点都被处理。
for each uncolored start:
    color[start] = 0
    push(start)
    while queue not empty:
        u = pop()
        for v in neighbors[u]:
            if color[v] is unset:
                color[v] = color[u] xor 1
                push(v)
            else if color[v] == color[u]:
                return false
return true

每个顶点至多入队一次,每条边检查常数次,所以总复杂度是O(|V| + |E|)。本题不必实际跑染色,因为“列号奇偶”已经直接给出了颜色。

扩展地图二分结构还能优化哪些问题?九类问题逐项查看;本文主线只使用最大独立集、点覆盖与匹配

为什么值得专门命名

同一个简单限制,会让一批问题出现更强的结论

二分图没有组内边、没有奇环,路径会稳定地在两侧交替。 这使有些问题直接变成常数级结论,有些问题能统一归约到最大匹配。 点击下面的概念,只显示一项详细解释。

直接由结构得到由匹配关系推出把现实问题建成两岸分配
下一章详解

最小点覆盖

τ(G) = ν(G)
问题在问什么

选择最少的点,使每条边至少碰到一个被选点。

二分图里怎样解决

在二分图中,柯尼希定理保证最小点覆盖数等于最大匹配数;一般图没有这个等式。

为什么结构有帮助

匹配给出覆盖所需点数的下界,而二分结构保证这个下界一定能够达到。

对应场景

撤下尽量少的座位消除全部作弊冲突,正是本文的问题。

资料来源本章的定义、性质和应用依据OI Wiki、MIT、Cornell、NetworkX 与 MathWorld
回到本文二分结构只说明所有冲突都发生在两组之间,并没有直接给出座位方案。下一章先回到 A、B、E、F 四个座位,用“实际撤下谁”理解问题怎样继续转化。
06

转化视角

撤下谁

先暂时忘掉“点覆盖”这个词。假设 A、B、E、F 四个好座位一开始全部坐了学生, 此时存在 A—B、A—E、B—F、E—F 四组作弊冲突。 我们不直接猜最终该保留谁,只做一个更具体的动作:撤下一名学生,看看哪些冲突随之消失

真实撤座实验A、B、E、F 全部保留还剩 4 条没有消除的冲突
红色点:已撤下的座位绿色边:冲突已消除红色边:冲突仍存在
四个座位都保留时,四条冲突边都还存在,这当然不是合法安排。

做完动作,再给它命名

被撤下的 {B,E},在图论中就是一个点覆盖

“点”是因为我们选择的是座位点 B、E,不是选择冲突边;“覆盖”是因为每一条冲突边都至少碰到一个被撤下的端点。 例如 A—B 碰到 B,A—E 碰到 E,所以这两条冲突都会被破坏。

图上的点一个真实的好座位

A、B、E、F 是四个点;被选入点覆盖的点,在本题里表示准备撤下的座位。

一条边被覆盖至少撤下它的一个端点

对 A—B,只撤 A、只撤 B、或两者都撤都能消除冲突;A、B 都保留才会出问题。

整张图被覆盖每条冲突边都已经碰到撤下点

只要漏掉一条边,剩下的学生中就仍有一对会作弊,因此还不是合法安排。

原问题最多保留多少座位?保留下来的点之间不能有冲突边
换个视角
等价动作最少撤下多少座位?每条冲突边至少撤下一个端点
数量关系
同一个方案的两面保留数 = 好座位总数 − 撤下数4 − 2 = 2
为什么撤下 1 个不够?A—B 与 E—F 是两条互不相干的冲突

这两条边没有公共端点。无论只撤下哪一个座位,都最多消除其中一条,另一条仍然存在。 所以至少要撤下 2 个座位。

为什么撤下 2 个足够?撤下 B、E 后四条冲突全部消失

“至少要 2 个”和“确实能用 2 个做到”同时成立,因此 {B,E}是一个最小点覆盖{A,F} 也是另一份最优答案。

扩展抽象把刚才的撤座过程写成集合与公式先理解具体例子,再看 V、E、C 与补集关系
V = {A,B,E,F}:全部好座位C = {B,E}:被撤下的座位V \ C = {A,F}:最终保留的座位每条 {u,v} ∈ E,都有 u ∈ C 或 v ∈ C

最小化 |C| 就是在尽量少撤座;总座位数 |V| 固定,所以 |C| 越小, 保留下来的 |V| − |C| 就越大。

集合关系图

撤下区与保留区恰好互补

红色集合 C 负责碰到每条冲突边;剩余的 V \ C 内部便不可能再有冲突边,因此它是独立集。

全集 V 被分为点覆盖 C 和补集 V 减 C;所有冲突边都碰到 C,而补集内部没有冲突边
C 是点覆盖,当且仅当 V \ C 是独立集;两者描述的是同一份座位方案。
本章结论原题要“最多保留”;点覆盖改问“最少撤下”。只要每条冲突边都碰到一个撤下点,剩余座位就一定无冲突。现在才需要继续问:怎样高效找到最小点覆盖?
07

柯尼希定理

从覆盖到匹配

点覆盖仍然在“从很多点中找最少的一组”。先寻找一个容易验证的下界: 如果挑出若干条互不共享端点的冲突边,那么每条边都必须由不同的撤下座位负责。 挑出的这组边越多,对“至少要撤下多少点”的约束就越强——这正是引入匹配的原因。

匹配 M选择一组互不共享端点的边

A—B 与 F—E 可以同时选,因为四个端点互不重复;A—B 与 A—E 不能同时选,因为它们都占用了 A。

最大匹配在所有匹配中,边数最多的那一份

匹配选择的是“冲突边”,不是最终保留的座位。它在这里用来证明至少需要撤下多少点。

若匹配中有 k 条互不共享端点的边,点覆盖至少需要 k 个点,所以任何图都有最大匹配数 ≤ 最小点覆盖数。二分图的特殊之处,是这个下界一定能恰好达到。

术语对照区分独立集、点覆盖、匹配和最大流最终选择的是座位点,匹配选择的是冲突边
独立集准备保留的座位点

点与点之间没有冲突边;这才是最终的学生安排。

点覆盖准备撤下的座位点

每条冲突边至少碰到一个被撤下的点,与独立集互为补集。

匹配互不共享端点的冲突边

它不是座位方案,而是用来计算“至少需要撤下多少点”。

最大流求最大匹配的算法工具

流量不是学生人数;一单位流代表一条匹配边。

柯尼希定理图解

同一个“2”,在二分图中可以从边落到点

左、中两幅使用同一张二分冲突图;右侧三角形则展示一般图为什么不能保证等号。

三幅图对比:二分图最大匹配为二、最小点覆盖为二;三角形最大匹配为一而最小点覆盖为二
黄色粗边表示匹配边,红色节点表示点覆盖选择的节点。 “匹配”挑边,“点覆盖”挑点,两者不是同一套选择。
展开证明:为什么任何图只有 ≤,二分图却能取等号?
任何图都成立最大匹配数 ≤ 最小点覆盖数

匹配边之间没有公共端点。每条匹配边都必须被覆盖, 一个覆盖点不能同时处理两条端点完全分离的匹配边,所以至少需要同样多的点。

二分图额外成立最大匹配数 = 最小点覆盖数

所有边只跨越左右两岸后,可以利用交替路径, 从最大匹配构造出数量完全相同的点覆盖。

一般图反例三角形:1 < 2

三角形最多只能选一条匹配边,却至少需要两个点覆盖三条边,因此没有等号保证。

设最大匹配为 M。从左组所有未匹配点出发,只按照“左到右走非匹配边、 右到左走匹配边”的规则寻找交替路径,把能够到达的点记为 Z。

最终选择“左组中不在 Z 的点”与“右组中位于 Z 的点”,即(L − Z) ∪ (R ∩ Z)。这个集合会覆盖所有边,并且恰好从每条匹配边 取一个端点,因此点数等于匹配数。这一步依赖所有边只能在左右两组之间连接。

最大学生数 = 好座位数 − 二分图最大匹配数
下一步数学关系已经闭合;现在只需真正算出最大匹配。下一章把匹配翻译成可执行的网络流。
08

流网络建模

变成流

上一章已经把答案缩减为“求二分图最大匹配”,但定理本身还不是执行过程。 我们需要一种机制同时保证:每条候选冲突边可以被选择,而每个座位最多参加一次匹配。 网络流用容量统一表达这些限制,因此成为这里的计算工具。

起点与终点S 发出,T 接收

目标是让从源点 S 到汇点 T 的总流量尽可能大。

有向边流只能沿箭头前进

方向在这里组织计算步骤,不一定是原问题中的真实方向。

容量限制0 ≤ f(e) ≤ c(e)

边上的当前流量不能超过容量;容量 1 表示最多使用一次。

流量守恒中间点流入 = 流出

一单位流必须走完 S 到 T 的完整路径,不能在座位点凭空消失。

第一张图:描述题目

冲突图点是座位无向边表示两个座位不能同时保留
改造成辅助网络 →

第二张图:执行算法

流网络保留原来的座位点,并增加人工节点 S、T有向边负责限制一个座位最多参加一次匹配
扩展机制算法怎样继续找路,又怎样撤回旧选择?增广路与残量网络是实现细节,不影响先理解建模
增广路在当前剩余容量中,还能从 S 走到 T 的完整路径;找到一条就能继续增加总流量。
残量网络记录每条边还能走多少,并用反向边允许撤回旧流量,从而改正早先不理想的选择。

为使“一单位流”恰好对应“一条匹配边”,需要添加三种不同含义的边:

1

S → 每个左组座位,容量 1

人工入口边,不代表冲突;保证一个左侧座位最多参与一次匹配。
2

左组 → 有冲突的右组,容量 1

由原来的冲突边改成有向边;只有这一段仍然对应真实冲突。
3

每个右组座位 → T,容量 1

人工出口边,不代表冲突;保证一个右侧座位最多参与一次匹配。
增广实验当前流量:0 单位已找到 0 条不共享端点的匹配边
尚未找到匹配边。下一步寻找一条 S → 左座位 → 右座位 → T 的完整路径。
最大流 = 2两条完整的 S→T 路径S→A→B→T、S→F→E→T
同义
最大匹配 = 2两条端点不重复的冲突边A—B 与 F—E
二分图定理
最小点覆盖 = 2至少撤下两个座位例如撤下 B、E
回到原题
最大学生数 = 2好座位 4 − 最少撤下 2最终保留 A、F,或保留 B、E
Dinic 的执行过程与完整 C++

Dinic 反复做两件事:BFS 给节点分层,只保留向下一层前进的边; DFS 在分层图里尽量推流。推不动后重新 BFS,直到 T 不可达。 反向边负责“撤回并改配”,这正是贪心匹配可能选错时的纠错机制。

class Solution {
    struct Edge { int to, rev, cap; };
    vector<vector<Edge>> g;
    vector<int> level, cur;

    void addEdge(int u, int v, int cap) {
        g[u].push_back({v, (int)g[v].size(), cap});
        g[v].push_back({u, (int)g[u].size() - 1, 0});
    }

    bool bfs(int s, int t) {
        fill(level.begin(), level.end(), -1);
        queue<int> q;
        level[s] = 0;
        q.push(s);
        while (!q.empty()) {
            int u = q.front(); q.pop();
            for (auto &e : g[u]) {
                if (e.cap && level[e.to] == -1) {
                    level[e.to] = level[u] + 1;
                    q.push(e.to);
                }
            }
        }
        return level[t] != -1;
    }

    int dfs(int u, int t, int f) {
        if (u == t) return f;
        for (int &i = cur[u]; i < (int)g[u].size(); ++i) {
            Edge &e = g[u][i];
            if (e.cap && level[e.to] == level[u] + 1) {
                int pushed = dfs(e.to, t, min(f, e.cap));
                if (pushed) {
                    e.cap -= pushed;
                    g[e.to][e.rev].cap += pushed;
                    return pushed;
                }
            }
        }
        return 0;
    }

    int maxFlow(int s, int t) {
        int result = 0;
        while (bfs(s, t)) {
            fill(cur.begin(), cur.end(), 0);
            while (int pushed = dfs(s, t, INT_MAX))
                result += pushed;
        }
        return result;
    }

public:
    int maxStudents(vector<vector<char>>& seats) {
        int m = seats.size(), n = seats[0].size();
        int S = m * n, T = S + 1;
        g.assign(m * n + 2, {});
        level.resize(m * n + 2);
        cur.resize(m * n + 2);

        int good = 0;
        auto id = [n](int r, int c) { return r * n + c; };
        int dr[6] = {0, 0, -1, -1, 1, 1};
        int dc[6] = {-1, 1, -1, 1, -1, 1};

        for (int r = 0; r < m; ++r) {
            for (int c = 0; c < n; ++c) {
                if (seats[r][c] == '#') continue;
                ++good;
                int u = id(r, c);

                if (c % 2 == 0) {
                    addEdge(S, u, 1);
                    for (int k = 0; k < 6; ++k) {
                        int nr = r + dr[k], nc = c + dc[k];
                        if (nr >= 0 && nr < m && nc >= 0 && nc < n
                            && seats[nr][nc] == '.') {
                            addEdge(u, id(nr, nc), 1);
                        }
                    }
                } else {
                    addEdge(u, T, 1);
                }
            }
        }
        return good - maxFlow(S, T);
    }
};
09

适用条件

何时能用

本题走的是“冲突图 → 二分图 → 匹配 → 网络流”,但这只是网络流的一条入口, 不是所有网络流题都先找二分图。判断新题时,先看自己正在解决哪一种困难,再选择对应路线。

本文路线两类对象的一对一选择二分图 → 最大匹配 → 最大流

二分性是匹配定理和三层流网络成立的前提。

一般流路线资源沿网络受容量限制地传递容量网络 → 最大流 / 最小割

运输、通信、切割不要求原图是二分图。

01对象定义

明确一个点代表人、座位、任务、机器还是格点。

02关系定义

不能共存、可以匹配、需要切断,决定边的语义。

03二分性验证 · 匹配路线

若要套二分匹配,需进行二染色验证;一般运输网络则跳过此项。

04容量含义

每人一次用 1,多份名额用 k,代价另需最小费用流。

05流量对应关系

每单位流必须能还原成一个合法决策,反之也能构造成流。

列奇偶失效,但可能换染色

如果加入前后冲突

上下座位列差为 0,会落在同一列奇偶组。不过若只有上下左右冲突, 可改用 (row + col) % 2 的棋盘染色。

整个二分图路线失效

如果八个方向都冲突

三个相邻座位可以形成三角形。三角形无法只用两种颜色染开, 最大匹配也不再等于最小点覆盖。

扩展迁移把同一套建模方法迁移到其他领域招聘、排班、运输与最小割;一次展开一个具体问题

同一工具的其他题目

先辨认“一单位流”代表什么

下面四类问题都能画成流网络,但边的含义并不相同。展开卡片可查看题意、 建模方式和一个具体答案。

招聘候选人与岗位的一对一最大匹配一单位流 = 录用一个人并分配一个岗位

题目

有 3 名候选人和 3 个岗位。每个人只能入职一个岗位,每个岗位只有 1 个名额,而且只能把候选人分配到他胜任的岗位。求最多能录用多少人。

建模

  1. S → 每位候选人,容量 1:一个人最多被录用一次。
  2. 候选人 → 能胜任的岗位,容量 1:只允许合法分配。
  3. 每个岗位 → T,容量 1:一个岗位最多录用一人。

答案

最大流为 3。一种方案是 A→后端、B→Android、C→测试,因此三人都能录用。

胜任关系

候选人Android后端测试
A
B
C
S→1候选人→1岗位→1T
若某岗位有 k 个名额,只需把“岗位 → T”的容量改为 k。
排班员工与班次的容量分配一单位流 = 一名员工承担一个班次

题目

已知每名员工可以上哪些班次、每人最多工作几班,以及每个班次需要几人, 求最多能填补多少个值班名额。

解决方案

S→员工的容量设为该员工最多可上班次数;员工→可用班次连边; 班次→T 的容量设为该班次需求人数。最大流就是最多填补的名额数。 若还要尽量满足偏好,可把偏好转成费用,改用最小费用最大流。

运输边容量限制下的最大运输量一单位流 = 一单位货物或数据

题目

仓库到门店之间有若干中转站和道路,每条道路每天最多运输一定数量, 求一天最多能从仓库送多少货到门店。

解决方案

仓库直接作为 S,门店作为 T,道路就是带容量的边,求最大流即可。 若限制的是中转站吞吐量,可把一个站拆成入点和出点,并在两点之间连一条容量边。

切割最小代价切断源点与汇点最大流值 = 最小割容量

题目

每条通信线路都有切断代价,希望用最小总代价,使攻击源 S 再也无法把信息传到 核心服务器 T。

解决方案

把切断代价作为边容量并求最大流。最大流结束后,在残量网络中从 S 仍可到达的点属于左侧,其余点属于右侧;原图中从左侧跨到右侧的边组成最小割, 其容量和等于最大流。

核心方法

明确“选择”的对象,
再由约束结构确定算法。

回到开头 ↑