最大权二分匹配问题就是给二分图的每条边一个权值,选择若干不相交的边,得到的总权值最大。解决这个问题可以用KM算法。理解KM算法需要首先理解“可行顶标”的概念。可行顶标是指关于二分图两边的每个点的一个值lx[i]或ly[j],保证对于每条边w[i][j]都有lx[i]+ly[j]-w[i][j]>=0。如果所有满足lx[i]+ly[j]==w[i][j]的边组成的导出子图中存在一个完美匹配,那么这个完美匹配肯定就是原图中的最大权匹配。理由很简单:这个匹配的权值之和恰等于所有顶标的和,由于上面的那个不等式,另外的任何匹配方案的权值和都不会大于所有顶标的和。
但问题是,对于当前的顶标的导出子图并不一定存在完美匹配。这时,可以用某种方法对顶标进行调整。调整的方法是:根据最后一次不成功的寻找交错路的DFS,取所有i被访问到而j没被访问到的边(i,j)的lx[i]+ly[j]-w[i][j]的最小值d。将交错树中的所有左端点的顶标减小d,右端点的顶标增加d。经过这样的调整以后:原本在导出子图里面的边,两边的顶标都变了,不等式的等号仍然成立,仍然在导出子图里面;原本不在导出子图里面的边,它的左端点的顶标减小了,右端点的顶标没有变,而且由于d的定义,不等式仍然成立,所以他就可能进入了导出子图里。
初始时随便指定一个可行顶标,比如说lx[i]=max{w[i][j]|j是右边的点},ly[i]=0。然后对每个顶点进行类似Hungary算法的find过程,如果某次find没有成功,则按照这次find访问到的点对可行顶标进行上述调整。这样就可以逐步找到完美匹配了。
值得注意的一点是,按照上述d的定义去求d的话需要O(N^2)的时间,因为d需要被求O(N^2)次,这就成了算法的瓶颈。可以这样优化:设slack[j]表示右边的点j的所有不在导出子图的边对应的lx[i]+ly[j]-w[i][j]的最小值,在find过程中,若某条边不在导出子图中就用它对相应的slack值进行更新。然后求d只要用O(N)的时间找到slack中的最小值就可以了。
如果是求最小权匹配,只需要把那个不等式反一下就行了。算法需要作出的改变是:lx的初值为所有临界边中的最小值,find中t反号。
最大权匹配
/* hdu 2255 奔小康赚大钱 题意:有n个村民n个房子,每个村名对每个房子都有一个报价,求报价和最大的安排 最大权匹配 也就是最优匹配 这儿有个KM算法的讲解 http://blog.csdn.net/hqd_acm/article/details/5829682 http://www.byvoid.com/blog/tag/%E6%9C%80%E5%B0%8F%E6%9D%83%E5%8C%B9%E9%85%8D/zh-hant/ */ #include<stdio.h> #include<string.h> #define N 305 int map[N][N],match[N],lx[N],ly[N],slack[N],visx[N],visy[N]; int n; int hungray(int i)//适应KM算法的 匈牙利算法 { int j; visx[i]=1; for(j=1;j<=n;++j)// { if(visy[j]) continue; if(lx[i]+ly[j]==map[i][j])//若j在相等子图里 与普通匈牙利算法一样 { visy[j]=1; if(match[j]==-1||hungray(match[j])) { match[j]=i; return 1; } }else if(slack[j]>(lx[i]+ly[j]-map[i][j]))//不在相等子图里 并且可以使slack更小 更新之 slack[j]=lx[i]+ly[j]-map[i][j]; } return 0; } int km() { memset(match, -1, sizeof(match)); int i,j,d; memset(lx,0,sizeof(lx)); memset(ly,0,sizeof(ly)); for(i=1;i<=n;i++)//lx取与i连接的边的最大权 for(j=1;j<=n;++j) if(map[i][j]>lx[i]) lx[i]=map[i][j]; for(i=1;i<=n;++i)//对每一个点进行匹配 { for(j=1;j<=n;j++)//对每一个点来说,需要一直更新lx、ly 直到找到交错路 slack存的是那个较小的差值,所以求每个点前初始化成最大 slack[j]=0x7fffffff; while(1)//当找到交错路的时候 跳出 { memset(visx,0,sizeof(visx)); memset(visy,0,sizeof(visy));//初始化访问标记 if(hungray(i)) break; d=0x7fffffff;//寻找最小的改变量 for(j=1;j<=n;++j) if(!visy[j]&&slack[j]<d) d=slack[j]; for(j=1;j<=n;++j)//改变 { if(visx[j]) lx[j]-=d; if(visy[j]) ly[j]+=d; else slack[j]-=d;//对于通过这次更新顶标而进入相等子图的j来说,这不已经没有意义 //但是 对于还没有进入相等子图的j来说 他们相连的i已经-d了,所以那个差会减小d 故这里修改slack } } } d=0; for(i=1;i<=n;i++)//所有边的权都表示在了点上 d+=lx[i],d+=ly[i]; return d; } int main() { int i,j; while(scanf("%d",&n)!=EOF) { for(i=1;i<=n;i++) for(j=1;j<=n;++j) scanf("%d",&map[i][j]); printf("%d\n",km()); } return 0; }
/* poj 2195 Going Home 学习了二分图匹配,所以又用 最小权匹配 些了一边,比 最小费用最大流 快了一些 最小权匹配 与最大权匹配差不多 只需要把 边的权取相反数 然后过程跟最大权匹配一样 最后 边权和 是一个负数 再取相反数即可 */ #include<iostream> #include<cmath> using namespace std; int m,n; struct node//表示人或房子位置 { int x,y; }; struct edge//用邻接表存边 { int v,f,next; }e[100000]; node *man,*house;//人和房子的数组 int *match,*lx,*ly,*slack,*visx,*visy,*head,nman;//匹配 x的顶标 y的顶标 优化数组 x访问标志 y访问标志 邻接表头节点 人的数量 char map[110][110]; int hungray(int i)//匈牙利算法 { int j,v; visx[i]=1; for(j=head[i];j!=-1;j=e[j].next) { v=e[j].v; if(visy[v]) continue; if(lx[i]+ly[v]==e[j].f) { visy[v]=1; if(match[v]==-1||hungray(match[v])) { match[v]=i; return 1; } }else if(slack[v]>lx[i]+ly[v]-e[j].f) slack[v]=lx[i]+ly[v]-e[j].f; } return 0; } int km()//km { memset(match,-1,sizeof(int)*nman); int i,j,d; memset(ly,0,sizeof(int)*nman); for(i=0;i<nman;++i) { for(j=0;j<nman;++j) slack[j]=0x7fffffff; while(1) { memset(visx,0,sizeof(int)*nman); memset(visy,0,sizeof(int)*nman); if(hungray(i)) break; d=0x7fffffff; for(j=0;j<nman;++j) if(!visy[j]&&slack[j]<d) d=slack[j]; for(j=0;j<nman;++j) { if(visx[j]) lx[j]-=d; if(visy[j]) ly[j]+=d; else slack[j]-=d; } } } d=0; for(i=0;i<nman;++i) d+=lx[i],d+=ly[i]; return -d;//取相反数 } int main() { int i,j,yong; while(cin>>n>>m,m+n) { nman=0;//初始化 yong=0; for(i=0;i<n;++i)//读数据 for(j=0;j<m;++j) { cin>>map[i][j]; if(map[i][j]=='m') nman++; } match=new int[nman];//申请空间 man=new node[nman];// house=new node[nman];// lx=new int[nman];// ly=new int[nman];// slack=new int[nman];// visx=new int[nman];// visy=new int[nman];// head=new int[nman];// int jman=0,jhouse=0; for(i=0;i<n;++i)//统计人和房子的位置 for(j=0;j<m;++j) { if(map[i][j]=='m') { man[jman].x=i; man[jman++].y=j; }else if(map[i][j]=='H') { house[jhouse].x=i; house[jhouse++].y=j; } } memset(head,-1,sizeof(int)*nman);//计算人和房子的距离 建边 for(i=0;i<nman;++i) { int max=-(0x7fffffff);//在计算的同时给lx数组赋值,就是和i相连的边的最大权 for(j=0;j<nman;++j) { e[yong].v=j; e[yong].f=-(abs(man[i].x-house[j].x)+abs(man[i].y-house[j].y));//因为求最小权匹配 所以用权的相反数 if(max<e[yong].f) max=e[yong].f;//更新max e[yong].next=head[i]; head[i]=yong++; } lx[i]=max;//给lx赋值 } cout<<km()<<endl;//计算 输出 delete[] match;//释放空间 delete[] man; delete[] house; delete[] lx; delete[] ly; delete[] slack; delete[] visx; delete[] visy; delete[] head; } return 0; }