【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

奔小康赚大钱

Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)
Total Submission(s): 1836    Accepted Submission(s): 798

Problem Description
传说在遥远的地方有一个非常富裕的村落,有一天,村长决定进行制度改革:重新分配房子。
这可是一件大事,关系到人民的住房问题啊。村里共有n间房间,刚好有n家老百姓,考虑到每家都要有房住(如果有老百姓没房子住的话,容易引起不安定因素),每家必须分配到一间房子且只能得到一间房子。
另一方面,村长和另外的村领导希望得到最大的效益,这样村里的机构才会有钱.由于老百姓都比较富裕,他们都能对每一间房子在他们的经济范围内出一定的价格,比如有3间房子,一家老百姓可以对第一间出10万,对第2间出2万,对第3间出20万.(当然是在他们的经济范围内).现在这个问题就是村领导怎样分配房子才能使收入最大.(村民即使有钱购买一间房子但不一定能买到,要看村领导分配的).
 
Input
输入数据包含多组测试用例,每组数据的第一行输入n,表示房子的数量(也是老百姓家的数量),接下来有n行,每行n个数表示第i个村名对第j间房出的价格(n<=300)。
 
Output
请对每组数据输出最大的收入值,每组的输出占一行。
 
Sample Input
2 100 10 15 23
 
Sample Output
123

【分析】

  只是打一下模版。最佳二分匹配KM算法。

来来上代码:

 #include<cstdio>
#include<cstdlib>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
#define Maxn 310
#define Maxm 160010
#define INF 0xfffffff struct node
{
int x,y,c,next;
}t[Maxm];int len;
int first[Maxn]; void ins(int x,int y,int c)
{
t[++len].x=x;t[len].y=y;t[len].c=c;
t[len].next=first[x];first[x]=len;
} int mymin(int x,int y) {return x<y?x:y;}
int mymax(int x,int y) {return x>y?x:y;} int n;
int match[Maxn],lx[Maxn],ly[Maxn];//lx,ly 可行顶标
bool visx[Maxn],visy[Maxn];
int slack[Maxn]; bool ffind(int x)
{
visx[x]=;
for(int i=first[x];i;i=t[i].next) if(!visy[t[i].y])
{
int y=t[i].y;
if(t[i].c==lx[x]+ly[y])
{
visy[y]=;
if(!match[y]||ffind(match[y]))//这里可以理解为有向的增广路,只在一段记录match
{
match[y]=x;
return ;
}
}
else slack[y]=mymin(slack[y],lx[x]+ly[y]-t[i].c);
}
return ;
} void solve()
{
memset(match,,sizeof(match));
memset(lx,,sizeof(lx));
memset(ly,,sizeof(ly));
for(int i=;i<=n;i++)
for(int j=first[i];j;j=t[j].next) lx[i]=mymax(lx[i],t[j].c); for(int i=;i<=n;i++)
{
for(int j=;j<=n;j++)
slack[j]=INF;
while()
{
memset(visx,,sizeof(visx));
memset(visy,,sizeof(visy));
if(ffind(i)) break;
int delta=INF;
for(int j=;j<=n;j++)
{
if(!visy[j])
{
delta=mymin(delta,slack[j]);
}
}
if(delta==INF) return;
for(int j=;j<=n;j++)
{
if(visx[j]) lx[j]-=delta;
if(visy[j]) ly[j]+=delta;
else slack[j]-=delta;
}
}
}
} int main()
{
while(scanf("%d",&n)!=EOF)
{
len=;
memset(first,,sizeof(first));
for(int i=;i<=n;i++)
{
for(int j=;j<=n;j++)
{
int x;
scanf("%d",&x);
ins(i,j,x);
}
}
solve();
int ans=;
for(int i=;i<=n;i++) ans+=lx[i]+ly[i];
printf("%d\n",ans);
}
return ;
}

HDU 2255


基础:

二分图:简单来说,如果图中点可以被分为两组,并且使得所有边都跨越组的边界,则这就是一个二分图。准确地说:把一个图的顶点划分为两个不相交集 UU 和VV ,使得每一条边都分别连接UU、VV中的顶点。如果存在这样的划分,则此图为一个二分图。二分图的一个等价定义是:不含有「含奇数条边的环」的图。图 1 是一个二分图。为了清晰,我们以后都把它画成图 2 的形式。

