1. 程式人生 > >ACM-並查集之暢通工程——hdu1232

ACM-並查集之暢通工程——hdu1232

Problem Description
某省調查城鎮交通狀況,得到現有城鎮道路統計表,表中列出了每條道路直接連通的城鎮。省政府“暢通工程”的目標是使全省任何兩個城鎮間都可以實現交通(但不一定有直接的道路相連,只要互相間接通過道路可達即可)。問最少還需要建設多少條道路?

Input
測試輸入包含若干測試用例。每個測試用例的第1行給出兩個正整數,分別是城鎮數目N ( < 1000 )和道路數目M;隨後的M行對應M條道路,每行給出一對正整數,分別是該條道路直接連通的兩個城鎮的編號。為簡單起見,城鎮從1到N編號。
注意:兩個城市之間可以有多條道路相通,也就是說
3 3
1 2
1 2
2 1
這種輸入也是合法的
當N為0時,輸入結束,該用例不被處理。

Output

對每個測試用例,在1行裡輸出最少還需要建設的道路數目。

Sample Input
4 2
1 3
4 3
3 3
1 2
1 3
2 3
5 2
1 2
3 5
999 0
0

Sample Output
1
0
2
998

Hint

Huge input, scanf is recommended.

暢通工程是並查集裡面,非常基礎且典型的一道題。

相互連線的城市構成一個集合,只需要判斷集合個數即可知道要修多少條路。

集合個數的判斷也可以根據每個集合只有一個根節點的特徵,找n個數裡有幾個根節點,並減去1。

為什麼減去1?  3個孤獨的城鎮互聯,只需要兩條路,同理三個集合之間關聯也只需要兩條路,所以是集合總數減1。

再說一下並查集,並查集的概念用數來理解較好,但是實現是用陣列來實現的。


就用上面這個圖來簡單說一下吧。

令a,b,c,d組成一個集合,e,f,g組成另一個集合。

談到樹的概念,樹的維護用father陣列,就是父節點,每一個樹葉都有各自的父節點,最終會查詢到這棵樹的根節點,根節點是唯一的。例如:d的父節點是b,b的父節點是a,a是這棵樹的根節點。

如何判斷d和c有聯絡呢?

通過向上查詢根節點,檢視這兩個點的根節點是否相同,即可知道它們兩者是否有聯絡。

d的根節點是a,c的父節點是a,同樣a也是父節點→可以發現d和c的根節點相同,所以可以判斷他們之間屬於一個集合,即有聯絡。

並查集,顯然就是將集合合併起來。

合併的意義就是讓兩個集合之間都可以相關聯,判斷關聯與否在於父節點是否相同。

因此,合併就是將一個樹為主樹(一般以節點個數多的為主),一個樹擴充套件成主樹的一條枝。

修改只需要修改從樹根節點的father值,將從樹根節點的父節點設定為主樹根節點。

這樣兩個樹就合併成一個樹了。

同右面的圖,判斷d和g是否相關聯:

d的根節點為a,g的父節點為f,f父節點e,e父節點為a,因為兩者根節點相同,所以這兩者是相關聯的。

這些都是我的理解,若有不對的地方,歡迎提出。

下面是這道題的程式碼,這道題提示中說資料量很大,最好用scanf,用cin其實也可以過,不過時間多了15MS

#include <stdio.h>
const int MAX=1000;
int father[MAX];

//初始化函式
void Init(int n)
{
    int i;
	for(i=1;i<=n;i++)
		father[i]=i;
}
//查詢函式
int Find(int x)
{
	while(father[x]!=x)
		x=father[x];

	return x;
}
//合併函式
void combine(int a,int b)
{
    int temp_a,temp_b;
	temp_a=Find(a);
	temp_b=Find(b);

	if(temp_a!=temp_b)
		father[temp_a]=temp_b;
}
//確定連通分量個數
int find_ans(int n)
{
    int i,sum=0;
    for(i=1;i<=n;++i)
        if(father[i]==i)
            ++sum;
    return sum;
}

int main()
{
	int i,n,m,a,b;

	while(scanf("%d",&n)!=EOF)
	{
	    if(!n)  break;
		Init(n);
		scanf("%d",&m);

		for(i=0;i<m;++i)
		{
			scanf("%d%d",&a,&b);
			combine(a,b);
		}
		printf("%d\n",find_ans(n)-1);
	}
	return 0;
}