无权二分图的匹配与匈牙利算法
二分图:简单来说就是图中的点可以分成两部分,并且每一部分里面都没有边相连,任意一条边起点和终点都不在同一个点集。
匹配:边的集合,任意两条边都没有公共点。
匹配点,匹配边:在匹配中的点和边
非匹配点,非匹配边:不在匹配集合中的点和边。
最大匹配:图的所有匹配中,含边数最多的匹配就是最大匹配。
完美匹配:匹配集合中涵盖了所有的点,所有的点都是匹配点。显然,完备匹配一定是最大匹配,注意,并不是所有的二分图都存在完美匹配。
求二分图的最大匹配数一般都是使用经典的匈牙利算法。匈牙利算法中涉及到两个概念,分别是交替路和增广路。交替路是指匹配边和非匹配边交替出现。增广路是指从非匹配点出发沿着交替路前进终于非匹配点的路径。若存在增广路,那么只要把增广路上匹配边和非匹配边交换角色,就能在不减少当前匹配的情况下多增加一个匹配。于是匈牙利算法的概念就很明显了,就是不断的找增广路并交换角色。
匈牙利算法的流程如下:
1.从第一个未匹配点开始寻找增广路。若找到增广路,则修改匹配情况,并且增加匹配数。如果不存在增广路,停止对当前结点的搜索,从下一个未匹配的节点开始搜索增广路。
2.寻找增广路需要标记路径节点,如果找到一个未匹配点,则说明存在增广路,此时交换匹配边增广即可。若找到匹配点,则从该匹配点沿着匹配边寻找增广路。
匈牙利算法一般用DFS实现,因为思路清晰而且简单,但是其在稀疏图的情况下,效果不如BFS,在实践中为了加速DFS,会选择先用贪心的思路先寻找到一个匹配再来增广。具体模板代码实现如下:
// Todo
二分图的匹配问题很经典,但是一般不会直接问,可能绕个弯子或者是加个权重。简单来说绕弯子的话就是求二分图的最小顶点覆盖数,DAG的最小路径覆盖,最大独立集。
二分图的最小顶点覆盖:在二分图中求最少的点,使得每条边至少和一个点关联。最小顶点覆盖数就等于最大匹配数(Konig 定理)(HDOJ 1150)
DAG的最小路径覆盖:用尽量少的不相交的简单路径覆盖有向无环图的所有顶点。其最小覆盖数=节点数-最大匹配数(HDOJ 1151)
最大独立集:其等于节点数-最大匹配数(HDOJ1068)
Kuhn-Munkres算法(二分图最大权匹配)
二分图的带权匹配,就是求出一个匹配集合,使得集合中边的权值最大或者最小。这种情况下,直接用KM算法就可以求出最大权重即可。了解KM算法首先需要了解一个概念,就是完备匹配。
定义:设G=<V1,V2,E>为二分图,|V1|≤|V2|,M为G中一个最大匹配,且|M|=|V1|,则称M为V1到V2的完备匹配。
在上述定义中,若|V2|=|V1|,则完备匹配即为完美匹配,若|V1|<|V2|,则完备匹配为G中最大匹配。
二分图的最佳(最大权或者最小权)匹配一定是完备匹配,于是就在完备匹配的基础上再来要求边权的最大或者最小,从而求得最佳匹配。而KM算法就是这么一个思路,但是KM算法的运行要求是必须存在一个完备匹配,如果要求一个最大权匹配(不一定完备)的话就需要把不存在的边的权值赋值为0。
KM算法是求最大权重的匹配,若是要求最小权重的话,就需要将所有边取负权值,然后结果取反。若是要求边权之积最大的话,则需要对权值取对数后运行KM算法,得出结果后映射回去。
KM算法是通过给每个顶点一个标号(也叫顶标)将求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B[i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j),A[i]+B[j]>=w[i,j]始终成立,初始A[i]为与xi相连的边的最大边权,B[j]=0。KM算法的正确性基于以下定理:
设:G(V,E) 为二部图,G'(V,E') 为二部图的子图。如果对于 G' 中的任何边<x,y>满足, L(x)+ L(y)== Wx,y,我们称G'(V,E') 为 G(V,E) 的等价子图或相等子图(是G的生成子图)。
若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的等价子图有完备匹配,那么这个完备匹配就是二分图的最大权匹配。对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和(即不是最佳匹配)。所以相等子图的完备匹配一定是二分图的最大权匹配。
算法在初始时为了使A[ i ]+B[j]>=w[i,j]恒成立,令A[ i ]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。
我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交替路。这时我们获得了一棵匈牙利树,它的叶子结点全部是X顶点。现在我们把匈牙利树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现:
1)两端都在交错树中的边(i,j),A[ i ]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。
2)两端都不在交错树中的边(i,j),A[ i ]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。
3)X端不在交错树中,Y端在交错树中的边(i,j),它的A[ i ]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。
4)X端在交错树中,Y端不在交错树中的边(i,j),它的A[ i ]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
现在的问题就是求d值了。为了使A[ i ]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j] | Xi在交错树中,Yi不在交错树中}。
以上就是KM算法的基本思路。但是朴素的实现方法,时间复杂度为O(n^4),需要找O(n)次增广路,每次增广最多需要修改O(n)次顶标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n^2)。实际上KM算法的复杂度是可以做到O(n^3)的。我们给每个Y顶点一个“松弛量”函数slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在等价子图中,则让slack[j]变成原值与A[i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在匈牙利树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改顶标后,要把所有的不在匈牙利树中的Y顶点的slack值都减去d。
Kuhn-Munkras算法流程:
(1)初始化可行顶标的值
(2)用匈牙利算法寻找完备匹配
(3)若未找到完备匹配则修改可行顶标的值
(4)重复(2)(3)直到找到相等子图的完备匹配为止
简单的来说,KM算法就是用一个标记函数lx(i)来去找完备匹配,在找的过程中不断降低总权重使边加入匹配中。完备匹配是用匈牙利算法求的,外加一个节点函数的update。总的复杂度是O(n^4),加上松弛维护可以达到O(n^3)。
书上的O(n^4)的模板
const int maxn = 105; // 最大节点数
int W[maxn][maxn], n; // 边的权重和点的个数
int Lx[maxn], Ly[maxn]; // 节点函数
int left[maxn]; //匹配
bool S[maxn], T[maxn]; //匹配的点集
// 匈牙利算法寻找完备匹配
bool match(int i)
{
S[i] = true;
for (int j = 1; j <= n; ++j) if (Lx[i] + Ly[j] == W[i][j] && !T[j]) // 边权的条件
{
T[j] = true;
if (!left[j] || match(left[j]))
{
left[j] = i;
return true;
}
}
return false;
}
// 节点更新
void update()
{
int a = 1<<30;
for (int i = 1; i <= n; ++i) if (S[i])
for (int j = 1; j <= n; ++j) if (!T[j])
a = min(a, Lx[i] + Ly[j] - W[i][j]); // 寻找最小的d
// 修改节点的权值
for (int i = 1; i <= n; ++i)
{
if (S[i]) Lx[i] -= a;
if (T[i]) Ly[i] += a;
}
}
void KM()
{
// 初始化
for (int i = 1; i <= n; ++i)
{
left[i] = Lx[i] = Ly[i] = 0;
for (int j = 1; j <= n; ++j)
Lx[i] = max(Lx[i], W[i][j]);
}
for (int i = 1; i <= n; ++i)
{
// 对每一个节点都找到匹配,即寻找完备匹配
for (;;)
{
for (int j = 1; j <= n; ++j) S[j] = T[j] = 0;
if (match(i)) break;
else update();
}
}
}
来自红书加上松弛的模板,O(n^3)
const int maxn = 555;
const int inf = 1000000000;
int w[maxn][maxn], x[maxn], y[maxn];
int prev_x[maxn], prev_y[maxn], son_y[maxn], slack[maxn], par[maxn];
int lx, ly, pop;
void adjust(int v)
{
son_y[v] = prev_y[v];
if (prev_x[son_y[v]] != -2)
adjust(prev_x[son_y[v]]);
}
bool find(int v)
{
int i;
for (i = 0; i < pop; i++)
if (prev_y[i] == -1)
{
if (slack[i] > x[v]+y[i]-w[v][i])
{
slack[i] = x[v]+y[i]-w[v][i];
par[i] = v;
}
if (x[v]+y[i] == w[v][i])
{
prev_y[i] = v;
if (son_y[i] == -1)
{
adjust(i);
return true;
}
if (prev_x[son_y[i]] != -1)
continue;
prev_x[son_y[i]] = i;
if (find(son_y[i]))
return true;
}
}
return true;
}
int km()
{
int i, j, m;
for (i = 0; i < pop; ++i)
{
son_y[i] = -1;
y[i] = 0;
}
for (i = 0; i < pop; ++i)
{
x[i] = 0;
for (j = 0; j < pop; ++j)
x[i] = min(x[i], w[i][j]);
}
bool flag;
for (i = 0; i < pop; ++i)
{
for (j = 0; j < pop; ++j)
{
prev_x[j] = prev_y[j] = -1;
slack[j] = inf;
}
prev_x[i] = -2;
if (find(i)) continue;
flag = false;
while (!flag)
{
m = inf;
for (j = 0; j < pop; ++j)
if (prev_y[j] == -1)
m = min(m, slack[j]);
for (j = 0; j < pop; ++j)
{
if (prev_x[j] != -1)
x[j] -= m;
if (prev_y[j] != -1)
y[j] += m;
else
slack[j] -= m;
}
for (j = 0; j < pop; ++j)
if (prev_y[j] == -1 && !slack[j])
{
prev_y[j] = par[j];
if (son_y[j] == -1)
{
adjust(j);
flag = true;
break;
}
prev_x[son_y[j]] = j;
if (find(son_y[j]))
{
flag = true;
break;
}
}
}
}
int ans = 0;
for (int i = 0; i < pop; ++i)
ans += w[son_y[i]][i];
return ans;
}