匹配:在图论中,一个「匹配」(matching)是一个边的集合,其中任意两条边都没有公共顶点。例如,图 3、图 4 中红色的边就是图 2 的匹配。

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)  【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)  【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)  【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

我们定义匹配点匹配边未匹配点非匹配边,它们的含义非常显然。例如图 3 中 1、4、5、7 为匹配点,其他顶点为未匹配点;1-5、4-7为匹配边,其他边为非匹配边。

最大匹配:一个图所有匹配中,所含匹配边数最多的匹配,称为这个图的最大匹配。图 4 是一个最大匹配,它包含 4 条匹配边。

完美匹配:如果一个图的某个匹配中,所有的顶点都是匹配点,那么它就是一个完美匹配。图 4 是一个完美匹配。显然,完美匹配一定是最大匹配(完美匹配的任何一个点都已经匹配,添加一条新的匹配边一定会与已有的匹配边冲突)。但并非每个图都存在完美匹配。

举例来说:如下图所示,如果在某一对男孩和女孩之间存在相连的边,就意味着他们彼此喜欢。是否可能让所有男孩和女孩两两配对,使得每对儿都互相喜欢呢?图论中,这就是完美匹配问题。如果换一个说法:最多有多少互相喜欢的男孩/女孩可以配对儿?这就是最大匹配问题。

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

基本概念讲完了。求解最大匹配问题的一个算法是匈牙利算法,下面讲的概念都为这个算法服务。

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

交替路:从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边…形成的路径叫交替路。

增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发的点不算),则这条交替路称为增广路(agumenting path)。例如,图 5 中的一条增广路如图 6 所示(图中的匹配点均用红色标出):

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

增广路有一个重要特点:非匹配边比匹配边多一条。因此,研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份交换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配边数目比原来多了 1 条。

我们可以通过不停地找增广路来增加匹配中的匹配边和匹配点。找不到增广路时,达到最大匹配(这是增广路定理)。匈牙利算法正是这么做的。在给出匈牙利算法 DFS 和 BFS 版本的代码之前,先讲一下匈牙利树。

匈牙利树一般由 BFS 构造(类似于 BFS 树)。从一个未匹配点出发运行 BFS(唯一的限制是,必须走交替路),直到不能再扩展为止。例如,由图 7,可以得到如图 8 的一棵 BFS 树:

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)   【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)    【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

这棵树存在一个叶子节点为非匹配点(7 号),但是匈牙利树要求所有叶子节点均为匹配点,因此这不是一棵匈牙利树。如果原图中根本不含 7 号节点,那么从 2 号节点出发就会得到一棵匈牙利树。这种情况如图 9 所示(顺便说一句,图 8 中根节点 2 到非匹配叶子节点 7 显然是一条增广路,沿这条增广路扩充后将得到一个完美匹配)。



KM算法(最佳完美匹配)

二分图的最佳完美匹配


如果二分图的每条边都有一个权(可以是负数),要求一种完备匹配方案,使得所有匹配边的权和最大,记做最佳完美匹配。(特殊的,当所有边的权为1时,就是最大完备匹配问题) 
我们使用KM算法解决该问题。

KM(Kuhn and Munkres)算法,是对匈牙利算法的一种贪心扩展,如果对匈牙利算法还不够明白,建议先重新回顾一下匈牙利算法。

KM是对匈牙利算法的一种贪心扩展,这种贪心不是对边的权值的贪心,算法发明者引入了一些新的概念,从而完成了这种扩展。

可行顶标


对于原图中的任意一个结点,给定一个函数L(node)求出结点的顶标值。我们用数组lx(x)记录集合X中的结点顶标值,用数组ly(y)记录集合Y中的结点顶标值。 
并且,对于原图中任意一条边edge(x,y),都满足

lx(x)+ly(y)>=weight(x,y)

相等子图


相等子图是原图的一个生成子图(生成子图即包含原图的所有结点,但是不包含所有的边),并且该生成子图中只包含满足

lx(x)+ly(y)=weight(x,y)

的边,这样的边我们称之为可行边

