對匈牙利演算法理解——對二分圖進行最大匹配的演算法

allenwakler發表於2019-04-09

        演算法很重要,不知道為什麼到現在才發現演算法的重要性,最近在學習一本很有趣味的書——《演算法的樂趣》。為了防止在看完書後就把知識還回去現象的發生,決定在學習的過程中養成每天一篇演算法文的習慣。

        還記得自己在學校開設的《演算法設計與分析》課程中所學的知識,當時使用的教科書好像是一位阿拉伯作者所著,書中的程式碼均使用虛擬碼展示,當時覺得虛擬碼什麼的真的是不切實際,到現在才發現,如果你不深入理解演算法,那確實會產生一種虛擬碼不痛不癢的感覺,但是如果你把虛擬碼中的思路全部理解了之後然後去自己寫出真正的程式碼後會發現虛擬碼是對程式碼精煉最好的東西。


      閒話不多說了,切入正題。

      首先,理清幾個名詞:匹配,最大匹配,完美匹配,穩定匹配:
      都以女孩子找男朋友為例子,闡述上述的幾個概念。首先假設男生有一個集合,女生有一個集合,且他們兩個集合的數量是相等的。

      ①匹配:一個女生找到一個男朋友就是一個匹配,這個匹配由一個女生和一個男生構成。

      ②最大匹配:每個男生和女生只在匹配中出現至多一次,將男生和女生按照某種方法匹配了之後剩下的沒有匹配的男生或者女生的數量最少,就說完成了一次最大匹配。

      ③完美匹配 :  每個男生和女生只在匹配中出現至多一次,且男生和女生正好匹配完了,就說完成了一次完美匹配。

     ④ 穩定匹配:如果匹配是完美匹配而且匹配中不存在不穩定因素,則稱匹配為穩定匹配。不穩定因素的定義如下:比如一組數量相等的男生和女生配對去舞會跳舞,每個男生均有一個自己的喜愛列表,在列表中,他越中意的女生越排在列表的前面,同樣,每個女生也有一個自己的偏愛列表,男生在其中的排名越靠前,表明女生越中意這個男生,當完成一次完美匹配後檢查匹配的結果,如果存在兩對匹配:男生1,他匹配的女伴是11,男生2,他匹配的舞伴是22。但是男生1明明更喜歡女生22,女生22同時也更喜歡男生1。這樣的匹配對就稱為不穩定因素。只有同時滿足是完美匹配而且不存在不穩定因素的匹配才稱之為穩定匹配。


匈牙利演算法實施的物件是一個二分圖,二分圖的概念在演算法課上也有所介紹,簡而言之就是將圖中的節點分為兩個集合,圖中的邊的兩個端點必須要在不同的集合中。

匈牙利演算法的思想用語言描述就是:

      假設二分圖的節點分為平均分為兩部分,分別儲存在集合1和集合2中。對於集合1的節點1,先在第二部分找一個和他**有連線**的節點2,並**假設將該節點分配給節點1**,(即節點2被節點1佔有了),進一步看節點2是否還是自由節點,當且僅當**結點2是自由節點**或**者雖然節點2不是自由節點,但是將當前匹配做稍許改動後可以將節點2變為自由節點時**,才將節點1與節點2相連。並將count做+1操作,表示完成了一對匹配。對集合1的所有節點進行這個操作,當最後的count值為集合1(或者集合2,因為兩部分中節點個數相等)中節點的個數時,該匹配變成完美匹配,進而可以根據不確定因素的描述,對該完美匹配進行不確定因素的搜尋,當不存在不確定因素時,此次匹配為穩定匹配。

也可以用增廣路徑的相關知識來解釋,增光路徑的一個好的例子我就照搬了:

https://www.cnblogs.com/logosG/p/logos.html

原作者講的很清楚明瞭哇,自己差距還很大!


接下來上程式碼:

#include<iostream>
#include<string>
#include<string.h>
using namespace std;
const int UNIT_COUNT = 5;  //二部圖中X集合與Yj集合的節點個數

//定義二部圖中節點資料結構結構
typedef struct tagPartner {
	const char *name;
	int current;                 //當前匹配的節點
	int pCount;                 //與該節點相連的另一個二部圖中節點集合中的節點的個數
	int perfect[UNIT_COUNT];   //與之有連線的Y節點的編號
}PARTNER;

//定義表示二部圖最大匹配的資料結構
typedef struct tagMaxMatch {
	int edge[UNIT_COUNT][UNIT_COUNT];
	bool supposed_on_path[UNIT_COUNT];
	int real_on_path[UNIT_COUNT];
	int max_match;
}MAX_MATCH;

