從金庸小說,親戚問題,格子游戲帶你深入瞭解------並查集

在金庸先生的小說裏面,有着這樣的人物關係圖
在這裏插入圖片描述

畫紅線的代表他們是朋友,可以知道,胡青牛和金毛獅王通過張無忌成爲了朋友,那麼如果張無忌和張三丰,認識的話,那金毛獅王也能和張三丰成爲朋友,即A和B是朋友,B和C是朋友,那麼A和C也是朋友

下面我們再看一下親戚問題

題目描述

或許你並不知道,你的某個朋友是你的親戚。他可能是你的曾祖父的外公的女婿的外甥女的表姐的孫子。如果能得到完整的家譜,判斷兩個人是否親戚應該是可行的,但如果兩個人的最近公共祖先與他們相隔好幾代,使得家譜十分龐大,那麼檢驗親戚關係實非人力所能及。在這種情況下,最好的幫手就是計算機。爲了將問題簡化,你將得到一些親戚關係的信息,如Marry和Tom是親戚,Tom和Ben是親戚,等等。從這些信息中,你可以推出Marry和Ben是親戚。請寫一個程序,對於我們的關於親戚關係的提問,以最快的速度給出答案。

輸入

輸入由兩部分組成。

第一部分以N,M開始。N爲問題涉及的人的個數(1≤N≤20000)。這些人的編號爲1,2,3,…, N。下面有M行(1≤M≤1 000 000),每行有兩個數ai, bi,表示已知ai和bi是親戚。

第二部分以Q開始。以下Q行有Q個詢問(1≤Q≤1 000 000),每行爲ci, di,表示詢問ci和di是否爲親戚。

輸出

對於每個詢問ci, di,輸出一行:若ci和di爲親戚,則輸出“Yes”,否則輸出“No”。

樣例輸入

10 7
2 4
5 7
1 3
8 9
1 2
5 6
2 3
3
3 4
7 10
8 9

樣例輸出

Yes
No
Yes

用集合的思路,對於每個人建立一個集合,開始的時候集合元素是這個人本身,表示開始時不知道任何人是他的親戚。以後每次給出一個親戚關係時,就將兩個集合合併。這樣實時地得到了在當前狀態下的集合關係。如果有提問,即在當前得到的結果中看兩元素是否屬於同一集合。對於樣例數據的解釋如下圖:

在這裏插入圖片描述

由上圖可以看出,操作是在集合的基礎上進行的,沒有必要保存所有的邊而且每一步得到的劃分方式是動態的。

我們可以運用並查集的思想實現它

那麼什麼是並查集?

它所處理的是“集合”之間的關係,即動態地維護和處理集合元素之間複雜的關係,當給出兩個元素的一個無序對(a,b)時,需要快速“合併”a和b分別所在的集合,這其間需要反覆“查找”某元素所在的集合。“並”、“查”和“集”三字由此而來。

在這裏插入圖片描述

用father[i]表示元素i的父親結點,進行不斷併到不同的集合中

查找可以有兩種方法

用非遞歸的方法實現

int find(int x)
{
 while(father[x]!=x) x=father[x];
 return x;
} 

用遞歸的方法實現

if(father[x]!=x) return find(father[x]);
else return x;

合併

void unionn(int a,int b)
{
 father[a]=b;
} 

然而,通過這種方法去求,往往時間超限,我們需要進行路徑壓縮實現優化

路徑壓縮實際上是在找完根結點之後,在遞歸回來的時候順便把路徑上元素的父親指針都指向根結點。

就像上面的圖中,合併3和4,不是把4指向3,而是直接把4指向3的根節點1

由此,我們得到了一個複雜度幾乎爲常數的算法

也就是
用遞歸實現路徑壓縮-------查

int find(int x)
{
 if(father[x]!=x) father[x]=find(father[x]);
 return father[x];
}

void unionn(int a,int b)
{
 a=find(a);
 b=find(b);
 if(a!=b)
  father[a]=b;
}

使用這兩部分進行優化之後,代碼基本就可以AC了,不過還有一點需要注意,如果使用cin,cout的也需要改爲scanf,printf,因爲cin比scanf慢很多,特別是在這種數據量很大的情況下,劣勢更加明顯

