KM是用来求带权二分图的最优匹配的一种算法。
我们要求一个二分图的最优匹配,直接求的话肯定是不太好求的,因为每一条边都带有自己的边权,而我们是要求得一个匹配,使得所有的边权加起来值最大。
然后智商超级高的KM算法发明者将这个问题转化为了求一个带权二分图的完备匹配的问题。
既然对于每一条边都满足上述性质,如果我们能够在二分图中找到一种全部由可行边构成的完备匹配,使得任意 u,v u , v 都满足 lx[u]+ly[v]=w(u,v) l x [ u ] + l y [ v ] = w ( u , v ) ,那么我们就可以证明这个完备匹配是最优的(参见顶标的性质)。
既然我们可以这样来求出最优匹配,那么就要构造这样的顶标集合满足上面的所有条件。
初始的时候我们把每个左边的点的顶标设为和它相连的权值最大的边的权值。然后用匈牙利算法去一遍一遍地跑增广路,直到是全部由可行边构成的完备匹配为止。但是数据给的图不一定有完备匹配怎么办?我们可以构造一个完全图,原图中没有的边权值设为0。
显然这样不一定可以找到满足条件的完备匹配。所以我们要在满足性质的情况下修改点的顶标使得可以找得到满足条件的完备匹配。考虑修改目前在交错树中的边,如果将左边的点的顶标减去了 gap g a p ,那么相应的,对于已经匹配上的边,右边的对应点的顶标要加上 gap g a p ,这样原来的边的可行性才不会发生变化。
所以,为了找到由可行边构成的增广路,我们要尽量修改顶标使得可行边的条数增多,对应上面的第四点,修改量即为 lx[u]+ly[v]−va[u][v] l x [ u ] + l y [ v ] − v a [ u ] [ v ] 。为了让整个图都满足限制条件,修改量即为所有满足u端在在交错树中,而v端不在交错树的 min(lx[u]+ly[v]−va[u][v]) min ( l x [ u ] + l y [ v ] − v a [ u ] [ v ] ) 。
通过上面的数学证明和分析,可以得到KM算法的步骤:
网上有很多的分析是不严谨的,我们按照上面的方法,每一个点找一次增广路,时间上一个 n n ,对于每一次寻找,可能要将n个点添加进目前的可行边,时间上又一个 n n ,每一次匈牙利至多访问 n2 n 2 条边,所以时间复杂度是 O(n4) O ( n 4 ) 。
我们接受不了如此高的复杂度,所以要进行优化。发现如果修改了顶标之后再从最开始的点去跑匈牙利会浪费很多次无用的循环,因为当前修改了之后可能最少只增加了一条边,所以我们可以修改之后只取顶标的最小修改量所在的那个点进行下一次匈牙利,这样对于每一个要增广的点,访问的总的边数便至多是 n2 n 2 条。但是这样会有一个问题,我们不可以在会回溯的时候修改匹配,这里用一个链表维护即可。
/*============================
* Author : ylsoi
* Problem : uoj80
* Algorithm : KM
* Time : 2018.6.16(updated)
* =========================*/
#include
#include
#include
#include
#include
#include
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,m,n,dis[maxn][maxn],Ans[maxn];
int lw[maxn],rw[maxn],slack[maxn],be[maxn],fa[maxn];
bool vis[maxn];
ll ans;
bool Hungary(int las){
vis[las]=1;//这个点有可能是新拓展的路
int u=be[las];
if(!u){
while(las){
be[las]=be[fa[las]];
las=fa[las];
}
return true;
}
REP(i,1,n){
int gap=lw[u]+rw[i]-dis[u][i];
if(vis[i])continue;//要在前面就判断是否重复访问
if(!gap){
vis[i]=1;
fa[i]=las;//链表记录在交替树中的右边的点的关系,以便于修改。
if(Hungary(i))return true;
}
else if(chkmin(slack[i],gap))
fa[i]=las;/*!!!重点,由于每一个访问不到的点都有可能成为下一个新开的点
但在记i的前驱的时候避免不了由多个点可以访问到i
但最后取得是gap值最小的哪一个,所以只有chkmin成功的时候才把las即为i的前驱。*/
}
return false;
}
void KM(){
REP(i,1,n){
mem(fa);
mem(vis);
REP(j,1,n)slack[j]=inf;
be[0]=i;
int fr=0;
while(1){
if(Hungary(fr))break;
int gap=inf;
REP(j,1,n)if(!vis[j] && chkmin(gap,slack[j]))
fr=j;
REP(j,0,n)if(vis[j]){
lw[be[j]]-=gap;
if(j)rw[j]+=gap;
}
else slack[j]-=gap;
}
}
REP(i,1,n)ans+=lw[i]+rw[i];
printf("%lld\n",ans);
REP(i,1,n)if(dis[be[i]][i])Ans[be[i]]=i;
REP(i,1,nl)printf("%d ",Ans[i]);
}
void init(){
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);
dis[u][v]=w;
chkmax(lw[u],w);
}
}
int main(){
File();
init();
KM();
return 0;
}
许多的东西是我自己的见解,避免不了有错误,如有发现请指出,谢谢。