完美匹配-匈牙利算法(Hungarian method Edmonds)講解

目錄

匈牙利算法(Hungarian method Edmonds)

例題1 有完美匹配

例題2 無完美匹配

代碼實現

變量及函數說明

測試數據1

測試結果1

測試數據2

測試結果


匈牙利算法(Hungarian method Edmonds)

以任意一個匹配M作爲開始。(可取M=∅。)

①若M已飽和X的每個頂點,停止(M爲完美匹配)。否則,取X中M-不飽和頂點u,今:S<-{u},T=∅。

②若N(S)=T,則停止,算法結束(無完美匹配);否則N(S)⊃T,轉到下一步。

③取y∈N(S)\T,若y爲M-飽和的,設yz∈M,則令S=S∪(z),T=T∪{y},轉步驟②;否則,y爲M-不飽和的,存在M-可擴路P,令M=M△E(P),轉到步驟①。

M-飽和的:邊uv∈M,則稱點u與v爲M-飽和的。

M-不飽和的:與點w相關聯的所有邊都不屬於M,則稱點m爲M-不飽和的。

N(S):點集S的鄰集,圖中所有與S中的點相鄰接的頂點的集合。

M-交錯路:p是G的一條通路,如果p中的邊爲屬於M中的邊與不屬於M但屬於G中的邊交替出現,則稱p是一條M-交錯路。

M-可擴路(增廣路)P:屬於M的邊和不屬於M的邊(即已匹配和待匹配的邊)在P上交替出現,起點與終點都是M-不飽和的

E(P): M-可擴路(增廣路)P的邊的集合。

M△E(P)=(M∪E(P))\(M∩E(P))

由可擴路(增廣路)的定義可以推出下述三個結論:

(1)P的路徑個數必定爲奇數,第一條邊和最後一條邊都不屬於M

(2)將M和P進行異或操作可以得到一個更大的匹配M,比之前的匹配M多1

(3)M爲G的最大匹配當且僅當不存在M的增廣路徑。

例題1 有完美匹配

從下圖中給定的M={x1y2,x5y4}開始,用匈牙利算法求完美匹配。

 

初始圖

1)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x3,令S={x3},T=∅,則N(S)={y1,y2,y3},N(S)⊃T, 取y3∈N(S)\T,y3爲M-不飽和的,找到M-可擴路P=x3y3,令M=M△E(P)={ x1y2, x3y3,x5y4}

添加x3y3

2)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x2,令S={x2},T=∅,則N(S)={y1},N(S)⊃T, 取y1∈N(S)\T,y1爲M-不飽和的,找到M-可擴路P=x2y1,令M=M△E(P)={ x1y2,x2y1,x3y3,x5y4}

添加x2y1

3)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x4,令S={x4},T=∅,則N(S)={y2}, N(S)⊃T, 取y2∈N(S)\T,y2爲M-飽和的,且y2x1∈M(y2x1=x1y2),令S=S∪{x1}={x1, x4},T=T∪{y2}={y2}。

N(S)={y2,y4}, N(S)⊃T, 取y4∈N(S)\T,y4爲M-飽和的,且y4x5∈M(y4x5=x5y4),令S=S∪{x5}={x1, x4,x5},T=T∪{y4}={y2,y4}。

N(S)={y2,y4,y5}, N(S)⊃T, 取y5∈N(S)\T,y5爲M-不飽和的,找到M-可擴路P=x4y2x1y4x5y5,令M=M△E(P)={ x1y2,x2y1,x3y3,x5y4}△{x4y2,y2x1,x1y4,y4x5,x5y5}={ x1y4, x2y1,x3y3,x4y2,x5y5},M包含X的每個頂點,停止。

(可擴路的尋找,可以倒推,y5是由於x5引入的,x5是由於y4引入的,y4是由於x1引入的,x1是由於y2引入的,y2是由於剛開始取的x4)

添加x5y5,x1y4,x4y2,去除x1y2,x5y4

注意:在考試中手寫時,不必從與x相連的y中遍歷,例如1)並沒有選擇x3y1,因爲很明顯x2只連接了y1。當然,你接下來看代碼實現時,就沒有手寫這麼簡單了,手寫時你已經對深搜進行了部分剪枝,請耐心看代碼。

手算驗證:M爲空,x4僅與y2相連,M={x4y2};x2僅與y1相連M={x2y1,x4y2};x3與y1、y2、y3相連,y1與y2已是M-飽和的,x3僅能與y3匹配,M={x2y1,x3y3,x4y2 };x1與y2、y4相連,y2已是M-飽和的,x1僅能與y4匹配,M={x1y4, x2y1,x3y3,x4y2};僅剩x5與y5,兩者是邊的兩點,匹配即可,故存在完美匹配M={ x1y4, x2y1,x3y3,x4y2,x5y5}。

