匈牙利算法是常见的二分图最大匹配算法,核心在于寻找增广路径。
首先来说,设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i在A,j在B),则称图G为一个二分图(二部图)。如下图所示:
而二分图的匹配,就是说对于G的一个子图M,若M的边集中的任意两条边都不依附于同一个顶点,则称M是一个匹配。当M的边数达到最大时,则称之为最大匹配。若一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配(完备匹配)。
至于增广路径,则是说:若P是图G中一条连通两个未匹配顶点的路径,并且属于M的边和不属于M的边(即已匹配和待匹配的边)在P上交替出现,则称P为相对于M的一条增广路径。从这里可以推出下述三个结论:
1.P的路径中边数必定为奇数,第一条边和最后一条边都不属于M;
2.将P进行取反操作可以得到一个更大的匹配M’;
3.M为G的最大匹配当且仅当不存在M的增广路径。
匈牙利算法正是利用增广路径的这些特点来实现二分图的最大匹配的,其流程可以概括为:
1.置M为空;
2.寻找一条增广路径,通过取反获得更大的M’来代替M;
3.重复2步骤,直到找不出增广路径;
4.此时所得为最大匹配。
其中2、3步寻找增广路径时,可以从二分图的结构特点入手。首先,假设二分图G的顶点V由了两个不相交的子集A和B构成,那么可以依次遍历子集A(当然也可以是B)的顶点。假设a点还没有匹配,则开始访问B的各个顶点,寻找可以与之匹配的顶点b。若b点不存在,表示不存在增广路径;若b点存在且没有与其他点相匹配,则表示这就是一条增广路径;若b点已经与c点相匹配,则从c点开始继续寻找可以与c点相匹配的d点,如此反复。可以察觉的是,这个过程满足增广路径中属于M的边和不属于M的边交替出现,而且都符合以不属于M的边开始和结束的特点。
全部步骤可以以第一个图为例,首先置M为空,找到一条增广路径1-4,将其取反后纳入M。下一条增广路径可以是2-5,再将其取反,把2-5纳入M。显然,此时的M的边数为2,比之前多了一条。然后还可以找多一条增广路径3-4-1-5-2-7,此时可以注意到3-4、1-5、2-7是在M外的,1-4、2-5是在M内的,M外内的交替出现。此时再次取反,M的边数扩充为3。此时,找不到更多的增广路径了,则表示已经是最大匹配了,尽管其不是完全匹配。
另外从中可以注意到一个细节,增广路径中的每条边每次取反后,已经匹配的点还是匹配的,只不过匹配的对象变了而已,故已经匹配的点就彻底丧失了作为增广路径中起始顶点和终至顶点的可能。上述过程可以参考下图(左边的顶点可以理解为顶点集A,右边的顶点可以理解为顶点集B,黑体实线表示每一个步骤结束时属于属于M的边):
具体代码可以参照下面:
#include <iostream>
#include <cstring>
using namespace std;
int n1,n2; //表示二分图中的两个顶点子集V1和V2的节点数,默认<=100
bool edge[ 101 ][ 101 ]; //表示V1中的点与V2中的点之间是否存在边
bool visited[ 101 ]; //表示寻找增广路径时是否访问过V2中的某一点
int map[ 101 ]; //表示与V2中每个点相匹配的V1中的另一个点
bool find( int t ); //寻找增广路径的函数
int main()
{
/*此过程省略,为相关数据输入和处理,并且保证二分图的性质*/
int count = 0; //表示匹配数
memset( map, 0, sizeof( map ) ); //将匹配设为0,表示处于未匹配状态
/*依次访问每个顶点,并尝试去寻找增广路径*/
for ( int i = 1; i <= n1; i++ ) {
if ( find( i ) )
count++;
}
cout << count << endl;
return 0;
}
bool find( int t ) {
memset( visited, false, sizeof( visited ) ); //默认所有顶点未被访问
visited[ t ] = true;
for ( int i = 1; i <= n2; i++ ) {
if ( edge[ t ][ i ] && !visited[ i ] ) { //t和i中存在边,且i未被访问
visited[ i ] = true;
if ( map[ i ] == 0 || find( map[ i ] ) ) { //i未被匹配,或者与之匹配的点有增广路
map[ i ] = t;
return true;
}
}
}
return false;
}