算法原理

  • 定理:如果原图的一个相等子图中包含完备匹配,那么这个匹配就是原图的最佳二分图匹配。

  • 证明 :由于算法中一直保持顶标的可行性,所以任意一个匹配的权值之和肯定小于等于所有结点的顶标之和,则相等子图中的完备匹配肯定是最优匹配。

这就是为什么我们要引入可行顶标相等子图的概念。 
上面的证明可能太过抽象,我们结合图示更直观的表述。

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

该图表示原图,且X=1,2,3,Y=4,5,6,给出权值

weight(1,4)=5 
weight(1,5)=10 
weight(1,6)=15 
weight(2,4)=5 
weight(2,5)=10 
weight(3,4)=10 
weight(3,6)=20

对于原图的任意一个匹配M

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)

那么对于

edge(1,6)weight(1,6)=15 
edge(2,5)weight(2,5)=10 
edge(3,4)weight(3,4)=10

都满足

lx(x)+ly(y)>=weight(x,y)

所以

∑i=1xi∈Xlx(xi)+∑i=1yi∈Yly(yi)=K>=∑weight(xi,yi)

可以看出,一个匹配中的边权之和最大为K。

那么很显然,当一个匹配G∗的边权之和恰好为K时,那么G∗就是二分图的最佳完美匹配。

如果对于每一条边edge(xi,yi)都满足

lx(xi)+ly(yi)==weight(xi,yi)

那么

∑i=1xi∈Xlx(xi)+∑i=1yi∈Yly(yi)=K=∑weight(xi,yi)

相等子图的完备匹配(完美匹配)即满足上述条件(因为相等子图的每条边都是可行边,可行边满足lx(xi)+ly(yi)=weight(xi,yi))所以当相等子图有完备匹配的时候,原图有最佳完美匹配。


KM的算法流程

流程


Kuhn-Munkras算法(即KM算法)流程:

  1. 初始化可行顶标的值 (设定lx,ly的初始值)
  2. 用匈牙利算法寻找相等子图的完备匹配
  3. 若未找到增广路则修改可行顶标的值
  4. 重复(2)(3)直到找到相等子图的完备匹配为止

KM算法的核心部分即控制修改可行顶标的策略使得最终可到达一个完美匹配。

  1. 初始时,设定lx[xi]为和xi相关联的edge(xi,yj)的最大权值,ly[yj]=0,满足公式lx[xi]+ly[yj]>=weight(xi,yj)
  2. 当相等子图中不包含完备匹配的时候(也就是说还有增广路),就适当修改顶标。直到找到完备匹配为止。(整个过程在匈牙利算法中执行)

现在我们的问题是,遵循什么样的原则去修改顶标的值?

对于正在增广的增广路径上属于集合X的所有点减去一个常数delta,属于集合Y的所有点加上一个常数delta。

为什么要这样做呢,我们来分析一下: 
对于图中任意一条边edge(i,j) (其中xi∈X,xj∈Y)权值为weight(i,j)

  1、如果i和j都属于增广路,那么lx[i]−delta+ly[j]−+delta=lx[i]+ly[j]值不变,也就说edge(i,j)可行性不变,原来是相等子图的边就还是,原来不是仍然不是
  2、如果i属于增广路,j不属于增广路,那么lx[i]−delta+ly[j]的值减小,也就是原来这条边不在相等子图中(否则j就会被遍历到了),现在可能就会加入到相等子图。
  3、如果i不属于增广路,j属于增广路,那么lx[i]+ly[j]+delta的值增大,也就是说原来这条边不在相等子图中(否则j就会被遍历到了),现在还不可能加入到相等子图
  4、如果i,j都不属于增广路,那么lx[i]和ly[j]都不会加减常数delta值不变,可行性不变

//看成匈牙利树更容易理解。

你每次i++,选一个为匹配的x点进行增广,找到以i为起点的匈牙利树。

如果长这样:

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法),那么你就找到了增广路,i的工作就完成了,i++即可,不需改变顶标。

否则,你的匈牙利树长这样,

【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)你会把整棵树完整的找一遍。我的意思是,能走到的点的vis值都会标记到的。

上面说的“在增广路上”实际上是“在匈牙利树上”。