例題2 無完美匹配

從下圖中給定M=∅開始,用匈牙利算法求完美匹配。

初始圖

1)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x1,令S={x1},T=∅,則N(S)={y2,y3},N(S)⊃T, 取y2∈N(S)\T,y2爲M-不飽和的,找到M-可擴路P=x1y2,令M=M△E(P)={ x1y2}

添加x1y2

2)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x2,令S={x2},T=∅,則N(S)={y1,y2,y4,y5},N(S)⊃T, 取y1∈N(S)\T,y1爲M-不飽和的,找到M-可擴路P=x2y1,令M=M△E(P)={ x1y2,x2y1}

添加x2y1

3)M沒有飽和X的每個頂點,取X中M-不飽和的頂點x3,令S={x3},T=∅,則N(S)={y2,y3},N(S)⊃T, 取y3∈N(S)\T,y3爲M-不飽和的,找到M-可擴路P=x3y3,令M=M△E(P)={ x1y2,x2y1,x3y3}

添加x3y3

M沒有飽和X的每個頂點,取X中M-不飽和的頂點x4,令S={x4},T=∅,則N(S)={y2,y3},N(S)⊃T, 取y2∈N(S)\T,y2爲M-飽和的,且y2x1∈M(y2x1=x1y2),令S=S∪{x1}={x1, x4},T=T∪{y2}={y2}。

N(S)={ y2,y3}, N(S)⊃T, 取y3∈N(S)\T,y3爲M-飽和的,且y3x3∈M(y3x3=x3y3),令S=S∪{x3}={x1, x4,x3},T=T∪{y3}={y2,y3}。

N(S)={y2,y3}=T={y2,y3},結束,沒有完美匹配。

注意:手寫時可以直接取x1,x3,x4即可,可以看出他們都是連接的y2,y3,兩個y沒有辦法匹配給3個x。

代碼實現

通過例子可以看出,S並沒有必要,只需要N(S)再原來基礎上更新即可,爲簡化,代碼沒有加入S。

變量及函數說明

int M[]                                   初始爲 - 1, 下標爲X下標,值爲匹配的Y集合中的元素下標 ,做對稱差時覆蓋即可
bool X[Maxnum], Y[Maxnum] 初始爲false, 用於判斷X, Y集合中元素是否爲M飽和的
vector<int> P                         可擴路P,初始爲空,記錄X,Y集合中在P上的元素的下標
set<Vartype> NS, T               對應算法N(S)與T
bool visitedx[Maxnum], visitedy[Maxnum]   每次深搜時標記是否已遍歷。

void Init(Graph &G)          初始化函數,參數:圖G,功能:初始化圖G
void Print(Graph G)          打印圖函數,參數:圖G,功能:以矩陣形式打印圖,可去除
void PrintP(Graph G)        打印路徑函數,參數:圖G,功能:打印路徑P
void PrintM(Graph G)       打印匹配集合M函數,參數:圖G,功能:打印匹配集合M
void Delta()                       對稱差函數,參數:無,功能:M與E(P)做對稱差
void DFS(Graph G,bool x,int start) 深度遍歷函數(遞歸形式)參數:圖G,X點集,開始結點下標start 作用:深度遍歷,找可擴路

