Singercoder
2020-06-07 01:45:09
算法简介:km算法(Kuhn-Munkres算法),是一种在二分图上求解最大权完美匹配的算法,用邻接矩阵存图即可。相比费用流较高的复杂度,km算法有着更为优秀的效率,但局限性在于只能做匹配,而不像费用流那样灵活可变。
前置知识:匈牙利算法的bfs写法(附上匈牙利和km算法的bfs模板,可供参考,欢迎交流)
km算法的核心思路在于:
由此,我们便能将km算法简单地理解为:匈牙利算法+扩大相等子图
顶标的设计是km的精髓,这里参考ltao的定义:
$ly[i]$右部点的顶标 $vx[i]$左部点遍历标记 $vy[i]$右部点遍历标记 $px[i]$左部点匹配 $px[i]$右部点匹配 $slack[i]$对于指向右部点$i$的所有边,$min(lx[u]+ly[i]-e[u][i])$的值,即松弛量(初始化为inf) PS:当slack[i]=0,表示对于右部点i,相等子图中有一条指向它的边
而修改顶标的思路可以具体看代码,核心语句如下(copy的是下面bfs正解的过程):
if(vx[i])lx[i]-=d;//对于vx[i]=0的情况我们根本没有讨论,实际上也并不需要讨论
if(vy[i])ly[i]+=d;else slack[i]-=d;
这样修改的目的是保证已在相等子图中的边两侧顶标和不变,同时通过左部点顶标的减小实现向相等子图中拉进新相等边的目的。
如果还不是很理解可以画个图,对于原图中某条边:
这里提供ltao's blog以加深理解。他充分讨论了修改顶标对所有边产生的影响,还提出了关于序号3的一些非常重要的观点,本篇题解在后记中也会提到。
首先提示坑点:
long long
存储。直接打出匈牙利dfs写法+扩大相等子图的模板,初学者一定要先练熟这个。
const int MAXN=510;
int n,m;
int e[MAXN][MAXN];
int lx[MAXN],ly[MAXN],py[MAXN],d;
bool vx[MAXN],vy[MAXN];
bool dfs(int u)
{
vx[u]=1;
for(int i=1;i<=n;++i)if(!vy[i])
{
if(lx[u]+ly[i]==e[u][i])
{
vy[i]=1;
if(!py[i] || dfs(py[i]))
{
py[i]=u;
vy[i]=1;
return 1;
}
}
else d=min(d,lx[u]+ly[i]-e[u][i]);
}
return 0;
}
int main()
{
for(int i=1;i<=n;++i)
{
while(1)
{
memset(vx,0,sizeof(vx));
memset(vy,0,sizeof(vy));
d=inf;
if(dfs(i))break;
for(int j=1;j<=n;++j)
{
if(vx[j])lx[j]-=d;
if(vy[j])ly[j]+=d;
}
}
}
}
结果成功tle,不得不说卡km+dfs的题目真的不多。
我们简单分析一下复杂度:
也就是说,km+dfs的复杂度完全可以卡到
考虑如何优化,我们不难发现每次扩大相等子图之后,都要从増广起点重新开始dfs,这个过程是有明显的时间浪费的。
能不能在扩大相等子图之后,保留上次状态呢?
答案是可行的,我们只需要换用bfs写法:在每次扩大子图后,都记录一下新加入的相等边所为我们提供的新増广方向,然后从此处继续寻找増广路即可。
扩大相等子图复杂度:
増广复杂度:
由此可见,通过简单的状态延续策略,我们成功将km算法的复杂度降到了
const int MAXN=510;
int n,m;
int e[MAXN][MAXN];
int lx[MAXN],ly[MAXN],slack[MAXN];
int px[MAXN],py[MAXN],pre[MAXN];
bool vx[MAXN],vy[MAXN];
queue<int> q;
void aug(int v)
{
int t;
while(v)
{
t=px[pre[v]];
px[pre[v]]=v;
py[v]=pre[v];
v=t;
}
}
void bfs(int s)
{
memset(vx,0,sizeof(vx));
memset(vy,0,sizeof(vy));
fill(slack+1,slack+n+1,inf);
while(!q.empty())q.pop();
q.push(s);
while(1)
{
while(!q.empty())
{
int u=q.front();q.pop();
vx[u]=1;
for(int i=1;i<=n;++i)if(!vy[i])
{
if(lx[u]+ly[i]-e[u][i]<slack[i])
{
slack[i]=lx[u]+ly[i]-e[u][i];
pre[i]=u;
if(slack[i]==0)
{
vy[i]=1;
if(!py[i]){aug(i);return;}
else q.push(py[i]);
}
}
}
}
int d=inf;
for(int i=1;i<=n;++i)if(!vy[i])d=min(d,slack[i]);
for(int i=1;i<=n;++i)
{
if(vx[i])lx[i]-=d;
if(vy[i])ly[i]+=d;else slack[i]-=d;
}
for(int i=1;i<=n;++i)if(!vy[i])
{
if(slack[i]==0)
{
vy[i]=1;
if(!py[i]){aug(i);return;}
else q.push(py[i]);
}
}
}
}
int main()
{
for(int i=1;i<=n;++i)bfs(i);
}
还有一种迭代bfs的写法,是榜一
另外
这两篇博客分别使用的是队列写法和迭代写法,供参考。