首先要介绍下二分图。
二分图又称为二部图,是图论中的一个特殊模型。
设有一无向图G,G的所有顶点可以分割为两个互不相交的子集A和B,且图中每一条边所关联的两个点分属于两个集合A,B,那么图G便是一个二分图。
2. 最大匹配
对于一个二分图G,存在一个子集M,使得M的边集中的任意两条边都不关联同一个顶点,则M是图G的一个匹配。
显然,边数最大的子集M即为图的最大匹配。
举个例子:有一批男女,异性之间互有好感,不存在同性恋(嘻嘻),一个人可以同时对多名异性有好感。这样我们就可以根据暗恋关系建图,互相有好感则连一条边。这样得到的就是一个二分图。因为我们可以将其分为男性和女性两个互不相交的子集,且两个子集之间有连线,同一个子集之中没有连线。 然后让我们当一回月老,进行牵线。规定不准脚踏多只船,一个人只能和一名异性牵手成功。而且要根据好感决定,互相没有好感的不能牵手。那么怎样选择能使牵手的情侣数最多呢?这就是一个求最大匹配的问题了。
3. 求法?
那我们应该怎么求最大匹配呢?
有种方法就是枚举,枚举点,枚举边,然而数量一大就原地爆炸。奈何?要找优秀算法。
3.1 增广路径
我们引入增广路径的概念。
设二分图G中存在一条路径,它的起点和终点都是未被匹配的点(不属于子集M的点),不重复经过点和边,且路径中已匹配的边(属于M的边)和未匹配的边(不属于M的边)交替出现,则该路径是一条增广路径。
还是上面的男女关系问题。假设,有男性3人:A,B,C;女性3人:X,Y,Z。下面用<-->表示互相有好感:
A<-->X A<-->Z B<-->X C<-->Y C<-->Z
于是建出二分图。现在初步决定A-X牵手,C-Z牵手,那么就存在这样一条增广路径:
B<-->X<==>A<-->Z<==>C<-->Y (<-->表示未被选择的关系 <==>表示已被选择的关系)
这条路径中相邻点在图中都有连边。于是我们发现:
结论1:增广路径中边的个数必为奇数,相邻点必然是一个已匹配,一个未匹配。且始边和终边都不属于子集M(即都未被匹配)
这一结论显而易见。因为是交替的,且开始和结束要相同,边数自然是奇数,相邻点必然是一个已匹配,一个未匹配。又因为起点和终点都不属于子集M,与他们相关联的始边和终边自然不会属于子集M。(但若一个点属于子集M,与该点相关联的某一条边不一定属于子集M)
结论2:对增广路径取反,可以得到更大的匹配方案
这一点让我们用上面的例子来帮助理解。对于增广路径 B<-->X<==>A<-->Z<==>C<-->Y ,如果我们把已经匹配的边取出,反而放入没有匹配的边,即身份交换,我们可以得到 B<==>X<-->A<==>Z<-->C<==>Y,这依然是一条交替路,但是匹配数多了1。这样操作之后依旧符合要求(没有脚踏多只船,没有同性相吸),却得到了更优解。是不是很妙呀?所以由这一条结论我们可以得出下一条结论:
结论3:若找得到增广路径,则当前方案不是最大匹配;反之亦然。
这根据结论二很容易得出。
综上所述,我们知道,求最大匹配,一个关键就是求增广路径。对于一个新图,不停地寻找增广路径,不停地优化匹配方案,直到找不到增广路径为止这便是匈牙利算法。
那么程序如何实现?
3.2 Hungary程序实现
为了方便实现,我们采用枚举顶点扩展增广路的方式。定义一个函数如下(伪代码)
func found(x):boolean //布尔类型函数 x为枚举到的A子集顶点下标。采用dfs找增广路
for i 1 to m //枚举B子集顶点
{
if (not used)and(u{x,i}) //当点x与点i间有边连接,且该点未被访问过时
{
used => TRUE //更新该点为已访问
if (match=0)or(found(match)) //当这个点未被匹配,或者已经匹配了但它的匹配点可以扩出一条增广路
{
match => x //该点的匹配点改为x
exit(TRUE) //返回真,表示已找到
}
}
}
exit(FALSE) //运行至此搜遍了点但仍未找到,则返回假,说明该点无增广路可以扩展
main{ //主程序中调用
for i 1 to n //枚举A子集中的顶点
{
used => FALSE //初始化访问列表(均未访问过)
if found(i) //如果以点i拓展出了增广路
{
ans => ans+1 //那么匹配数加1
}
}
}
上面便是代码的实现过程。简洁易懂。其中used match可以用一维数组储存,边集u可用邻接矩阵或邻接表存储。
要注意两次的循环中是按A、B两子集的顶点数分别循环的,n表示A子集顶点数,m表示B子集顶点数。
下面是完整Pascal模板
var
match:array[0..1001]of longint; //记录B子集中元素所相匹配的A子集元素下标
used:array[0..1001]of boolean; //记录B子集中元素是否被访问过
a:array[0..1001,0..1001]of boolean; //邻接矩阵存储边
i,j,k,n,m,l,ans,t:longint;
function found(x:longint):boolean; //寻找增广路径
var
i,j:longint;
begin
for i:=1 to m do //枚举B子集中元素
if (a[x,i])and(not used[i]) then //如果两点之间有边且没有访问过
begin
used[i]:=TRUE; //记录为已访问
if (match[i]=0)or(found(match[i])) then //如果该点未被匹配,或相邻点能引出另一条增广路径
begin
match[i]:=x; //更新
exit(TRUE); //返回已找到
end;
end;
exit(FALSE); //无法找到
end;
begin
{文件操作}
//输入部分可做改动
readln(n,m,t); //该模板中N为A子集点数,M为B子集点数,T为边数
for i:=1 to n do
for j:=1 to m do
a[i,j]:=FALSE;
for i:=1 to t do
begin
readln(j,k);
a[j,k]:=TRUE;
end;
//输入部分可做改动
//Hungary算法,计算最大匹配数
ans:=0; //匹配数清零
fillchar(match,sizeof(match),0); //匹配子集清空
for i:=1 to n do //枚举A子集中元素
begin
fillchar(used,sizeof(used),FALSE); //每次都要清空访问列表
if found(i) then ans:=ans+1; //如果从点i出发能找到增广路径就将匹配数加1
end;
//输出部分可做改动
writeln(ans);
//输出部分可做改动
{文件操作}
end.
4. Hungary应用实战
4.1 luogu P1129 矩阵游戏
二分图匹配,难点在建图。我们可以发现,不论如何操作,原本在同一列或行黑色方格不可能分离到不同列或行,原本不在同一列或行的黑色方格也不可能合并到同一列或行。抓住这一点,就可以想到一些微妙的关系。将行与列作为二分子集,某行与某列交点有黑色方格,则该行与该列之间连边,最后判断匹配数是否达到行列数。数据量不大,由此用匈牙利算法很快水过。
var
used:array[0..201]of boolean;
match:array[0..201]of longint;
a:array[0..201,0..201]of boolean;
i,j,k,n,m,l,t,cnt:longint;
function found(x:longint):boolean;
var
i,j:longint;
begin
for i:=1 to n do
if (a[x,i])and(not used[i]) then
begin
used[i]:=TRUE;
if (match[i]=0)or(found(match[i])) then
begin
match[i]:=x;
exit(TRUE);
end;
end;
exit(FALSE);
end;
begin
readln(t);
repeat
t:=t-1;
readln(n);
l:=0;
for i:=1 to n do
for j:=1 to n do
begin
read(k);
if k=1 then begin a[i,j]:=TRUE;l:=l+1;end else a[i,j]:=FALSE;
end;
if l=n then writeln('Yes') else writeln('No');
end;
until t<=0;
end.
这道题稍稍复杂些,感觉有点绕。但是最大匹配的算法显而易见,关键是如何处理。
二分图的两个子集分别是需要留宿的学生和床。学生和能睡的床间连线,然后进行匹配,最后判断匹配数是否达到要求即可。
有几点要注意:一是n个学生并非全部要留宿,最后比较时不应与n比较,而要与留宿的学生比较; 二是输入中每个学生自己与自己的关系没有标为1,但是自己当然是可以睡自己的床的,所以不要忘记自己与自己的床也要连线!三是n个学生也不是所有学生都有床!所以dfs的时候要记得判断该学生是否是在校生。
var
match:array[0..51]of longint;
used,could,bed:array[0..51]of boolean;
a:array[0..51,0..51]of boolean;
i,j,k,n,m,l,cnt,t:longint;
function found(x:longint):boolean;
var
i,j:longint;
begin
for i:=1 to n do
if (a[x,i])and(not used[i])and(could[i]) then
begin
used[i]:=TRUE;
if (match[i]=0)or(found(match[i])) then
begin
match[i]:=x;
exit(TRUE);
end;
end;
exit(FALSE);
end;
begin
readln(t);
repeat
t:=t-1;
readln(n);
m:=0;
for i:=1 to n do
begin
read(k);
if k=1 then could[i]:=TRUE else could[i]:=FALSE;
end;
for i:=1 to n do
begin
read(k);
if (could[i]) then if k=0 then bed[i]:=TRUE else bed[i]:=FALSE;
if not(could[i]) then bed[i]:=TRUE;
if bed[i] then m:=m+1;
end;
for i:=1 to n do
for j:=1 to n do
begin
read(k);
if (k=1)or(i=j) then a[i,j]:=TRUE else a[i,j]:=FALSE;
end;
cnt:=0;
fillchar(match,sizeof(match),0);
for i:=1 to n do
begin
if not(bed[i]) then continue;
fillchar(used,sizeof(used),FALSE);
if found(i) then cnt:=cnt+1;
end;
if (cnt>=m)and(m<=n) then writeln('^_^') else writeln('T_T');
until t<=0;
end.
典型的最大匹配问题,难点也在于选取建图要素。
也许我把每个相互有冲突的点都连线,转而求、、、求什么?最大独立集?照样不够优秀。
要是这样想:炸弹是沿横向和纵向两个方向爆炸,那我们就分割横向和纵向。把每一行、列中的联通条分别标号,成为两个子集。当横向联通条与纵向联通条相交在某一块空地上时,将他们连边,说明会相互影响。然后求最大匹配即可。预处理过程可能有些繁琐。其实只要先记下某一个方向下的联通条标号,之后在对另一个方向的联通条标号的同时进行判断,当某一格是空地时就对该格上两个方向下的联通条连边就行了。
var
match:array[0..2001]of longint;
used:array[0..2001]of boolean;
a:array[0..2001,0..2001]of boolean;
f,x,y:array[0..51,0..51]of longint;
i,j,k,n,m,l,ans,cnt,tmp:longint;
c:char;
canset:boolean;
function found(x:longint):boolean;
var
i:longint;
begin
for i:=1 to tmp do
if (a[x,i])and(not used[i]) then
begin
used[i]:=TRUE;
if (match[i]=0)or(found(match[i])) then
begin
match[i]:=x;
exit(TRUE);
end;
end;
exit(FALSE);
end;
begin
readln(n,m);
for i:=1 to n do
begin
for j:=1 to m-1 do
begin
read(c);
case c of
'#':f[i,j]:=9;
'x':f[i,j]:=5;
'*':f[i,j]:=1;
end;
end;
readln(c);
case c of
'#':f[i,m]:=9;
'x':f[i,m]:=5;
'*':f[i,m]:=1;
end;
end;
cnt:=0;
for i:=1 to n do
begin
cnt:=cnt+1;
for j:=1 to m do
if f[i,j]<>9 then x[i,j]:=cnt
else begin cnt:=cnt+1;x[i,j]:=0;end;
end;
fillchar(a,sizeof(a),FALSE);
tmp:=0;
for j:=1 to m do
begin
tmp:=tmp+1;
for i:=1 to n do
begin
if f[i,j]<>9 then y[i,j]:=tmp
else begin tmp:=tmp+1;y[i,j]:=0;end;
if (x[i,j]<>0)and(y[i,j]<>0)and(not a[x[i,j],y[i,j]])and(f[i,j]=1)
then a[x[i,j],y[i,j]]:=TRUE;
end;
end;
fillchar(match,sizeof(match),0);
ans:=0;
for i:=1 to cnt do
begin
fillchar(used,sizeof(used),FALSE);
if found(i) then ans:=ans+1;
end;
writeln(ans);
end.