匈牙利算法相关证明
本文将在读者已经掌握匈牙利算法写法的情况下,解释说明下列问题的原因,以帮助读者更好理解匈牙利算法的合理性:
- 为什么搜索当前点能否加入最大匹配时不需要考虑原有点的匹配方式/连线方式
- 为什么最外层只需要遍历一遍左部点
- 为什么搜索时是否访问过左部点的 vis 数组在回溯时不需要置零/为什么每个左部点在搜索某个点的最大匹配时只需要访问一次
一、为什么搜索当前点能否加入最大匹配时不需要考虑原有点的匹配方式/连线方式
首先,匈牙利算法是在目前匹配的状态下直接尝试寻找新加入点的增广路,那原有点的匹配方式不会影响我们能否找到新加入点的增广路吗?答案是不会。因为一个点能否找到增广路与当前点的匹配方式无关,而只与已经匹配好的点集有关。
不妨设已经匹配好了 2 n 2n 2n 个点,点集中有 { u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn },当前新加入点 u n + 1 u_{n+1} un+1 与 v n + 1 v_{n+1} vn+1,假设加入 u n + 1 u_{n+1} un+1 与 v n + 1 v_{n+1} vn+1 后能使原图的最大匹配数 +1,那么把新的匹配图 S n e w S_{new} Snew 与原图 S o l d S_{old} Sold 求异或和,因为原图 S o l d S_{old} Sold 中 u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn 的边数都为 1,新匹配图 S n e w S_{new} Snew 中 u 1 u_1 u1, u 2 u_2 u2 … u n + 1 u_{n+1} un+1, v 1 v_1 v1, v 2 v_2 v2 … v n + 1 v_{n+1} vn+1 的边数都为 1,那么两图求异或和后的图 S x o r S_{xor} Sxor 中 u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn 的边数一定是 0 或 2,边数为1的点有且只有 u n + 1 u_{n+1} un+1 与 v n + 1 v_{n+1} vn+1,所以图中一定存在一条以 u n + 1 u_{n+1} un+1 为起点,以 v n + 1 v_{n+1} vn+1 为终点的链,而这条链就是 S o l d S_{old} Sold 的增广链,与 S o l d S_{old} Sold 的匹配方式无关。
二、为什么最外层只需要遍历一遍左部点
直观来看,在最外层完成某一点对最大匹配的更新后,左右部点的状态已经发生改变,那为什么我们不需要用两层 for 去重复遍历左部点呢?
遍历过的左部点分为两类:1. 已经在最大匹配中的点,2. 还不在最大匹配中的点。首先是已经在最大匹配中的点,由上面的(一)的结论可知:“一个点能否找到增广路与当前点的匹配方式无关,而只与已经匹配好的点集有关”,因此已经在最大匹配中的点自然没重复遍历的意义。对于不在最大匹配中的点,如果这一点在之前没有找到增广路的话,那它之后也不可能再找到增广路了,下面用反证法证明。
假设:点 u n + 1 u_{n+1} un+1 在之前的匹配图 S o l d S_{old} Sold 中没有找到增广路但能在之后的某个匹配图 S n e w S_{new} Snew 中找到增广路。
与(一)的证明类似,不妨设之前的匹配图 S o l d S_{old} Sold 中已经匹配好了 2 n 2n 2n 个点,点集中有 { u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn }, 之后的匹配图 S n e w S_{new} Snew 中已经匹配好了 2 ( n + k ) 2(n+k) 2(n+k) 个点,点集中有 { u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, u n + 1 u_{n+1} un+1 … u n + k u_{n+k} un+k, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn, v n + 1 v_{n+1} vn+1 … v n + k v_{n+k} vn+k },加入 u n + 1 u_{n+1} un+1 … u n + k u_{n+k} un+k, v n + 1 v_{n+1} vn+1 … v n + k v_{n+k} vn+k 后原图的最大匹配数 +n,把新的匹配图 S n e w S_{new} Snew 与原图 S o l d S_{old} Sold 求异或和,因为原图 S o l d S_{old} Sold 中 u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn 的边数都为 1,新匹配图 S n e w S_{new} Snew 中 u 1 u_1 u1, u 2 u_2 u2 … u n + k u_{n+k} un+k, v 1 v_1 v1, v 2 v_2 v2 … v n + k v_{n+k} vn+k 的边数都为 1,那么两图求异或和后的图 S x o r S_{xor} Sxor 中 u 1 u_1 u1, u 2 u_2 u2 … u n u_n un, v 1 v_1 v1, v 2 v_2 v2 … v n v_n vn 的边数一定是 0 或 2,边数为1的点有且只有 u n + 1 u_{n+1} un+1 … u n + k u_{n+k} un+k, v n + 1 v_{n+1} vn+1 … v n + k v_{n+k} vn+k ,所以图中一定存在 k k k 条链,其中一条便是以 u n + 1 u_{n+1} un+1 为起点的增广链,与点 u n + 1 u_{n+1} un+1 在之前的匹配图 S o l d S_{old} Sold 中没有找到增广路的前提矛盾。
三、为什么搜索时是否访问过左部点的 vis 数组在回溯时不需要置零/为什么每个左部点在搜索某个点的最大匹配时只需要访问一次
先放一段匈牙利的 dfs 片段 c++ 代码:
bool dfs(int l) {
if (vis[l] == true) return false;
vis[l] = true;
for (auto it : edge[l]) {
if (bel[it] == 0 || dfs(bel[it])) {
bel[it] = l;
return true;
}
}
// vis[l] = false;
return false;
}
根据我们对搜索的理解,假设 vis 数组表示的是这个点在目前的搜索有没有被占用的话,那么在回溯时显然需要回溯状态,在第 10 行把 vis 重新置 0,不过这样操作会导致我们的时间复杂度直逼阶乘,显然是可不行的。
那么为什么回溯不置 0 也是正确的呢?其实我们可以把 vis 理解成这一点能否直接连接到新增右部点。因为我们使用 dfs 搜索的过程本身就是在走一条增广路,所以我们只需找到那个与新增右部点直接连接的左部点就找到了增广路,所以每个点不需要重复搜索。
特别感谢
感谢 Livinfly 给的(三)的灵感,%%%