解释一下第2、3种情况。

2、若x在匈牙利树上,y不在,说明x->y并非相等子图上的边,不然因为找不到增广路,相等子图上的边我都会遍历一遍,所有点都会打上vis标记。

3、若x不在匈牙利树上,y在,x一定是i后面的点,它的匹配会在后面使其成立,所以可以看成不是相等子图上的边?【其实这个我还觉得有点迷吧。。但是顶标只是增加,所以不会错的。

//有slack的y必定有一条边连向一个打过vis标记的点【看代码这个就明白了】

【以上是个人的傻逼见解啊。。2017-04-25 10:44:23】

这 样,在进行了这一步修改操作后,图中原来的可行边仍可行,而原来不可行的边现在则可能变为可行边。那么delta的值应取多少?

观察上述四种情况,只有第二类边(xi∈X,yj∈Y)的可行性经过修改可以改变。

因为对于每条边都要满足lx(i)+ly(j)>=weight(i,j),这一性质绝对不可以改变,所以取第二种情况的 lx[i]+ly[j]−weight(i,j)的最小值作为delta。

证明 :

delta=Min(lx[i]+ly[j]−weight(i,j))=lx[i]+ly[j]−Max(weight(i,j))

第二类边 :

lx[i]−delta+ly[j]=lx[i]−lx[i]−ly[i]+Max(weight(i,j))+ly[j]=Max(weight)>=weight(i,j)

成立

下面我们重新回顾一下整个KM算法的流程 :

    1. 可行顶标:每个点有一个标号,记(xi∈X,yj∈Y)。如果对于图中的任意边edge(i,j)都有lx[i]+ly[j]>=weight(i,j),则这一顶标是可行的。特别地,对于lx[i]+ly[j]=weight(i,j),称为可行边(也就是相等子图里的边)
    2. KM 算法的核心思想就是通过修改某些点的标号(但要满足点标始终是可行的),不断增加图中的可行边总数,直到图中存在仅由可行边组成的完全匹配为止,此时这个 匹配一定是最佳的(证明上文已经给出)
    3. 初始化:lx[i]=Max(edge(i,j)),xi∈X,edge(i,j)∈E,ly[j]=0。这个初始顶标显然是可行的,并且,与任意一个X方点关联的边中至少有一条可行边
    4. 从每个X方点开始DFS增广。DFS增广的过程与最大匹配的Hungary算法基本相同,只是要注意两点:一是只找可行边,二是要把搜索过程中遍历到的X方点全部记下来,以便进行后面的修改
    5. 增广的结果有两种:若成功(找到了增广路),则该点增广完成,进入下一个点的增广。若失败(没有找到增广路),则需要改变一些点的标号,使得图中可行边的 数量增加。
    6. 修改后,继续对这个X方点DFS增广,若还失败则继续修改,直到成功为止


下面用图模拟:

初始化标杆使X标杆的值为斜体字的值。 
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
连接每条边并且使得x1和y3匹配,然后遍历x2,发现x2找不到合法增广路。 
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
把不合法路径上的x点都归为点集S,y点都归为T,将不在T中的y点和在S中的点尝试进行加边。 
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
找到两条边,更新顶标之后,成功形成增广路,运用匈牙利算法反选。 
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
给x3找一个合法的增广路,一下就找到了,直接反选,结束。 
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)
【HDU 2255】奔小康赚大钱 (最佳二分匹配KM算法)


KM算法的优化

KM算法可以优化到O(n3)

一个优化是对Y顶点引入松弛函数slack,slack[j]保存跟当前节点j相连的节点i的lx[i]+ly[j]−weight(i,j)的最小值,于是求delta时只需O(n)枚举不在交错树中的Y顶点的最小slack值即可。

松弛值可以在匈牙利算法检查相等子树边失败时进行更新,同时在修改标号后也要更新


转自:

http://www.renfei.org/blog/bipartite-matching.html

http://blog.csdn.net/sixdaycoder/article/details/47720471

http://blog.csdn.net/zxn0803/article/details/49999267


2016-10-26 21:12:20

上一篇:Java入门:JDK与Eclipse之类的集成开发工具的关系


下一篇:hdu 2255 奔小康赚大钱 (KM)