加权二分图之km算法

百科:
KM算法求的是完备匹配下的最大权匹配: 在一个二分图内,左顶点为X,右顶点为Y,现对于每组左右连接XiYj有权wij,求一种匹配使得所有wij的和最大。

步骤:
一般对KM算法的描述,基本上可以概括成以下几个步骤:
(1)初始化可行标杆
(2)用匈牙利算法寻找完备匹配
(3)若未找到完备匹配则修改可行标杆
(4)重复(2)(3)直到找到相等子图的完备匹配

km算法基于匈牙利算法求加权最大匹配
匈牙利算法一般用于寻找二分图的最大匹配。算法根据一定的规则选择二分图的边加入匹配子图中,其基本模式为:
初始化匹配子图为空
While 找得到增广路径
Do 把增广路径添加到匹配子图中

增广路
增广路径有如下特性:
1. 有奇数条边
2. 起点在二分图的X边,终点在二分图的Y边
3. 路径上的点一定是一个在X边,一个在Y边,交错出现。
4. 整条路径上没有重复的点
5. 起点和终点都是目前还没有配对的点,其他的点都已经出现在匹配子图中
6. 路径上的所有第奇数条边都是目前还没有进入目前的匹配子图的边,而所有第偶数条边都已经进入目前的匹配子图。奇数边比偶数边多一条边
7. 于是当我们把所有第奇数条边都加到匹配子图并把条偶数条边都删除,匹配数增加了1.

增广路反选:
将增广路上已匹配的边变为未匹配,未匹配的边变为匹配。

初始化标杆:
首先要初始化两个标杆分别为X标杆和Y标杆,X标杆初始化为与之相连的最大边权,Y标杆初始化为0,且直接加入拥有最大边权的边。

遵循什么样的原则去修改标杆的值?
对于正在增广的增广路径上属于集合X的所有点减去一个常数delta,属于集合Y的所有点加上一个常数delta。
为什么要这样做呢,我们来分析一下:
对于图中任意一条边edge(i,j) (其中xi∈X,xj∈Y)权值为weight(i,j)
如果i和j都属于增广路,那么lx[i]−delta+ly[j]−+delta=lx[i]+ly[j]值不变,也就说edge(i,j)可行性不变,原来是相等子图的边就还是,原来不是仍然不是
如果i属于增广路,j不属于增广路,那么lx[i]−delta+ly[j]的值减小,也就是原来这条边不在相等子图中(否则j就会被遍历到了),现在可能就会加入到相等子图。
如果i不属于增广路,j属于增广路,那么lx[i]+ly[j]+delta的值增大,也就是说原来这条边不在相等子图中(否则j就会被遍历到了),现在还不可能加入到相等子图
如果i,j都不属于增广路,那么lx[i]和ly[j]都不会加减常数delta值不变,可行性不变
这 样,在进行了这一步修改操作后,图中原来的可行边仍可行,而原来不可行的边现在则可能变为可行边。那么delta的值应取多少?
观察上述四种情况,只有第二类边(xi∈X,yj∈Y)的可行性经过修改可以改变。
因为对于每条边都要满足lx(i)+ly(j)>=weight(i,j),这一性质绝对不可以改变,所以取第二种情况的 lx[i]+ly[j]−weight(i,j)的最小值作为delta。

手工模拟:
有邻接矩阵如下:
||x1 x2 x3 x4 x5
y1 7 3 14 19 23
y2 8 10 15 18 20
y3 6 8 12 16 19
y4 4 9 13 20 25
y5 2 7 12 10 15

初始化标杆使X标杆的值为斜体字的值。

加权二分图之km算法_i++


连接每条边并且使得x1和y3匹配,然后遍历x2,发现x2找不到合法增广路。

加权二分图之km算法_初始化_02


把不合法路径上的x点都归为点集S,y点都归为T,将不在T中的y点和在S中的点尝试进行加边。

加权二分图之km算法_初始化_03


找到两条边,更新顶标之后,成功形成增广路,运用匈牙利算法反选。

加权二分图之km算法_子图_04


给x3找一个合法的增广路,一下就找到了,直接反选,结束。

加权二分图之km算法_i++_05


加权二分图之km算法_初始化_06


加权二分图之km算法_子图_07


加权二分图之km算法_子图_08


加权二分图之km算法_i++_09

km算法模板:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=110;
const int inf=0x7fffffff;
int n,m,w[maxn][maxn];//n表示x边点的个数,y表示y边点的个数,w表示边权值
int lx[maxn],ly[maxn];//lx表示x边的标杆,ly表示y边的标杆
int from[maxn],to[maxn];//from[i]=j表示y边的i由x边的j连接。to[i]=j表示x边的i可以到达y边的j
bool s[maxn],t[maxn];//s表示x边的点集,t表示y边的点集
bool find(int x)//匈牙利算法
{
s[x]=1;
for(int i=1;i<=m;i++)
if(lx[x]+ly[i]==w[x][i]&&!t[i])
{
t[i]=1;
if(!from[i]||find(from[i]))
{
from[i]=x;
to[x]=i;
return 1;
}
}
return 0;
}
void update()//更新标杆
{
int d=inf;
for(int i=1;i<=n;i++)
if(s[i])
for(int j=1;j<=m;j++)
if(!t[j])
d=min(d,lx[i]+ly[j]-w[i][j]);//按照上面给出的原则计算d
for(int i=1;i<=n;i++)//点集s中的点的标杆lx[i]-d
if(s[i]) lx[i]-=d;
for(int j=1;j<=m;j++)//点集t中的点的标杆ly[j]+d
if(t[j]) ly[j]+=d;
}
void km()//km算法
{
for(int i=1;i<=n;i++)//初始化标杆
for(int j=1;j<=m;j++)
lx[i]=max(lx[i],w[i][j]);
for(int i=1;i<=n;i++)//x边每个点的匹配
for(;;)
{
memset(s,0,sizeof(s));
memset(t,0,sizeof(t));
if(find(i)) break;
else update();
}
}
int main()
{
scanf("%d%d",&n,&m);//输入
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
scanf("%d",&w[i][j]);
km();
int ans=0;
for(int i=1;i<=n;i++)//计算答案
ans+=w[i][to[i]];
printf("%d",ans);//输出
return 0;
}