在金庸先生的小說裏面,有着這樣的人物關係圖
畫紅線的代表他們是朋友,可以知道,胡青牛和金毛獅王通過張無忌成爲了朋友,那麼如果張無忌和張三丰,認識的話,那金毛獅王也能和張三丰成爲朋友,即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;
}