KM算法学习笔记

· · 个人记录

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