KM算法学习笔记
Zeven
·
·
个人记录
KM
解决问题:
二分图最大权匹配(可以限定匹配次数),复杂度O(n^3)
适用条件:
图存在完备匹配。如果不存在的话可以通过添加虚点虚边来解决问题。
\text{ }
定义:
顶标:给左部点、右部点的赋权,分别记录为$lx[x],ly[y]$。赋权规则需要满足$lx[x]+ly[y] \ge e(x,y)
delta(x,y)$:$lx[x]+ly[y] - e(x,y)
相等边:满足lx[x]+ly[y] = e(x,y)的边。
相等子图:相等边及对应点构成的子图。
交替树:一次寻找增广路中,访问到的所有点构成的树型结构。
\text{ }
引理:
相等子图有完备匹配的时候原图恰有最大权匹配。
证明:选择的边一定是相等子图内的边,否则e(x,y)<lx[x]+ly[y],不是最优。
\text{ }
思路:
贪心、增量修改扩张子图
\text{ }
朴素实现O(n^4):
三种边:相等边、匹配边、其他边
一开始把标号设为INF,再不断缩小来扩张相等子图,直到最后形成完备匹配。
每次从当前点出发寻找以其作为起点,走相等边的增广路,访问到的点构成交替树。
如果没有增广路的话:
然后把交替树左部的点-=d,右边的点+=d。
d为由交替树左部点x出发,y右部点不在交替树内的其他边中delta最小的一条e(x,y)。
然后不断进行该过程。
\text{ }
证明:
分类讨论计算影响:
A/B都在树内或者树外,无影响。
A在树内,B在树外:由于已经增广完毕,所以不存在相等边和匹配边,故会使其他边减小,且至少会使一个降为0并使一个新的B加入交替树内,满足需求。
A在树外,B在树内:
1、匹配边:不存在(因为B集合的匹配点都会在增广路的时候进行扩展)
2、相等边(障碍之处)
即存在左部点在树外,右部点在树内的情况下,修改会使一条相等边变成其他边。
这条边不是匹配边,修改其不会减少匹配次数,我们只需要保证在不断修改下最终得到一条增广路即可(这就要求需要保证有完备匹配),所以我们可以暂且让它变大。
3、其他边:同上
\text{ }
复杂度:
这样的话对于每一个点的修改次数是O(n)级别的,因为最多修改O(n)次所有右部点都会进入交替树,必然出现新的增广路。总复杂度O(n^4)
O(n^3)
通过每次求增广路的时候记录重复信息来节省时间复杂度。
1、把所有没有匹配的左部点加入访问集合并更新sl[y],sl[y]表示右部在y的其他边的最小delta(同时记录前驱)。
2、执行修正操作(注意需要同时修改sl[y])。
3、如果sl[y]=0的话,则可以由其开始进行增广操作。
如果其没有匹配过的话就形成一组解;否则的话把其放入交替树内并更新,回到1。
这样一次访问全图可以得到一条增广路,复杂度O(n^3)
\text{ }
特殊性质
KM算法,如果把lx设为INF的话,前i次结果恰为选择i次的结果。
证明:此过程相当于找最长路。
考虑d的贡献:x子树内-=d,y子树内+=d,不断重复发现我们最后匹配了一组(x,y),则:每次我们将所有没有匹配的x都放进交替树,并将其lx[x]减少d,而y每次加入交替树就可以有一组匹配,ly[y]一定是0。
故匹配结果为:lx'[x]+ly'[y]=lx[x]-\sum d.
我们每次选择最小的一个d等价于寻找一个最长的增广路,故证明成立。
\text{ }
KM算法与费用流
考虑我们费用流的建边,求最大值相当于取反后求$min$,我们是否可以将此问题和费用流联系在一起呢?
我们发现上述证明可以解释与费用流“找最大增广路”类似,证明其本质是类似的。
遗憾的是,笔者只能通过这种证明来解释,并没有找到一组可以描述其实际意义的定义。
---
给出一份代码:
```cpp
#include<bits/stdc++.h>
using namespace std;
#define FOR(i,a,b) for(int i=a;i<=b;i++)
#define REP(x) for(int i=head[x],y=a[i].to;i;i=a[i].next,y=a[i].to) if(y!=fa)
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define mid ((l+r)>>1)
typedef vector<int> ve;
typedef long long ll;
typedef long double ld;
inline int read()
{
int s=0,w=1;
char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') w=-1;ch=getchar();}
while(ch>='0'&&ch<='9') s=s*10+ch-'0',ch=getchar();
return s*w;
}
const int N=2020,INF=1e9;
int n,q,lx[N],ly[N],vx[N],vy[N],mx[N],my[N],sl[N],pre[N],a[N][N];
ll ans[N];
void ranse(int x)
{
vx[x]=1;
FOR(y,1,n) {
if(vy[y]) continue;
if(lx[x]+ly[y]-a[x][y]<sl[y]) {
sl[y]=lx[x]+ly[y]-a[x][y];
pre[y]=x;
}
}
}
void aug(int x)
{
while(x) {
int y=mx[pre[x]];
my[x]=pre[x];
mx[pre[x]]=x;
x=y;
}
}
void work()
{
FOR(i,1,n)lx[i]=INF;
FOR(s,1,n) {
FOR(i,1,n) vx[i]=vy[i]=pre[i]=0,sl[i]=INF;
FOR(i,1,n) if(!mx[i]) ranse(i);
int tp=0;
while(!tp) {
int d=INF;
FOR(i,1,n) if(!vy[i]) d=min(d,sl[i]);
FOR(i,1,n) {
if(vx[i]) lx[i]-=d;
if(vy[i]) ly[i]+=d;
else sl[i]-=d;
}
FOR(i,1,n) {
if(!vy[i]&&!sl[i]) {
vy[i]=1;
if(!my[i]) {aug(i);tp=1;break;}
else ranse(my[i]);
}
}
}
FOR(i,1,n) ans[s]+=a[i][mx[i]];
}
}
int main()
{
int num=read();
n=read();q=read();
FOR(i,1,n)FOR(j,1,n)a[i][j]=read();
work();
while(q--) {
int x=min(n,read());
cout<<ans[x]<<"\n";
}
}
```
----
后记:
笔者第一次接触KM算法是在轩神的NOI模拟赛上,当时我问了问轩神KM算法中的一些细节,然而轩神说他记不清楚了,是背的板子。。。
然而,WC2020营员交流——“浅谈一类二分图带权匹配问题”。。。。
2333