KM算法 最优匹配(最大权匹配) hdu 2255 奔小康赚大钱 最小权匹配 poj 2195 Going Home

    最大权二分匹配问题就是给二分图的每条边一个权值,选择若干不相交的边,得到的总权值最大。解决这个问题可以用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;
}


你可能感兴趣的:(KM算法 最优匹配(最大权匹配) hdu 2255 奔小康赚大钱 最小权匹配 poj 2195 Going Home)