某個家族人員過於龐大,
要判斷兩個人是否是親戚很不容易。
現在給出某個親戚關系圖,
求任意給出的兩個人是否具有親戚關系。
這是親戚的題面
我們先不要管這道題的輸入輸出,
我們假設他給出的不是兩個人的親戚關系,
而是告訴你a是b的兒子。
那我這個時候就出現了二條
基本的法則:
1.如果a是b的兒子,那么b就不是a的兒子。
2.一個人可以有多個兒子,但只能有一個父親。
這個時候我們再來思考,
什么情況下兩人構成親戚關系呢?
他們可能是兄弟關系,也就是有共同的父親,
有可能是父子關系,也就是一方是另一方的父親,
有可能是叔侄關系……
關系有很多,但其實只有一條——
親戚的親戚就是親戚,
跟自己親戚不是親戚的就不是親戚。
根據這條,我們可以將一個家族譜簡單處理。
由此發現,無論如何,
每個小家庭都構成一個樹,
而整個大家庭就是一片森林。
因此親戚其實就是
判斷他是否在同一個樹。
只需要記錄下來他們的父親,
不斷遞歸,來判斷樹根是否一致,
樹根就是自己是自己的父親。
代碼如下:
int find(int x)
{
if(x!=fa[x])//如果不是樹根
{
return find(fa[x]);//返回父親的父親
}
return fa[x];//不然返回樹根
}
這樣子看起來很完美,
事實上問題也很明顯,
那就是當數據足夠大的時候。
遞歸會顯得很吃力。
其實我們求的是樹根,
但是我們卻每一次都要不斷的遞歸,
如果父親數組直接記錄樹根,那就可以節省很多時間。
我們每一次去找父親的父親時,
返回來的是樹根,
這個時候我們把父親設為這個樹根,
等到下一次再搜的時候可以直接返回樹根。
當然這個方法也有問題——大逆不道!
前一秒你的父親還是你的父親,
后一秒他就跟你是兄弟了。
當然,你父親的父親也成了你的兄弟……
但我們才管他是不是大孝子,我們能AC就行,
由此打出優化
int find(int x)
{
if(x!=fa[x])//如果不是樹根
{
fa[x]=find(fa[x]);//把父親設為父親的父親
}
return fa[x];//返回樹根
}
現在我們重新看回題面,
打出這個題目的代碼。
#include<bits/stdc++.h>
using namespace std;
int fa[100001];
int find(int x)
{
int son=x;
if(x!=fa[x])
{
fa[x]=find(fa[x]);
}
return fa[x];
}
void join(int a,int b)//合並的代碼,僅供參考。
{
int x=find(a);
int y=find(b);
if(x!=y)fa[x]=fa[y];//注意是設為別人的父親,這是一個優化。
return;
}
bool pd(int a,int b)
{
int x=find(a);
int y=find(b);
if(x!=y)return false;
else return true;
}
int main()
{
int n,m,k;
scanf("%d%d%d",&n,&m,&k);
for(int i=1; i<=n; i++)fa[i]=i;
for(int i=1; i<=m; i++)
{
int x,y;
scanf("%d%d",&x,&y);
join(x,y);
}
scanf("",);
for(int i=1; i<=k; i++)
{
int x,y;
scanf("%d%d",&x,&y);
if(pd(x,y)) printf("Yes\n");
else printf("No\n");
}
return 0;
}
這個算法就是並查集算法,
並代表合並,
查代表查詢,
集代表集合。
也就是可以合並和查詢的一個集合。
並查集雖然是一種圖論算法,但其實只要學了遞歸就可以學。
那么他為什么是一種圖論算法呢?
因為它可以判斷回路,
同時還存在帶權並查集這種玩法,
最小生成樹經典算法——
Kruskal(克魯斯卡爾)算法就是用並查集實現判斷回路的。
如果評論區有人讓我講的話,
我以后可能會去把這個克魯斯卡爾算法講一下。
那么今天就講到這里,如有不對請指正!