1.定义
二分图:
将一个图的所有顶点划分为两个不相交集U和V,使得图中的每一条边的顶点分别属于点集合U和点集V,即同一点集中的点不构成边,这样的图叫做二分图。维基百科中给出的无向图G的二分图的充分必要条件是:G至少有两个顶点,且其所有回路的长度均为偶数。
下图为带有回路的二分图举例:
匹配:
图中匹配的定义是指,这个图的一个边的集合,集合中任意两条边都没有公共的顶点,则称这个边集为一个匹配。我们称匹配中的边为匹配边,边中的点为匹配点;未在匹配中的边为非匹配边,边中的点为未匹配点。
最大匹配:
一个图中所有匹配中,所含匹配边数最大的匹配称为最大匹配。
完美匹配:
如果一个图的某个匹配中,图的所有顶点都是匹配点,那么这个匹配就是完美匹配。很显然,完美匹配一定是最大匹配,但是并不是所有的图都存在完美匹配。
Fig.2是某个二分图。Fig.3是该二分图中的某个匹配,匹配中的边用红色标明了。Fig.4是该二分图的某个最大匹配,同时也是完美匹配。
2.图的最大匹配问题
我们知道了图的最大匹配问题,那么如何求解图的最大匹配呢?这时候就得使用匈牙利算法。讲解算法之前,先讲解算法中的一些概念的定义。
交替路:
从一个未匹配点出发,依次经过非匹配边、匹配边、非匹配边、匹配边…,形成这样的交替进行的路径成为交替路。
增广路:
从一个未匹配点出发,走交替路,如果途径一个未匹配点(出发点不算),则这样一条交替路称为增广路。增广路有一个重要的特性,就是非匹配边要比匹配边多一条(从未匹配点出发,以未匹配点结束,走交替路,显然非匹配边多一条),此时,我们可以让增广路中的匹配边和非匹配边调换位置,匹配边变为非匹配边,非匹配边变为匹配边,这样匹配边的数量就会加1,并且由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质,保证了交换的正确性。
匈牙利算法:
算法就是根据增广路的思想,以一个未匹配的节点出发,遍历图,不断的寻找增广路来扩充匹配的边数,直到不能扩充为止。根据遍历图的方式不同,匈牙利算法可以分为dfs(深度遍历)和bfs(广度遍历)的实现。
以上面讲解匹配的图为例,其匈牙利算法实现的c语言代码如下:
#include<bits/stdc++.h>
using namespace std;
int graph[20][20]; //图的大小
int n; //节点数
int visit[20]; //是否访问
int matched[20]; //是否已经匹配,对应的匹配点
//显示匹配结果
void show(){
memset(visit, 0, sizeof(visit));
for(int i = 0; i < n; i++){
if(!visit[i]){
if(matched[i] != -1){
cout<<"("<<(i + 1)<<", "<<(matched[i] + 1)<<")"<<endl;
visit[i] = 1;
visit[matched[i]] = 1;
}
}
}
}
/*
* dfs实现,
* params:
* x:起始的未匹配点
* return:
* 1:找到增广路
* 0:未找到
*/
int dfs_solve(int x){
//找到一个和节点存在边的点,并且该点在本轮中没有被访问过
for(int i = 0; i < n; i++){
if(graph[x][i] && !visit[i]){
visit[i] = 1; //标记为匹配过
//按照交替路的模式找增广路,增广路相对于交替路的特性是就是,第一个节点和最后一个节点都是未匹配过的节点
if(matched[i] == -1 || dfs_solve(matched[i]) == 1){ //直接跳到matched[i]能够保证匹配边和未匹配边交替
//说明找到了一个未匹配节点,将所有匹配边变为未匹配边,将所有未匹配边变为匹配边,这样匹配边数会加1,这个交换过程通过回溯实现
matched[x] = i;
matched[i] = x;
cout<<(x+1)<<" 和 "<<(i+1)<<" 匹配"<<endl;
return 1;
}
}
}
return 0;
}
/*
* dfs实现,
* params:
* x:起始的未匹配点
* return:
* num:0表示未找到增广路,1表示找到
*/
int hungarian1(){
memset(matched, -1, sizeof(matched));
int sum = 0;
for(int i = 0; i < n; i++){
if(matched[i] == -1){
cout<<endl;
cout<<"从 "<<(i + 1)<<" 开始匹配"<<endl;
memset(visit, 0 , sizeof(visit));
sum += dfs_solve(i);
}
}
cout<<"共有 "<<sum<<" 匹配项"<<endl;
show();
return sum;
}
//匈牙利算法,bfs实现
int bfs_solve(int x){
queue<int> items;
int prev[20];
int num = 0;
memset(prev, -1, sizeof(prev));
items.push(x);
bool flag = 0;
while(!flag && items.size()){
int u = items.front();
items.pop();
for(int i = 0; i < n; i++){
if(graph[u][i] && !visit[i] && !flag){
visit[i] = 1;
if(matched[i] == -1){ //找到一个未匹配点
flag = 1; //交换匹配路径和未匹配路径,不能用回溯,只能向前找祖先
int l = u, r = i;
while(l != -1){
int tmp = matched[l];
matched[l] = r;
matched[r] = l;
cout<<(l+1)<<" 和 "<<(r+1)<<" 匹配"<<endl;
r = tmp;
l = prev[l];
}
num++;
}
else{ //点不是未匹配点
prev[matched[i]] = u;
items.push(matched[i]); //只有找到了匹配边才能压入端点,保证交替进行
}
}
}
}
return num;
}
int hungarian2(){
memset(matched, -1, sizeof(matched));
int sum = 0;
for(int i = 0 ; i < n; i++){
if(matched[i] == -1){
cout<<endl;
cout<<"从 "<<(i + 1)<<" 开始匹配"<<endl;
memset(visit, 0, sizeof(visit));
sum += bfs_solve(i);
}
}
cout<<"共有 "<<sum<<" 匹配项"<<endl;
show();
return sum;
}
int main()
{
memset(graph, 0, sizeof(graph));
graph[0][4] = 1;
graph[0][6] = 1;
graph[1][4] = 1;
graph[2][4] = 1;
graph[2][5] = 1;
graph[3][6] = 1;
graph[3][7] = 1;
graph[4][0] = 1;
graph[4][1] = 1;
graph[4][2] = 1;
graph[5][2] = 1;
graph[6][0] = 1;
graph[6][3] = 1;
graph[7][3] = 1;
n = 8;
hungarian1();
cout<<"**********************************"<<endl;
hungarian2();
return 0;
}