PARTNER X[] = {
	 {"X1",-1, 1, {2}  },
	 {"X2",-1, 2, {0,1}  },
	 {"X3",-1, 3, {1,2,3}  },
	 {"X4",-1, 2, {1,2}  },
	 {"X5",-1, 3, {2,3,4}  },
};
PARTNER Y[] = {
	{"Y1",-1, 1, {1}  },
	{"Y2",-1, 3, {1,2,3}  },
	{"Y3",-1, 4, {0,2,3,4}  },
	{"Y4",-1, 2, {2,4}  },
	{"Y5",-1, 1, {4}  },
};
bool FindAugmentPath(MAX_MATCH *match, int xi) {
	for (int yj = 0; yj < UNIT_COUNT; yj++) {
		if ((match->edge[xi][yj] == 1) && (!match->supposed_on_path[yj])) {
			match->supposed_on_path[yj] = true;
			if ((match->real_on_path[yj] == -1) || (FindAugmentPath(match, match->real_on_path[yj]))) {
				match->real_on_path[yj] = xi;
				return true;
			}
		}
	}
	return false;
}
void Clear_Supposed_On_Path_Sign(MAX_MATCH *match) {
	for (int i = 0; i < UNIT_COUNT; i++) {
		match->supposed_on_path[i] = false;
	}
}
bool Hungry_Match(MAX_MATCH *match) {
	for (int xi = 0; xi < UNIT_COUNT; xi++) {
		if (FindAugmentPath(match, xi)) {
			match->max_match++;
		}
		Clear_Supposed_On_Path_Sign(match);
	}
	return (match->max_match == UNIT_COUNT);
}
void InitGraph(MAX_MATCH *match, PARTNER *X, PARTNER *Y) {
	match->max_match = 0;
	memset(match->edge, 0, sizeof(int)*UNIT_COUNT*UNIT_COUNT);
	for (int i = 0; i < UNIT_COUNT; i++) {
		match->supposed_on_path[i] = false;
		match->real_on_path[i] = -1;
		for (int j = 0; j < X[i].pCount; j++) {
			match->edge[i][X[i].perfect[j]] = 1;
		}
	}
}
void PrintResult(MAX_MATCH *match,PARTNER *X,PARTNER *Y) {
	for (int i = 0; i < match->max_match; i++) {
		cout << X[match->real_on_path[i]].name << "  與之相連的Y點是: " << Y[i].name << endl;
	}
}
int main(){
	MAX_MATCH match;
	InitGraph(&match, X, Y);
	if (Hungry_Match(&match)) {
		PrintResult(&match, X, Y);
	}
	return 0;
}
/*
問題描述:有一個二部圖,圖中的節點分屬於兩個節點集合,求他們的一個最大匹配,使用匈牙利演算法求解二分圖的最大匹配問題
資料結構:首先使用PARTNER的資料結構儲存一個節點的基本資訊,比如節點名字,節點的邊有多少條,這些邊和另一個幾何中的哪些點相連,
          求最大匹配的時候將這個節點與哪個節點匹配了的資訊。然後使用兩個節點的集合來描述二部圖中所有節點和邊的資訊。
		  再使用叫MAX_MATCH的資料結構來儲存當前找到的最大匹配的資訊,首先需要記錄最大匹配中匹配對數的變數,需要儲存邊
		  資訊的結構(以便在假設相連的過程中進行判定),需要一個 supposed_on_path的陣列來在遞迴時隨著遞迴的深入將Y節點分配出
		  去,需要一個real_on_path的陣列在完成一次匹配時真正將Y節點與X節點匹配,所以real_on_path這個陣列中存貯的其實是真正與
		  對應下標Y節點相連的X節點的下標。這樣所有資料的儲存問題就解決了。
演算法描述:假設二分圖中節點集合有X節點集合與Y節點集合兩個集合,演算法假設總是從X節點出發去尋找匹配,對於X節點集合中的
          **每一個**xi節點,通過edge[][]陣列找在Y集合中和他**每一個**相連的yj節點,並從中選出一個yj,先假設將yj節點分配給
		  當前xi節點,如果yj節點是自由節點,就把yj對應的real_on_path[]陣列第j位設定為i,表示yj與xi相連。或者雖然當前的yj
		  已經通過前面的分配與X集合中的某個節點相連了,但是如果將前面已匹配成功的(xi,yj)對的匹配關係通過冗餘的邊稍微做改
		  變可以使它變成自由節點,就把yj對應的real_on_path[]陣列第j位設定為i,表示yj與xi相連。每次完成一對匹配就將MAX_MATCH
		  中的max_match變數加1,(通過當前演算法找到一對匹配)如果最後max_match值等於二分圖中所有節點數量的一半(這裡假設給出
		  的)圖中兩個集合中節點數量是相同的,就表明找到了一個完美匹配。(當最大匹配中沒有剩餘節點時,它將成為完美匹配)。
*/

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

相關文章