/*
Project: Hungarian method Edmonds
Date:    2020/01/02
Author:  Frank Yu
void Init(Graph &G)				   初始化函數,參數:圖G,功能:初始化圖G
void Print(Graph G)                打印圖函數,參數:圖G,功能:以矩陣形式打印圖,可去除
void PrintP(Graph G)               打印路徑函數,參數:圖G,功能:打印路徑P
void PrintM(Graph G)               打印匹配集合M函數,參數:圖G,功能:打印匹配集合M
void Delta()                       對稱差函數,參數:無,功能:M與E(P)做對稱差
void DFS(Graph G,bool x,int start) 深度遍歷函數(遞歸形式)參數:圖G,X點集,開始結點下標start 作用:深度遍歷,找可擴路
*/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<string>
#include<set>
#include<list>
#include<queue>
#include<vector>
#include<map>
#include<stack>
#include<iterator>
#include<algorithm>
#include<iostream>
#define Vartype string //頂點類型
#define EdgeType int
#define Maxnum 100 //二部圖點集最大數量
using namespace std;
//圖的數據結構
typedef struct Graph
{
	Vartype X[Maxnum];
	Vartype Y[Maxnum];
	EdgeType Edge[Maxnum][Maxnum];//邊表
	int xnum, ynum,edgenum;//頂點數
}Graph;
//M 下標爲X下標,值爲匹配的Y集合中的元素下標 初始-1
int M[Maxnum];
//M是否飽和X、Y 飽和爲True,不飽和爲False
bool X[Maxnum],Y[Maxnum];
//可擴路P
vector<int> P;
//鄰接點集合與T集合
set<Vartype> NS, T;
//標記是否已遍歷過
bool visitedx[Maxnum], visitedy[Maxnum];
//初始化函數
void Init(Graph &G)
{
	memset(G.Edge, 0, sizeof(G.Edge));
	cout << "請輸入X、Y頂點集個數:" << endl;
	cin >> G.xnum >> G.ynum;
	Vartype temp;
	cout << "請輸入X頂點集頂點名稱:" << endl;
	for (int i = 0; i < G.xnum; i++)
	{
		cin >> temp;
		G.X[i] = temp;
	}
	//for (int i = 0; i < G.xnum; i++) cout << G.X[i] << '\t' << endl;
	cout << "請輸入Y頂點集頂點名稱:" << endl;
	for (int i = 0; i < G.ynum; i++) 
	{
		cin >> temp;
		G.Y[i] = temp;
	}
	//for (int i = 0; i < G.xnum; i++) cout << G.X[i] << '\t' << endl;
	cout << "請輸入邊數:" << endl;
	cin >> G.edgenum;
	cout << "請輸入邊,空格分隔(例如: x y):" << endl;
	Vartype x, y;
	for (int i = 0; i < G.edgenum; i++)
	{	
		cin >> x >> y;
		int p1 = -1,p2 = -1;
		for (int j = 0; j < G.xnum; j++)
			if (!x.compare(G.X[j])) { p1 = j; break; }
		for (int k = 0; k < G.ynum; k++)
			if (!y.compare(G.Y[k])) { p2 = k; break;}
		//cout << p1 << " " << p2;
		if (p1 != -1 && p2 != -1)
		{
			G.Edge[p1][p2] = 1;
		}
		else
		{
			cout << "未找到該邊,請檢查端點是否輸入有誤!" << endl;
			break;
		}
	}
}
//打印圖函數
void Print(Graph G)
{
	cout << '\t';
	for (int i = 0; i < G.ynum; i++) cout << G.Y[i] << '\t';
	cout << endl;
	for (int i = 0; i < G.xnum; i++)
	{
		cout << G.X[i] << '\t';
		for (int j = 0; j < G.ynum; j++)cout << G.Edge[i][j]<<'\t';
		cout << endl;
	}
}
//輸出可擴路
void PrintP(Graph G)
{
	cout << "P:";
	for (int i = 0; i < P.size(); i++)
	{
		if (i % 2 == 0)cout << G.X[P[i]];
		else cout << G.Y[P[i]];
	}
	cout << endl;
}
//輸出集合M
void PrintM(Graph G)
{
	bool flag = false;
	cout << "M:{";
	for (int i = 0; i < G.xnum; i++)
	{
		if (M[i] != -1 && !flag) { cout << G.X[i] << G.Y[M[i]]; flag = true; }
		else if (M[i]!=-1&&flag)cout  << ","<< G.X[i] << G.Y[M[i]];
	}
	cout <<"}"<<endl;
}
//集合M與E(P)做對稱差
void Delta()
{
	vector<int>::iterator it;
	for (it = P.begin(); it != P.end();it++)
	{
		int x = *it;
		it++;
		int y = *it;
		X[x] = true;
		Y[y] = true;
		M[x] = y;
	}
}
//深度遍歷函數(遞歸形式)參數:圖G,X點集開始結點下標start 作用:深度遍歷
void DFS(Graph G,bool x,int start)
{
	/*
	cout << "DFS(";
	if (x)cout << "x,";
	else cout << "y,";
	cout << start << ")" << endl;*/
	//X頂點集 
	if (x)
	{
		P.push_back(start);
		cout << "當前路:" << endl;
		PrintP(G);
		visitedx[start] = true;
		for (int i = 0; i < G.ynum; i++) if (G.Edge[start][i] == 1)NS.insert(G.Y[i]);
		if (NS.size() == T.size())
		{
			cout << "N(S)==T,沒有完美匹配" << endl;
			system("pause");
		}
		for (int i = 0; i < G.ynum; i++)
		{
			//取Y中M - 飽和頂點
			if (G.Edge[start][i] == 1 && !visitedy[i] && Y[i])//是鄰接點且未訪問 M - 飽和頂點Y[i]
			{
				T.insert(G.Y[i]);
				cout << "取Y中M - 飽和頂點" << G.Y[i] << endl;
				DFS(G,false,i);//遞歸深度遍歷結點集Y
			}
			//Y爲M - 不飽和頂點 找到可擴路P 與M做對稱差
			if (G.Edge[start][i] == 1 && !visitedy[i] && !Y[i])
			{
				cout << G.Y[i]<< "爲M - 不飽和頂點,找到可擴路"  << endl;
				P.push_back(i);
				PrintP(G);
				Delta();
				PrintM(G);
				//返回步驟一
				for (int i = 0; i < G.xnum; i++)
				{
					memset(visitedx, false, sizeof(visitedx));
					memset(visitedy, false, sizeof(visitedy));
					P.clear();
					NS.clear();
					T.clear();
					//取X中M - 不飽和頂點
					if (!X[i])DFS(G, true, i);
				}
				cout << "找到完美匹配";
				PrintM(G);
				cout << endl;
				system("pause");
			}
		}
		P.pop_back();
		cout << "返回上一層前的路徑:" << endl;
		PrintP(G);
		return;//返回至上一層
	}
	else//Y頂點集
	{
		//cout << G.Y[start];
		P.push_back(start);
		cout << "當前路:" << endl;
		PrintP(G);
		visitedy[start] = true;
		for (int j = 0; j < G.xnum; j++)
		{
			if (M[j]==start)//找到Y[start]X[j]屬於M
			{
				cout << "存在"<<G.Y[start]<<G.X[j]<<"屬於M" << endl;
				DFS(G, true, j);//遞歸深度遍歷結點集X
			}
		}
		P.pop_back();
		cout << "返回上一層前的路徑:" << endl;
		PrintP(G);
		return ;//返回至上一層
	}
}
//匈牙利算法
int Hungarian(Graph &G)
{
	int i;
	memset(M, -1, sizeof(M));
	cout << "1.輸入初始M  2.M從空集開始" << endl;
	cout << "請選擇:";
	cin >> i;
	if (1 == i)
	{
		int num;
		cout << "請輸入M中邊的數量:" << endl;
		cin >> num;
		cout << "請輸入邊,空格分隔(例如: x y):" << endl;
		Vartype x, y;
		for (int i = 0; i < num; i++)
		{
			cin >> x >> y;
			int p1 = -1, p2 = -1;
			for (int j = 0; j < G.xnum; j++)
				if (!x.compare(G.X[j])) { p1 = j; break; }
			for (int k = 0; k < G.ynum; k++)
				if (!y.compare(G.Y[k])) { p2 = k; break; }
			if (p1 != -1 && p2 != -1)
			{
				M[p1] = p2;
				X[p1] = true;
				Y[p2] = true;
			}
			else
			{
				cout << "未找到該邊,請檢查端點是否輸入有誤!" << endl;
				break;
			}
			
		}
	}
	PrintM(G);
	//步驟1 判斷M是否飽和所有X元素
	for (int i = 0; i < G.xnum; i++)
	{
		memset(visitedx, false, sizeof(visitedx));
		memset(visitedy, false, sizeof(visitedy));
		P.clear();
		NS.clear();
		T.clear();
		//取X中M - 不飽和頂點
		if (!X[i])DFS(G, true, i);
	}
	cout << "找到完美匹配"; 
	PrintM(G);
	cout<< endl;
	return 0;
}
//主函數
int main()
{
	Graph G;
	Init(G);
	Print(G);
	Hungarian(G);
	return 0;
}

以上面兩個例題做測試

測試數據1

5 5
x1 x2 x3 x4 x5
y1 y2 y3 y4 y5
10
x1 y2
x1 y4
x2 y1
x3 y1
x3 y2
x3 y3
x4 y2
x5 y2
x5 y4
x5 y5
1 2
x1 y2
x5 y4

測試結果1

部分結果
部分結果

 

實際深搜樹

測試數據2

5 5
x1 x2 x3 x4 x5
y1 y2 y3 y4 y5
12
x1 y2
x1 y3
x2 y1
x2 y2
x2 y4
x2 y5
x3 y2
x3 y3
x4 y2
x4 y3
x5 y3
x5 y5
2

測試結果

部分結果
部分結果

 

更多數據結構與算法實現:數據結構(嚴蔚敏版)與算法的實現(含全部代碼)

有問題請下方評論,轉載請註明出處,並附有原文鏈接,謝謝!如有侵權,請及時聯繫。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章