匈牙利算法
二分图的最大匹配可以转换为一个网络流的问题,但是我们一般使用匈牙利算法,这种算法更易于理解,方便编写。
介绍这个算法之前,首先要介绍一些必要的概念。
交错路 : 从一个未匹配点出发,依次遍历未匹配边、匹配边、未匹配边,这样交替下去,这条路径称为交错路。
增广路 : 从一个未匹配点出发,依次遍历未匹配边、匹配边、未匹配边,这样交替下去,如果最后一个点是未匹配点,这条路径称为增广路。换句话说,起点和终点都为未匹配点的交错路为增广路(特别提醒,这里的增广路和网络流中的增广路的意义不同)
如图所示,图6 是图5 的其中一条增广路,可以看出由未匹配点9出发,依次沿着边
e
d
g
e
(
9
,
4
)
−
>
e
d
g
e
(
4
,
8
)
−
>
e
d
g
e
(
8
,
1
)
−
>
e
d
g
e
(
1
,
6
)
−
>
e
d
g
e
(
6
,
2
)
e d g e ( 9 , 4 ) −> e d g e ( 4 , 8 ) −> e d g e ( 8 , 1 ) −> e d g e ( 1 , 6 ) −> e d g e ( 6 , 2 )
edge(9,4)−>edge(4,8)−>edge(8,1)−>edge(1,6)−>edge(6,2) 到达未匹配点2,显然,这是一条增广路。
观察图6我们可发现增广路的一些特点。
-
增广路一定有奇数条边。
-
增广路中未匹配边一定比匹配边多一条(因为是从未匹配点出发走交错路到未匹配点结束)
这里其实就表明了研究增广路的意义。
如果找到了一条增广路,那么将未匹配点与匹配边的身份调换,那么匹配的边数就多了一条,这样直到找不到增广路为止,那么整个图的匹配的边数一定最大,也就是找到了二分图的最大匹配。
这里的身份调换是指 :
原来匹配的边为edge(1,6), edge (4,8),匹配边数为2 。找到一条增广路(这里不一定从9开始找,任何一个未匹配点都可以)后,现在匹配的边为edge(2,6), edge(1,8), edge(4,9), 匹配边数为3。
匈牙利算法正是利用了增广路的这个性质,从X集合中找到一个未匹配点,寻找增广路,找到了匹配数+1,如果没有找到,那么从X中找到下一个未匹配的点,再次寻找增广路…重复上述过程,直到X集合中的所有节点都被“增广”完毕,无论如何都找不到增广路,那么整个图的匹配数就最大了
伪代码:
void hungary()//匈牙利算法
{
for i->1 to nx//对于集合X中的每一个节点
if (从未匹配点i出发有增广路)
匹配数++;
输出 匹配数;
}
bool findpath(x)//寻找从x出发的对应项出的可增广路
{
//crossPath[x] = true;这条语句可能会有很多人加上,但是实际上crossPtah总是记录集合Y中的节点是否在交错路上
for each edge(x,y) in G.E
{
if(y is not in crossPath)
{
add y into crossPath
lastX = match[y];//lastX是X集合的上一个与y匹配的节点
if(y is not matched or findpath(lastX))//如果y已经被了,那么试试从lastX能不能另外找到一条增广路,把当前增广路让给现在的x
{
match[y] = x;
//match[x] = y,wrong !
return true;//从x出发有增广路
}
}
}
return false;//从x出发没有增广路
}
KM算法
如果二分图的每条边都有一个权重,要求一种完备匹配方案,使得所有匹配边的权重和最大,记作最佳完美匹配。一般使用KM算法解决该问题
KM算法,是对匈牙利算法的一种贪心扩展。
流程
Kuhn-Munkras算法(即KM算法)流程:
- 初始化可行顶标的值 (设定lx,ly的初始值)
- 用匈牙利算法寻找相等子图的完备匹配
- 若未找到增广路则修改可行顶标的值
- 重复(2)(3)直到找到相等子图的完备匹配为止
KM算法的核心部分即控制修改可行顶标的策略使得最终可到达一个完美匹配。
伪代码:
bool findpath(x)
{
visx[x] = true;
for(int y = 1 ; y <= ny ; ++y)
{
if(!visy[y] && lx[x] + ly[y] == weight(x,y)) //y不在交错路中且edge(x,y)必须在相等子图中
{
visy[y] = true;
if(match[y] == -1 || findpath(match[y]))//如果y还为匹配或者从y的match还能另外找到一条匹配边
{
match[y] = x;
return true;
}
}
}
return false;
}
void KM()
{
for(int x = 1 ; x <= nx ; ++x)
{
while(true)
{
memset(visx,false,sizeof(visx));//访问过X中的标记
memset(visy,false,sizeof(visy));//访问过Y中的标记
if(findpath(x))//找到了增广路,跳出继续寻找下一个
break;
else
{
for(int i = 1 ; i <= nx ; ++i)
{
if(visx[i])//i在交错路中
{
for(int j = 1 ; j <= ny ; ++j)
{
if(visy[j])//j不在交错路中,对应第二类边
delta = Min(delta,lx[x] + ly[y] - weight(i,j))
}
}
}
for(int i = 1 ; i <= nx ; ++i)//增广路中xi - delta
if(visx[i])
lx[i] -= delta;
for(int j = 1 ; j <= ny ; ++j)//增广路中yj + delta
if(visy[j])
ly[j] += delta;
}
}
}
参考链接:
https://blog.csdn.net/sixdaycoder/article/details/47680831
https://blog.csdn.net/sixdaycoder/article/details/47720471