以前用这个算法写过一两个水题,当时纯粹是套用模板,对算法本身是一知半解。然后Watashi的多校题中有个带花树模板题,现成的模板都套出了各种死循环,RE问题,弱爆了。这两天重新看了看论文和博客,重新理解了一遍,顺便把论文的前小半部分关于二分图最大匹配和一般图最大匹配的地方翻译了一下,论文的后半部分的二分图最大权匹配和一般图最大权匹配问题暂时还没看。
论文地址:http://builtinclz.abcz8.com/art/2012/Galil%20Zvi.pdf
我的挫翻译如下:
一、约定
G =(V, E),|V| = n,|E| = m;
M :边集,任何属于M的两条边没有公共的端节点
二、有关匹配的四个问题
1. 二分图最大匹配(boy – girl)
2. 一般图最大匹配(person – person)
3. 二分图最大权匹配
4. 一般图最大权匹配
三、知识前提——可增广路径
在逐步解决匹配问题的过程中,维护边集M,M在起始时为空,当且仅当存在一条边(i, j)在M中时才称节点i匹配了,这里也说边(i, j)匹配了。增广路径几点性质如下
1)交叉链上的每个点都是匹配的,链上的边是’匹配’-‘不匹配’-‘匹配’…-‘匹配’这样交替的
2)长度为基数,在二分图中,链的两个端点属于不同的分部。
结论1:当图中找不到可增广路径时,匹配数达到最大。// 这点性质在求最大权匹配中也有重要地位。
四、问题1的解决
O(n){枚举待匹配的节点} * O(m){增广取反路径}
对于枚举节点的每一步搜索,先清除上一步的所以标签,所以单身boy标上S,然后我们遵循两个标标签规则进行搜索
R1)如果(i,j)没匹配,i是S-boy,j是单身girl,那么给j标上T。
R2)如果(i,j)匹配了,j是T-girl, i是单身boy,那么给i标上S。
搜索直到确定其成功或失败为止。成功的标志是给一个单身girl标上了T。
注:这里S标签的意思是待匹配(跟完全单身有区别),T标签的意思是已经匹配,这个意思在问题2的解决中也是一致的。
引论1:
a)如果一个boy i(girl j)标上了S(T),那么就有一条从某个单身boy出发到i(j)的偶数(奇数)长度的交叉链
b)即使搜索失败,这个链的性质也是有效的(这句原文没看懂)
由引论1可知搜索失败意味着本轮没有得到新的增广路径;当有一个单身girl标上了T,那么就找到了一条到j的增广路径。整个搜索过程容易实现,时间复杂度不超过O(m)。
解决问题1的最好的算法是Hopcroft和Karp(HK)发明的。该算法在对图进行一次遍历时能找到多条增广路径。这个算法分为几个阶段实现,在每个阶段中,找出了最大的点不相交的增广路径集,并进行增广。所以一个阶段可以完成多次匹配。
对于某个阶段,我们同样使用R1 R2两条规则,从单身boy们开始宽度优先搜索得到G的子图G’,G’由所有在最短增广路径上的节点和边构成。子图G’按从某个单身boy的距离分层来看,第2m层(2m+1层)全是boys i(girls j)。当
1)某个单身girl在G’的底层中,意味着我们完成了最后一层,这时把这层的非单身的girls从G’中删去。
2)无法继续时,整个算法终止(不仅仅是本阶段),这样做的缘由是引论1。
在子图G’中,我们用深度优先遍历来找出最多的的不相交的增广路径。每一次我们找到一个单身girl意味着找到了一个可增广路径,把她及其配偶在G’中的边删除,然后从另一个单身boy开始新的搜索。每次我们回溯访问一条边后将其从G’中删除。可以看出,一个阶段耗时O(m)。
引论2:阶段数不会超过O(sqrt(n))。因此,这个算法的时间复杂度为O(m * sqrt(n));
有趣的是这个算法其实我们早就熟悉了。对于问题1,我们可以用最大流问题解决,最大流可以用dinic算法来做,本质上,HK算法就是Dinic,HK算法中的增广路径就是最大流中的流量增广路径。
五、问题2的解决
对于问题1,我们根据结论1可以很容易设计出一个O(n)阶段的算法,问题2也一样可以用R1 R2来做,这里要有两个的变化。第一,我们把boys和girls都替换成person ,并标上S的标签。第二,每次R1规则被用到且j标上了T,则R2也立马用来标记j的原先配偶,现把这条规则称为R12。
搜素中依次浏览S型点,浏览一个点意味着依次考虑哪些还没有匹配的边(这里至多只有一个)。当我们浏览S型点i考虑对边(i,j)进行匹配时,有两种情况:
C1)j未匹配;
C2)j是S型点。
C2的情况在二分图中不会出现。j是T型点的这种情况在此可以直接忽视。
情况C1我们用R12来解决。情况C2的做法如下:利用标签回溯到i到j得到S标签的位置。如果Si != Sj,我们就找到了一条Si到Sj的增广路径且将其匹配。麻烦的是如果Si = Sj的情况,对此将引入花朵的概念,这个概念在所有解决非二分图的匹配问题(问题2和问题4)的算法中都有决定性的作用。
如果Si = Sj = s,我们在此定义r为从i和j到s的路径上的第一个公共节点。容易看出,r也是S型点,i到r和j到r的两条路径不相交,r到s的部分也确定了下来。至此,我们已经发现了一条从r经过(i,j)到r自己的奇数长度的交替链。我们把这条链(环)称作花朵B,r则作为这个花朵的标志节点。
这里要做的是将花朵B缩成点:用一个超级节点B来代替这个花朵,用一个边集A来记录与B相连的边(A = {(B,j) | j 不属于B})。对于这样的集合A,其中最多只用一条边能匹配(如果r = s,将没有边能匹配)。这里重新定义G’为G缩点之后的图,那么:
结论2:当且仅当G’中有可增广路径时,G有可增广路径。
此处不给出结论2的详细证明,但有一点很明显,一旦给出了图G’中的一条可增广路径,图G中也能立马生成一条可增广路径。如果这条路径经过B,那么:用(r, k)替代匹配的边(B, k);用边(j, i)替代没有匹配的边(j, B),这里(j, i)是来源于与B中i到r的偶数长交替链。这样的路径总是存在的:因为在缩点形成B时,如果i是一个S型点,我们就能利用标签从i回溯到r。否则,这个花朵的标签会是截然相反的。在处理过程中,我们用一个带有花朵标志节点的双向链表来存储B会比较方便。
在找可增广路径时要用到队列Q,用来存储新的S型节点。在搜索过程中,Q中节点依次被访问并相应形成新的花朵。由于可能多次缩点,花朵可能嵌套。不过这样还是可以很方便的表示出每个节点是否属于某个大小为1的(本身)花朵。当花朵B1,…,Bk形成新的花朵B时,我们称B1~Bk为B的子花,当然B1~Bk已经退化不再称为是花了。因此,在任何时刻,每一个在老图G中的节点在当前的图中只属于一个花朵。对于每一个花朵,其子花和子子花乃至底层的单个节点的子花,之间形成一颗构造树,树的叶子就是属于B的节点。
如果搜索成功的话(情况C2),我们就找到了在当前图中的一条可增广路径。然后我们利用上面提到的结论2和构造树递归对原图中的可增广路径进行松弛。再扩展匹配,删除所有的标签和花朵,开始下一个阶段。整个时间复杂度为O(m)。如果搜索失败,即队列Q为空,根据结论2和引论1(这里boy和girl变成了person),此时已经达到最大匹配,整个算法结束。
朴素的算法时间复杂度为O(n4),其中每一个阶段O(n3)。更好一点的写法,可以达到O(n3):既然花朵都是不相交的,所以任何时候的构造树的总大小为n。当我们生成一个新的花朵时,不重新命名边。为了加快找出一个给定的节点所属的花朵,我们可以维护一个并查集。当花朵B形成时,我们把B中的T型点也放入Q中,这样就能在之后方便的访问到这些点而不是访问超级节点B。而B的其他节点(S型点)也一定已经放入Q中过了。当我们遇到情况二的边,如果边的两个端节点在同一个花朵里,可以直接忽略。由此,一个阶段耗时O(n2)。
====================================================================================================
另外在看的过程中,学习了博客http://fanhq666.blog.163.com/blog/static/8194342620120304463580/ 该博主的做法也是按照论文来写的,博主所附的代码也是我在学习中作为模板来理解算法的。下面的代码(Ural 1099)几乎和他附的代码一模一样,我加了我注释,便于理解:
#include <cstdio>
#include <cstring>
#include <iostream>
#include <queue>
using namespace std;
const int N = 250;
// 并查集维护
int belong[N];
int findb(int x) {
return belong[x] == x ? x : belong[x] = findb(belong[x]);
}
void unit(int a, int b) {
a = findb(a);
b = findb(b);
if (a != b) belong[a] = b;
}
int n, match[N];
vector<int> e[N];
int Q[N], rear;
int next[N], mark[N], vis[N];
// 朴素算法求某阶段中搜索树上两点x, y的最近公共祖先r
int LCA(int x, int y) {
static int t = 0; t++;
while (true) {
if (x != -1) {
x = findb(x); // 点要对应到对应的花上去
if (vis[x] == t) return x;
vis[x] = t;
if (match[x] != -1) x = next[match[x]];
else x = -1;
}
swap(x, y);
}
}
void group(int a, int p) {
while (a != p) {
int b = match[a], c = next[b];
// next数组是用来标记花朵中的路径的,综合match数组来用,实际上形成了
// 双向链表,如(x, y)是匹配的,next[x]和next[y]就可以指两个方向了。
if (findb(c) != p) next[c] = b;
// 奇环中的点都有机会向环外找到匹配,所以都要标记成S型点加到队列中去,
// 因环内的匹配数已饱和,因此这些点最多只允许匹配成功一个点,在aug中
// 每次匹配到一个点就break终止了当前阶段的搜索,并且下阶段的标记是重
// 新来过的,这样做就是为了保证这一点。
if (mark[b] == 2) mark[Q[rear++] = b] = 1;
if (mark[c] == 2) mark[Q[rear++] = c] = 1;
unit(a, b); unit(b, c);
a = c;
}
}
// 增广
void aug(int s) {
for (int i = 0; i < n; i++) // 每个阶段都要重新标记
next[i] = -1, belong[i] = i, mark[i] = 0, vis[i] = -1;
mark[s] = 1;
Q[0] = s; rear = 1;
for (int front = 0; match[s] == -1 && front < rear; front++) {
int x = Q[front]; // 队列Q中的点都是S型的
for (int i = 0; i < (int)e[x].size(); i++) {
int y = e[x][i];
if (match[x] == y) continue; // x与y已匹配,忽略
if (findb(x) == findb(y)) continue; // x与y同在一朵花,忽略
if (mark[y] == 2) continue; // y是T型点,忽略
if (mark[y] == 1) { // y是S型点,奇环缩点
int r = LCA(x, y); // r为从i和j到s的路径上的第一个公共节点
if (findb(x) != r) next[x] = y; // r和x不在同一个花朵,next标记花朵内路径
if (findb(y) != r) next[y] = x; // r和y不在同一个花朵,next标记花朵内路径
// 将整个r -- x - y --- r的奇环缩成点,r作为这个环的标记节点,相当于论文中的超级节点
group(x, r); // 缩路径r --- x为点
group(y, r); // 缩路径r --- y为点
}
else if (match[y] == -1) { // y自由,可以增广,R12规则处理
next[y] = x;
for (int u = y; u != -1; ) { // 交叉链取反
int v = next[u];
int mv = match[v];
match[v] = u, match[u] = v;
u = mv;
}
break; // 搜索成功,退出循环将进入下一阶段
}
else { // 当前搜索的交叉链+y+match[y]形成新的交叉链,将match[y]加入队列作为待搜节点
next[y] = x;
mark[Q[rear++] = match[y]] = 1; // match[y]也是S型的
mark[y] = 2; // y标记成T型
}
}
}
}
bool g[N][N];
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++)
for (int j = 0; j < n; j++) g[i][j] = false;
// 建图,双向边
int x, y; while (scanf("%d%d", &x, &y) != EOF) {
x--, y--;
if (x != y && !g[x][y])
e[x].push_back(y), e[y].push_back(x);
g[x][y] = g[y][x] = true;
}
// 增广匹配
for (int i = 0; i < n; i++) match[i] = -1;
for (int i = 0; i < n; i++) if (match[i] == -1) aug(i);
// 输出答案
int tot = 0;
for (int i = 0; i < n; i++) if (match[i] != -1) tot++;
printf("%d\n", tot);
for (int i = 0; i < n; i++) if (match[i] > i)
printf("%d %d\n", i + 1, match[i] + 1);
return 0;
}