通过数据理解 dfs 过程
用两组小数据逐轮模拟匈牙利算法的 dfs,覆盖直接匹配、协商成功、协商失败和连锁腾挪四种情形。
本文回答一个问题:匈牙利算法的 dfs 在真实数据上如何递归、如何回溯,match 数组每一步变成什么样。算法主体见:匈牙利算法;vis 防重复的原理见:没有 vis 会怎样。
约定
模拟用的代码以正文的实现为准,核心部分是:
1
2
3
4
5
6
7
8
9
10
11
bool dfs(int u) {
for (int v : g[u]) {
if (vis[v]) continue;
vis[v] = 1;
if (match[v] == 0 || dfs(match[v])) {
match[v] = u;
return true;
}
}
return false;
}
match[v]:右部点当前匹配的左部点, 0表示空闲。vis[v]:本轮搜索是否问过右部点,每轮开始前清空。 - 邻接表按输入顺序枚举。枚举顺序影响最后得到哪一组最大匹配,但不影响匹配数。
- 左部点记作
,右部点记作 。
例一:三种基本情况
左部点
| 左部点 | 可连的右部点 |
|---|---|
graph LR
U1((u1)) -.-> V1((v1))
U1 -.-> V2((v2))
U2((u2)) -.-> V1
U3((u3)) -.-> V2
这份数据覆盖三种情形:直接匹配、冲突后协商成功、冲突后协商失败。
第 1 轮:dfs(u1) 直接匹配
vis 清空后执行 dfs(u1):
- 枚举
: vis[v1]未置位,先置位,再检查match[v1] = 0。 空闲,令 match[v1] = u1,返回 true。
本轮结束,match[v1] = u1,匹配数
第 2 轮:dfs(u2) 协商成功
vis 清空后执行 dfs(u2):
- 枚举
:置 vis[v1];match[v1] = u1非空,递归调用dfs(u1),请挪位置。 - 进入
dfs(u1):- 枚举
: vis[v1]本轮已置位,跳过。这一步正是 vis 的作用:是当前争议点,绕回去只会死循环。 - 枚举
:置 vis[v2];match[v2] = 0,令match[v2] = u1,返回 true。
- 枚举
- 回到
dfs(u2):递归返回 true,说明已挪到 ,于是令 match[v1] = u2,返回 true。
本轮结束,match[v1] = u2、match[v2] = u1,匹配数
第 3 轮:dfs(u3) 协商失败
vis 清空后执行 dfs(u3):
- 枚举
:置 vis[v2];match[v2] = u1,递归dfs(u1)。 - 进入
dfs(u1):- 枚举
:置 vis[v1];match[v1] = u2,递归dfs(u2)。 - 进入
dfs(u2):枚举, vis[v1]已置位,跳过;没有其他边,返回 false。 - 回到
dfs(u1):递归失败,继续枚举; vis[v2]已置位,跳过。没有其他边,返回 false。
- 枚举
- 回到
dfs(u3):递归失败,没有其他边,返回 false。
本轮失败,match 一个字都没变。match[v] = u 只写在成功分支里,所以递归链上任何一层失败,整条链都不会留下改动。
三轮的结果:
| 轮次 | 发起的左部点 | 结果 | 本轮后的 match |
|---|---|---|---|
| 1 | 成功 | v1: u1 |
|
| 2 | 成功, |
v1: u2,v2: u1 |
|
| 3 | 失败 | 不变 |
匹配数为
例二:连锁腾挪
再看一个递归更深的例子。边为
graph LR
U1((u1)) -.-> V1((v1))
U1 -.-> V2((v2))
U2((u2)) -.-> V1
U2 -.-> V3((v3))
U3((u3)) -.-> V2
U3 -.-> V4((v4))
U4((u4)) -.-> V1
前三轮的过程与例一同类,只列结果:
| 轮次 | 发起的左部点 | 结果 | 本轮后的 match |
|---|---|---|---|
| 1 | 成功 | v1: u1 |
|
| 2 | 成功, |
v1: u2,v2: u1 |
|
| 3 | 成功,连锁两层 | v1: u1,v2: u3,v3: u2 |
第 3 轮值得留意:
第 4 轮:dfs(u4) 连锁腾挪
此时 match 是 v1: u1、v2: u3、v3: u2、v4: 空。vis 清空后执行 dfs(u4):
只有 一条边:置 vis[v1];match[v1] = u1,递归dfs(u1)。- 进入
dfs(u1)(第 2 层):- 枚举
: vis[v1]已置位,跳过。 - 枚举
:置 vis[v2];match[v2] = u3,递归dfs(u3)。
- 枚举
- 进入
dfs(u3)(第 3 层):- 枚举
: vis[v2]已置位,跳过。 - 枚举
:置 vis[v4];match[v4] = 0,令match[v4] = u3,返回 true。整条链找到出口。
- 枚举
- 回到第 2 层
dfs(u1):递归成功,令match[v2] = u1,返回 true。 - 回到
dfs(u4):递归成功,令match[v1] = u4,返回 true。
最终 match 是 v1: u4、v2: u1、v3: u2、v4: u3,匹配数
回看这条递归链:
起点 match[v4] = u3、match[v2] = u1、match[v1] = u4)正是沿着这条路翻转匹配边与非匹配边,匹配数因此增加
小结
dfs的递归链就是“我抢你的位置、你去抢他的位置”的协商链,它在图上就是一条交替路。- 出口是空闲右部点。找到出口后,
match的改写沿回溯从内向外逐层进行,每层把自己盯上的右部点登记为当前左部点。 - 任何一层失败,整条链立即返回 false 且不留下任何改动,之前坐好的人全部原位不动。
vis保证一轮里每个右部点只被问一次,这既堵死回头路,也让单轮复杂度保持在。