KM算法:
KM是用来求带权二分图的最优匹配的一种算法。
原理:
我们要求一个二分图的最优匹配,直接求的话肯定是不太好求的,因为每一条边都带有自己的边权,而我们是要求得一个匹配,使得所有的边权加起来值最大。
然后智商超级高的KM算法发明者将这个问题转化为了求一个带权二分图的完备匹配的问题。
概念:
- 顶标:每一个点有一个顶标,左边的点的顶标为 ,右边的点的顶标为 。
- 性质:保证对于算法进行的任意时刻,对于属于此二分图的任意一条边 有 。
- 可行边: 满足 的边
- 增广路:全部由可行边构成,类似于匈牙利算法那样,由一条匹配边,一条非匹配边构成的,且两段都是非匹配边的一条路
做法:
既然对于每一条边都满足上述性质,如果我们能够在二分图中找到一种全部由可行边构成的完备匹配,使得任意
都满足
,那么我们就可以证明这个完备匹配是最优的(参见顶标的性质)。
既然我们可以这样来求出最优匹配,那么就要构造这样的顶标集合满足上面的所有条件。
初始的时候我们把每个左边的点的顶标设为和它相连的权值最大的边的权值。然后用匈牙利算法去一遍一遍地跑增广路,直到是全部由可行边构成的完备匹配为止。但是数据给的图不一定有完备匹配怎么办?我们可以构造一个完全图,原图中没有的边权值设为0。
顶标的修改:
显然这样不一定可以找到满足条件的完备匹配。所以我们要在满足性质的情况下修改点的顶标使得可以找得到满足条件的完备匹配。考虑修改目前在交错树中的边,如果将左边的点的顶标减去了 ,那么相应的,对于已经匹配上的边,右边的对应点的顶标要加上 ,这样原来的边的可行性才不会发生变化。
- 两端都在交错树中的边 , 的值没有变化。也就是说,它原来是可行边,现在仍是可行边。
- 两端都不在交错树中的边 , 都没有变化。也就是说,这条边的可行性并没有发生改变。
- u端不在交错树中,v端在交错树中的边 ,它的 的值有所增大。它原来不是可行边,现在仍不是可行边
- u端在交错树中,v端不在交错树中的边 ,它的 的值有所减小。也就说,它原来不是可行边,现在就可能成为了可行边。
所以,为了找到由可行边构成的增广路,我们要尽量修改顶标使得可行边的条数增多,对应上面的第四点,修改量即为 。为了让整个图都满足限制条件,修改量即为所有满足u端在在交错树中,而v端不在交错树的 。
过程:
通过上面的数学证明和分析,可以得到KM算法的步骤:
- 用匈牙利算法给每一个左端点来找增广路。
- 若找不到增广路,记下修改量的最小值,修改访问过的点的顶标,重复第一步
- 换下一个点来找增广路
时间复杂度:
网上有很多的分析是不严谨的,我们按照上面的方法,每一个点找一次增广路,时间上一个 ,对于每一次寻找,可能要将n个点添加进目前的可行边,时间上又一个 ,每一次匈牙利至多访问 条边,所以时间复杂度是 。
优化:
我们接受不了如此高的复杂度,所以要进行优化。发现如果修改了顶标之后再从最开始的点去跑匈牙利会浪费很多次无用的循环,因为当前修改了之后可能最少只增加了一条边,所以我们可以修改之后只取顶标的最小修改量所在的那个点进行下一次匈牙利,这样对于每一个要增广的点,访问的总的边数便至多是 条。但是这样会有一个问题,我们不可以在会回溯的时候修改匹配,这里用一个链表维护即可。
/*============================
* Author : ylsoi
* Problem : uoj80
* Algorithm : KM
* Time : 2018.5.31
* =========================*/
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<climits>
using namespace std;
void File(){
freopen("uoj80.in","r",stdin);
freopen("uoj80.out","w",stdout);
}
template<typename T>bool chkmax(T &_,T __){return _<__ ? (_=__,1) : 0;}
template<typename T>bool chkmin(T &_,T __){return _>__ ? (_=__,1) : 0;}
#define REP(i,a,b) for(register int i=a;i<=b;++i)
#define DREP(i,a,b) for(register int i=a;i>=b;--i)
#define MREP(i,x) for(register int i=beg[x];i;i=E[i].last)
#define mem(a) memset(a,0,sizeof(a))
#define ll long long
#define inf INT_MAX
const int maxn=400+10;
int nl,nr,n,m,G[maxn][maxn],S[maxn];
int lx[maxn],ly[maxn],be[maxn],lst[maxn],slack[maxn];
ll ans,sum[maxn];
bool visl[maxn],visr[maxn];
bool dfs(int las){
visr[las]=1;//!!!由于上一个点可能是新添加进去的,所以要把vis设为1
int u=be[las];
if(!u){
while(las){
be[las]=be[lst[las]];
las=lst[las];
}
return true;
}
visl[u]=1;
REP(i,1,n){
if(visr[i])continue;
int gap=lx[u]+ly[i]-G[u][i];
if(!gap){
lst[i]=las;//链表记录在交替树中的右边的点的关系,以便于修改。
visr[i]=1;
if(dfs(i))return true;
}
else if(chkmin(slack[i],gap))
lst[i]=las;
/*!!!重点,由于每一个访问不到的点都有可能成为下一个新开的点
但在记i的前驱的时候避免不了由多个点可以访问到i
但最后取得是gap值最小的哪一个,所以只有chkmin成功的时候才把las即为i的前驱。*/
}
return false;
}
void KM(){
REP(k,1,n){
int i=S[k];
mem(lst);
mem(visl);
mem(visr);
REP(j,1,n)slack[j]=inf;
be[0]=i;
int rt=0;
while(1){
if(dfs(rt))break;
int gap=inf;
REP(j,1,n)if(!visr[j] && chkmin(gap,slack[j]))
rt=j;
REP(j,1,n){
if(visl[j])lx[j]-=gap;
if(visr[j])ly[j]+=gap;
else slack[j]-=gap;
}
}
}
REP(i,1,n)ans+=lx[i]+ly[i];
}
bool cmp(int _,int __){return sum[_]>sum[__];}
int main(){
File();
scanf("%d%d%d",&nl,&nr,&m);
n=max(nl,nr);
REP(i,1,m){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
G[u][v]=w;
chkmax(lx[u],w);
sum[u]+=w;
}
REP(i,1,n)S[i]=i;
sort(S+1,S+n+1,cmp);
KM();
printf("%lld\n",ans);
int qu[maxn]={0};
REP(i,1,n)if(G[be[i]][i])qu[be[i]]=i;
REP(i,1,nl)printf("%d ",qu[i]);
return 0;
}
许多的东西是我自己的见解,避免不了有错误,如有发现请指出,谢谢。