1. 程式人生 > >求解二分圖的最大匹配的匈牙利演算法---POJ 1325 Machine Schedule

求解二分圖的最大匹配的匈牙利演算法---POJ 1325 Machine Schedule

【基本概念】

二分圖簡單來說,如果圖中點可以被分為兩組,並且使得所有邊都跨越組的邊界,則這就是一個二分圖。準確地說:把一個圖的頂點劃分為兩個不相交集 U 和V ,使得每一條邊都分別連線U、V中的頂點。如果存在這樣的劃分,則此圖為一個二分圖

匹配:在圖論中,一個「匹配」(matching是一個邊的集合,其中任意兩條邊都沒有公共頂點。匹配其實是邊獨立集。顯然,在二分圖中的匹配就相當於對兩組點進行匹配。

完美匹配:對於一個圖G 與給定的一個匹配M,如果圖G 中不存在M 的未蓋點,則稱匹配M 為圖G 的完美匹配

最大匹配:一個圖所有匹配中,所含匹配邊數最多的匹配,稱為這個圖的最大匹配。

舉例來說:如下圖所示,如果在某一對男孩和女孩之間存在相連的邊,就意味著他們彼此喜歡。是否可能讓所有男孩和女孩兩兩配對,使得每對兒

都互相喜歡呢?圖論中,這就是完美匹配問題。如果換一個說法:最多有多少互相喜歡的男孩/女孩可以配對兒?這就是最大匹配問題。


【求解演算法】

求二部圖最大匹配的演算法有:

(1) 網路流解法 

(2) 匈牙利演算法

(3) Hopcroft-Karp 演算法(匈牙利演算法的改進)。

1.網路流解法

(1)基本原理:

設二部圖為G(V, E),它的頂點集合V 所包含的兩個子集為X = { x1, x2, …, xm }和Y = { y1, y2, …,yn },如下圖(a)所示。如果把二部圖中看成一個網路,邊(xi, yk)都看成有向邊<xi, yk>,則在求最大匹配時要保證從頂點xi 發出的邊最多隻選一條、進入頂點yk 的邊最多也只選一條,在這些前提下將盡可能多的邊選入到匹配中來。

設想有一個源點S,控制從S 到xi 的弧<s, xi>的容量為1,這樣就能保證從頂點xi 發出的邊最多隻選一條。同樣,設想有一個匯點T,控制從頂點yk 到T 的弧<yk, t>的容量也為1,這樣就能保證進入頂點yk 的邊最多也只選一條。另外,設邊<xi, yk>的容量也為1。

按照上述思路構造好容量網路後,任意一條從S 到T 的路徑,一定具有S−xi−yk−T 的形式,且這條路徑上3 條弧<s, xi>、<xi, yk>、<yk, t>的容量均為1。因此,該容量網路的最大流中每條從S 到T 的路徑上,中間這一條邊<xi, yk>的集合就構成了二部圖的最大匹配。

(2)網路流的構造

  1. 求二部圖最大匹配的容量網路構造和求解方法如下:從二部圖G 出發構造一個容量網路G´,步驟如下:

  • a) 增加一個源點S 和匯點T;
  • b) 從S 向X 的每一個頂點都畫一條有向弧,從Y 的每一個頂點都向T 畫一條有向弧;
  • c) 原來G 中的邊都改成有向弧,方向是從X 的頂點指向Y 的頂點;
  • d) 令所有弧的容量都等於1。構造好的流量網路如下圖(b)所示。
  2. 求容量網路G´的最大流F。 

  3.最大流F 求解完畢後,從X 的頂點指向Y 的頂點的弧集合中,弧流量為1 的弧對應二部圖最大匹配中的邊,最大流F 的流量對應二部圖的最大匹配的邊數。

下圖表示了一個網路流構造方法的例項:


(3)演算法複雜度

取決於網路流演算法。

2.匈牙利演算法

(0)基本概念:

交替軌:又稱交替路。從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊…形成的路徑叫交替軌。特別地,如果軌P 僅含一條邊,那麼無論這條邊是否屬於匹配M,P 一定是一條交替路



可增廣軌:又稱可增廣路。對於一個給定的圖G 和匹配M,兩個端點都是未蓋點(不與匹配中的任意一條邊關聯,即沒有匹配的點)的交錯軌稱為關於M 的可增廣軌


在上圖中1->5->2->7->4->8就是一條增廣軌。


為什麼這樣的軌是可增廣軌?

增廣路有一個重要特點:非匹配邊比匹配邊多一條。因此,研究增廣路的意義是改進匹配。只要把增廣路中的匹配邊和非匹配邊的身份交換即可。由於中間的匹配節點不存在其他相連的匹配邊,所以這樣做不會破壞匹配的性質。交換後,圖中的匹配邊數目比原來多了 1 條。

