二分图最大匹配。
去年这时候学过,学得很模糊,今年重温。
先给出一些定义:
二分图: 设G是一个图。如果存在VG的一个划分X,Y,使得G的任何一条边的一个端点在X中,另一个端点在Y中,则称G为二分图,记作G=(X,Y,E)。
二分图匹配: 给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。
二分图最大匹配:在图G的所有二分图匹配中包含边数最多的一个匹配就是二分图最大匹配。
增广路: 若P是图G中一条连通两个未匹配顶点的路径,并且属M的边和不属M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。
找二分图的最大匹配,最朴素的算法自然是dfs枚举。这当然会超时的。
所以就要用高级的算法,于是有了 匈牙利算法。
个人觉得本质上还是枚举。只是因为引入了“增广路”的概念,所以枚举快了很多。
算法轮廓:
(1)置匹配M为空
(2)找出一条增广路径P,通过取反操作获得更大的匹配M’代替M
(3)重复(2)操作直到找不出增广路径为止
上述第二步是关键。为了达到“通过取反操作获得更大的匹配M’代替M”,只能通过枚举边来扩充增广路。用边来扩充增广路,自然有3种情况。
1)边的两个端点在这条增广路上。那么这条边肯定无法扩充增广路。
2)边的没有端点在这条增广路上。那么直接把这条边加到增广路中,增广路长度+2。
3)边的一个端点在这条增广路上。这个就比较麻烦了,可能整个增广路都要发生改变要通过递归来求解。
在程序实践时,又有了不同。程序中并没有很明显的“增广路”,更没有取反。
var
n:longint;
s:longint;
map:array[1..1000+1,1..1000+1]of boolean;//记录读入的边
mark:array[1..1000+1]of boolean;//标记在一个点是否在增广路中
link:array[1..1000+1]of longint;//不是记录增广路,而是记录匹配边
procedure init;
var
i,x,y,e:longint;
begin
fillchar(map,sizeof(map),0);
read(n,e);
for i:=1 to e do
begin
read(x,y);
map[x,y]:=true;//邻接表储存
end;
end;
function find(x:longint):boolean;
var
i,q:longint;
begin
for i:=1 to n do
if map[x,i] and not mark[i] then//枚举边。同时排除第一种情况。
begin
q:=link[i]; link[i]:=x; mark[i]:=true;//改变增广路
if (q=0) or find(q) then exit(true);//若是第2种情况,那么第一次递归时q=0。若是第3种情况,则增广路发生改变,为了判断这种改变是有用的还是没用的,就要find(q)。因为只有增广路两端的点的link才=0,所以只要找到q=0,就表示把枚举的边 加入增广路成功。
link[i]:=q;
end;
exit(false);//表示找不到合适的边,该增广路不合理。
end;
procedure main;
var
i:longint;
begin
s:=0;
for i:=1 to n do//这里枚举的是点,但进入find函数后,枚举的还是以i点为端点的边。就是要把i点加入增广路中
begin
fillchar(mark,sizeof(mark),false);
if find(i) then inc(s);//s是最大匹配数,这里的意思是:从i出发的一条边在增广路中,所以s自然要+1
end;
writeln(s);
end;
begin
init;
main;
end.
上面只是我的一些个人简介,不一定对。
对于匈牙利算法,考得时候难点主要在建图,算法本身还是背下来比较好。
因为本来代码也不是很长,如果考试时现推得不偿失。