下面是AC代碼

#include<iostream>
#include<cstdio>
using namespace std;
int father[20000];
//int find(int x)
{
	if(father[x]!=x) father[x]=find(father[x]);
	return father[x];
}
//並 
void unionn(int a,int b)
{
	a=find(a);
	b=find(b);
	if(a!=b)
		father[a]=b;
}

int main()
{
	int m,n,x,y,i,r1,r2,q;
	scanf("%d %d",&n,&m);
		for(i=1;i<=n;i++) father[i]=i;
		for(i=1;i<=m;i++)
		{
			cin>>x>>y;
			r1=find(x);
			r2=find(y);
			if(r1!=r2) unionn(r1,r2);
		}
		cin>>q;
		for(i=1;i<=q;i++)
		{
			scanf("%d %d",&x,&y);
			if(find(x)==find(y)) printf("Yes\n");
			else printf("No\n"); 
		}
	
}

下面我們再看一題------格子游戲

題目描述

Alice和Bob玩了一個古老的遊戲:首先畫一個n * n的點陣(下圖n = 3)   
接着,他們兩個輪流在相鄰的點之間畫上紅邊和藍邊:

直到圍成一個封閉的圈(面積不必爲1)爲止,“封圈”的那個人就是贏家。因爲棋盤實在是太大了(n <= 200),他們的遊戲實在是太長了!他們甚至在遊戲中都不知道誰贏得了遊戲。於是請你寫一個程序,幫助他們計算他們是否結束了遊戲?

輸入

輸入數據第一行爲兩個整數n和m。m表示一共畫了m條線。以後m行,每行首先有兩個數字(x, y),代表了畫線的起點座標,接着用空格隔開一個字符,假如字符是"D ",則是向下連一條邊,如果是"R "就是向右連一條邊。輸入數據不會有重複的邊且保證正確。

輸出

輸出一行:在第幾步的時候結束。假如m步之後也沒有結束,則輸出一行“draw”

樣例輸入

3 5
1 1 D
1 1 R
1 2 D
2 1 R
2 2 D

樣例輸出

4

這個題也是使用並查集的思想

首先初始化結點爲自身

查,此處返回值是node型,傳入的也是node

node root(node k)
{
	//說明他的根節點就是自身 
	if(f[k.x][k.y].x==k.x&&f[k.x][k.y].y==k.y) return k;
	//繼續遞歸,路徑壓縮 
	f[k.x][k.y]=root(f[k.x][k.y]);
	return f[k.x][k.y];
}

在這裏插入圖片描述

就比如這個圖,如果(1,1)的根節點和(2,1)的根節點相同,就說明連接之後,可以實現封閉,如果不同,那麼中間就有開路,無法實現封閉

下面是AC代碼

#include<iostream>
#include<cstdio>
using namespace std;
struct node
{
	int x;
	int y;
}f[210][210],k1,k2;
node root(node k)
{
	//說明他的根節點就是自身 
	if(f[k.x][k.y].x==k.x&&f[k.x][k.y].y==k.y) return k;
	//繼續遞歸,路徑壓縮 
	f[k.x][k.y]=root(f[k.x][k.y]);
	return f[k.x][k.y];
}
int main()
{
	int m,n,i,j,x,y;
	char op;
	scanf("%d %d",&n,&m);
	//初始化根節點爲自身
	for(i=1;i<=n;i++)
	{
		for(j=1;j<=n;j++)
		{
			f[i][j].x=i;
			f[i][j].y=j;
		}
	}
	for(i=1;i<=m;i++)
	{
		cin>>x>>y>>op;
		if(op=='D')
		{
			k1=root(f[x][y]);
			k2=root(f[x+1][y]);
		} 
		if(op=='R')
		{
			k1=root(f[x][y]);
			k2=root(f[x][y+1]);
		}
		//封閉 
		if(k1.x==k2.x&&k1.y==k2.y)
		{
			cout<<i<<endl;
			return 0;
		}
		//未封閉 
		else
		{
			f[k1.x][k1.y]=k2;
		}
	}
	cout<<"draw"<<endl;
	return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章