(1)基本原理:

從當前匹配M(如果沒有匹配,則取初始匹配為M=Ø)出發,檢查每一個未蓋點,然後從它出發尋找可增廣路,找到可增廣路,則沿著這條可增廣路進行擴充,直到不存在可增廣路為止。

根據搜尋增廣路方式的不同可以分為:(1)DFS增廣(2)BFS增廣(3)多路增廣

下面主要介紹DFS增廣,因為DFS增廣好理解而且程式碼量少

演算法最基本流程:

  1. 置邊集M為空(初始化,誰和誰都沒連著)
  1. 選擇一個新的原點尋找增廣路
  1. 重複(2)操作直到找不出增廣路徑為止(2,3步驟構成一個迴圈)
虛擬碼:(來源:https://www.byvoid.com/blog/hungary
bool 尋找從k出發的對應項出的可增廣路
{
    while (從鄰接表中列舉k能關聯到頂點j)
    {
        if (j不在增廣路上)
        {
            把j加入增廣路;
            if (j是未蓋點 或者 從j的對應項出發有可增廣路)
            {
                修改j的對應項為k;
                則從k的對應項出有可增廣路,返回true;
            }
        }
    }
    則從k的對應項出沒有可增廣路,返回false;
}

void 匈牙利hungary()
{
    for i->1 to n
    {
        if (則從i的對應項出有可增廣路)
            匹配數++;
    }
    輸出 匹配數;
}

為什麼這個遞迴就是在尋找最短路?因為在遞迴過程中尋找到已經匹配的點會對這個點再進行遞迴直到到達未蓋點,這就構成了一條增廣路,具體見演算法演示。

(2)演算法演示

看這個圖:


演算法的演示過程如下:


看這個gif不好看的話可以看看下面這三位的部落格。

(3)演算法複雜度

如果二分圖的左半邊一共有n個點,最多找n條增廣路徑,如果圖中有m條邊,每一條增廣路徑把所有邊遍歷一遍,所以時間複雜度為O(n*m)。

圖論中獨立和覆蓋的關係:
α1 + β1 = n,即:邊覆蓋數 +邊獨立數 = n

α0 + β0 = n,即:點覆蓋數 +點獨立數 = n 

二部圖的點覆蓋數α0 =匹配數β1

二部圖的點獨立數β0 =邊覆蓋數α1

上面的這幾條關係非常重要,通常其他的問題可以通過這些關係轉化成求二分圖最大匹配的問題。

題意:假設有2臺機器,A和B。機器A有n種工作模式,分別稱為mode_0, mode_1, …, mode_n-1。同樣機器B 有m 種工作模式,分別為mode_0, mode_1, … , mode_m-1。剛開始時,A 和B 都工作在模式mode_0。

給定k 個作業,每個作業可以工作在任何一個機器的特定模式下。很顯然的是,為了完成所有的作業,必須時不時切換機器的工作模式,但不幸的是,機器工作模式的切換隻能通過手動重啟機器完成。要求改變作業的順序,給每個作業分配適的機器,使得重啟機器的次數最少。

題解:將兩臺機器的每個模式看成一個點,則在在每個作業在兩臺機器上的不同模式之間連一條邊。則此題可以看成求最小點覆蓋即,求最少的工作模式完成所有作業。在二分圖中,最小點覆蓋數=匹配數(最大點獨立數)。

程式碼:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
int n,m,k;
const int MAX=100+10;
int g[MAX][MAX];
int vis[MAX];
int match[MAX];

bool dfs(int u)
{
	for(int v=0;v<m;v++)
	{
		if(g[u][v]&&!vis[v])
		{
			vis[v]=1;
			if(match[v]==-1||dfs(match[v]))
			{
				match[v]=u;
				return true;
			}
		}
	}
	return false;
}

int hungray()
{
	int res=0;
	memset(match,0xff,sizeof(match));
	for(int i=0;i<n;i++)
	{
		memset(vis,0,sizeof(vis));
		if(dfs(i)) res++;
	}
	return res;
}
int main()
{
	//freopen("in.txt","r",stdin);
	//freopen("out.txt","w",stdout);
	while(scanf("%d",&n)&&n)
	{
		scanf("%d%d",&m,&k);
		memset(g,0,sizeof(g));
		for(int i=0;i<k;i++)
		{
			int a,b;
			scanf("%d%d%d",&i,&a,&b);
			if(a&&b)
				g[a][b]=1;
		}
		printf("%d\n",hungray());
	}
	return 0;
}