並查集經典題目


還是先看兩道題:

試題描述
俗話說得好,敵人的敵人就是朋友。

現在有n個人,編號1至n,初始互不相識。接下來有m個操作,操作分為兩種:

(1)檢查x號和y號是否是朋友,若不是,則變成敵人
(2)詢問x號的朋友有多少個
請你針對每個操作中的詢問給出回答。
輸入
第一行兩個正整數n、m,表示人的數量和操作的數量。
接下來m行,依次描述輸入。每行的第一個整數為1或2表示操作種類。對於操作(1),隨后有兩個正整數x,y。對於操作(2),隨后一個正整數x。
輸出
輸出包含m行,對於操作(1),輸入'N'或"Y",'N'表示x和y之前不是朋友,'Y'表示是朋友。對於操作(2),輸出x的朋友數量。
輸入示例
5 8
1 1 2
1 1 3
1 2 3
2 3
1 4 5
2 3
1 1 4
2 3
輸出示例
N
N
Y
1
N
1
N
2
其他說明
n,m<=300000。
對於80%的數據,不包含操作2。

 

試題描述

有N只動物分別編號為1,2,……,N。所有動物都屬於A,B,C中的一類。已知A能吃掉B,B能吃掉C,C能吃掉A。按順序給出下面的兩種信息共K條:
  第一種:x和y屬於同一類;
  第二種:x吃y。
然而這些信息可能會出錯,有可能有的信息和之前給出的信息矛盾,也有的信息可能給出的x和y不在1到N的范圍內。求在K條信息中有多少條是不正確的。計算過程中,我們將忽視諸如此類的錯誤信息。

呂紫劍為學有余力的同學提供了一個提高版,題目鏈接:http://oj.cnuschool.org.cn/oj/home/problem.htm?problemID=1000(事實上只提高了一點)

輸入
第一行兩個自然數,兩數間用一個空格分隔,分別表示N和K,接下來的K行,每行有三個數,第一個數為0或1,分別對應第一種或第二種,接着的兩個數,分別為該條信息的x和y,三個數兩兩之間用一個空格分隔。
輸出
一個自然數,表示錯誤信息的條數。
輸入示例
100 7
0 101 1
1 1 2
1 2 3
1 3 3
0 1 3
1 3 1
0 5 5
輸出示例
3
其他說明
數據范圍:1<=N<=50000,0<=K<=100000,其它說有輸入都不會超過100000.

兩道並查集的經典題目,現有兩種做法可供參考:

先以第一題(名為敵人)為例, 這題和那種並查集的模板題有一點區別,那就是這里多了一種關系:叫做朋友與敵人,如果只有一種關系那就好辦了。那怎么處理這多出來的一種關系呢?這里我們先介紹第一種方法,我們管它叫精神分裂法(又稱分身術)

現在我們有三個小朋友,由於本題的關系只有2種,於是我們只需召喚一個分身即可

好的我們成功召喚了這三個小朋友的三個分身,我們可以把一個分身a'當做a自己的敵人(上圖中連了他們的三條邊,但是這並沒有什么用。)

上圖中的連邊表示這1,2是敵人關系,2,3是敵人關系,這樣1,3就借着2的分身成為朋友了。但是這樣連邊並不完備

這樣的連邊才是比較完備的。

知道這些就可以寫程序了:

 1 #include<iostream>
 2 #include<cmath>
 3 #include<algorithm>
 4 #include<queue>
 5 using namespace std;
 6 const int maxn=300000+10;
 7 int read() 
 8 {
 9     int f=1,x=0;
10     char ch=getchar();
11     if(ch=='-') f=-1;
12     while(ch<'0'||ch>'9')
13     {
14         if(ch=='-')f=-1;
15         ch=getchar();
16     }
17     while(ch>='0'&& ch<='9') { x=x*10+ch-'0';  ch=getchar(); }
18     return x*f;
19 }
20 int n,m,tp,x,y,f[2*maxn],size[2*maxn];//其中size數組表示一個組所含元素的個數
21 int getf(int n){return f[n]==n ? n : f[n]=getf(f[n]);}//帶路徑壓縮版
22 void merge(int a,int b)
23 {
24      int x=getf(a),y=getf(b);
25      if(x!=y){f[y]=x;size[x]+=size[y];}
26      return;
27 }//合並兩個組
28 int main()
29 {
30     n=read();m=read();
31     for(int i=1;i<=2*n;i++){f[i]=i;}//並查集初始化
32     for(int i=1;i<=n;i++)size[i]=1;//每一個集合初始元素個數都是1
33     while(m--)
34     {
35         tp=read();
36         if(tp==1)
37         {
38             x=read();y=read();
39             if(getf(x)==getf(y))printf("Y\n");//這兩個人已經是朋友關系了
40             else
41             {
42                 merge(x,y+n);
43                 merge(y,x+n);//與分身合並
44                 printf("N\n");
45             }
46         }
47         if(tp==2)
48         {
49             x=read();
50             printf("%d\n",size[getf(x)]-1);//輸出x所在集合中元素的個數再-1也就是x朋友的個數(不算自己)
51         }
52     }
53     return 0;
54 }

 其中還有一些細節,第一個細節就是分身的存儲,分身也應該存在f數組,為了使下標不重復,所以a的分身存在a+n,b的分身存在b+n,以此類推。第二個細節就是並查集合並:在我寫的merge函數中,合並的是a所在的集合和b所在的集合,其中合並過程是f[x]=y;(x,y分別是a,b所在集合的標識元素),也就是把b合並到a里,因為題目中會隨時詢問朋友的個數,但是每一個集合中都包含敵人與朋友,不好處理。所以你可以看到:在第32行代碼並查集初始化,每一個集合容量初始為1時,是從1到n,並沒有算分身的初始化。因為分身是虛的,所在集合元素個數應該是0.這樣的設定帶來的好處就是:在合並敵人關系時,更新size數組,敵人不會被算在內。(這個有點難想,需要讀者仔細思考,認真體會)。

我們可以再通過食物鏈這種擁有三種關系的並查集習題來理解一下所謂的“分身術”,下面是代碼:

 1 #include<iostream>
 2 #include<algorithm>
 3 #include<cmath>
 4 #include<queue>
 5 using namespace std;
 6 const int maxn=150000+10;
 7 int n,k,t[maxn],a[maxn],b[maxn],ans;
 8 int f[maxn];
 9 int getf(int x){return x==f[x] ? x : f[x]=getf(f[x]);}
10 void unite(int x,int y)
11 {
12     int a=getf(x),b=getf(y);
13     if(a!=b)f[b]=a;
14     return;
15 }
16 int main()
17 {
18     scanf("%d%d",&n,&k);
19     for(int i=0;i<k;i++)
20         scanf("%d%d%d",&t[i],&a[i],&b[i]);
21     for(int i=0;i<3*n;i++)f[i]=i;
22     for(int i=0;i<k;i++)
23     {
24         int tp=t[i];
25         int x=a[i]-1,y=b[i]-1;
26         if(x<0 || x>=n || y<0 || y>=n)//輸入不合法
27         {
28             ans++;
29             continue;
30         }
31         if(tp==0)
32         {
33             if(getf(x)==getf(y+n) || getf(x)==getf(y+2*n) ) ans++;//屬於吃或被吃關系
34             else
35             {
36                 unite(x,y);
37                 unite(x+n,y+n);
38                 unite(x+n*2,y+n*2);//三個分身都合並
39             }
40         }
41         else 
42         {
43              if(getf(x)==getf(y) || getf(x)==getf(y+2*n) ) ans++;//同類或被吃關系
44              else
45              {
46                 unite(x,y+n);
47                 unite(x+n,y+2*n);
48                 unite(x+2*n,y);//三個分身交叉合並,表示吃的關系
49              }
50         }
51     }
52     printf("%d",ans);
53     return 0;
54 }

下面以食物鏈為例,我們介紹第二種方法:帶權並查集

食物鏈一題一共有三種關系:同類,吃,被吃   

同類權值為0,就像上圖

上圖中1到2邊的權值1,2到1的權值是2,分別表示1吃2,2被1吃

知道這些基礎的后,我們就可以通過點點之間的邊權的值來弄明白他們之間的關系了,我們用一個r數組來存這種關系,其中r[x]表示從x點指出去邊的權值,指向的是f[x]

首先是並查集的基礎:查找一個元素所在集合的標識元素

對n做一遍getf函數,順帶路徑壓縮(過程中順帶更新r[n]):起初n的祖先是f[n],所以節點n到節點f[n]存在一條邊權值為r[n],要想求得n到根的邊權,需要知道r[f[n]]的值,然后(r[n]+r[f[n]])%3就可算出n到根的關系了(要模3的原因是因為只有三種關系),r[f[n]]的值嗎。。就交給偉大的遞歸了:

1 int getf(int n)
2 {
3     if(f[n]==n)return n;
4     int tmp=f[n];//提前存下f[n]的值,因為程序執行完下一行后f[n]直接就變成n所在集合的表示元素了
5     f[n]=getf(f[n]);//路徑壓縮
6     r[n]=(r[tmp]+r[n])%3;//公式計算
7     return f[n];
8 } 

會寫getf函數后我們再來想想如何寫並查集合並的程序,在此題中針對兩個動物a,b我們是先知道他們之間的關系,才進行合並的,所以

圖中x,y分別是a,b所在集合的標識元素,在merge過程中,我們遵循y連到x的規則(其實都一樣),因為在未合並前,集合x和集合y是兩個沒有任何關系的集合,多了a到b的關系后,才建立聯系的。這里有一個非常非常重要的結論:一個並查集中從一個點出發,沿着它所指向的邊一直走,並累加下路上的權值(如果邊是反的那么就取權值的負數),走一圈回來,累加的和模3結果一定是0.這個我也不怎么會證明,但是讀者可以通過類似矢量的東西來理解這個結論,應該不算太難吧。根據這個結論和上圖,我們能得到一個等式:(r[b]+r[y]+k-r[a])%3=0,其中k是a到b的關系,這個邊其實在並查集中是不存在的,但是加上這條邊能更好的解釋。在這個等式里面,有哪些是我們不知道的呢?只有r[y],所以在合並時我們只需算出r[y]的值即可。r[y]=(r[a]-k-r[b]+3)%3,這個是由剛才的等式推出的,其中有一個+3是為了防負數的。

知道這些其實就可以做題了,本題的意思是找出所有不合法信息的個數,不合法信息包括數字問題,還有的就是並查集之間的沖突。對於輸入的兩個數a,b,如果是兩個未知關系的元素,我們就進行正常的並查集合並就可以了,如果他們之前有關系(即在一個集合之中)我們就需要分析他們的關系是否正確了。

由於r[a],r[b]存儲的都是a,b與他們的標識元素之間的關系,無法直接得出a和b的關系,但是假設a,b之間存在一個關系k,如果等式(k+r[b]-r[a]+3)%3=0的話,那么關系就就是正確的了,反之,則不對。

下面貼代碼:

 1 #include<iostream>
 2 #include<algorithm>
 3 #include<cmath>
 4 #include<cstring>
 5 using namespace std;
 6 int f[50010],r[50010],cnt,n,k,tp,a,b,x,y;
 7 int getf(int n)
 8 {
 9     if(f[n]==n)return n;
10     int tmp=f[n];
11     f[n]=getf(f[n]);
12     r[n]=(r[tmp]+r[n])%3;
13     return f[n];
14 } 
15 int main()
16 {
17     scanf("%d%d",&n,&k);
18     for(int i=1;i<=n;i++)f[i]=i;
19     while(k--)
20     {
21         scanf("%d%d%d",&tp,&a,&b);
22         if(a<1 || a>n || b<1 || b>n){cnt++;continue;}//數字非法
23         x=getf(a);y=getf(b);//找根
24         if(x!=y)
25         {
26             f[x]=y;//這里的合並與上面不同,是把x合並到y;
27             r[x]=(r[b]-r[a]+tp+999)%3;//我這里寫了999,並沒有寫3,其實都一樣
28         }
29         else if((tp+r[b]-r[a])%3) cnt++;//判斷關系是否正確
30     }
31     printf("%d",cnt);
32     return 0;
33 }

然后我們再來看看敵人這道題,用帶權並查集做

 1 #include<iostream>
 2 #include<algorithm>
 3 #include<cmath>
 4 #include<cstring>
 5 using namespace std;
 6 const int maxn=300000+10;
 7 int f[maxn],r[maxn],size1[maxn],size2[maxn],n,k,tp,a,b,x,y;
 8 int read() 
 9 {
10     int f=1,x=0;
11     char ch=getchar();
12     if(ch=='-') f=-1;
13     while(ch<'0'||ch>'9')
14     {
15         if(ch=='-')f=-1;
16         ch=getchar();
17     }
18     while(ch>='0'&& ch<='9') { x=x*10+ch-'0';  ch=getchar(); }
19     return x*f;
20 }
21 int getf(int n)
22 {
23     if(f[n]==n)return n;
24     int tmp=f[n];
25     f[n]=getf(f[n]);
26     r[n]=(r[tmp]+r[n])%2;
27     return f[n];
28 } 
29 int main()
30 {
31     n=read();k=read();
32     for(int i=1;i<=n;i++)
33     {
34        f[i]=i;
35        size1[i]=1;
36     }
37     while(k--)
38     {
39         tp=read();
40         if(tp==1)
41         {
42             a=read();b=read();
43             x=getf(a),y=getf(b);
44             if(x==y)
45             {
46                 if((r[a]-r[b]+2)%2==0) printf("Y\n");//是朋友關系
47                 else printf("N\n");
48             }    
49             else
50             {
51                 printf("N\n");
52                 f[x]=y;
53                 r[x]=(r[b]-r[a]+132245)%2;//這里的132245也是我閑的,只要是一個大於等於3的奇數即可(1+2,其中1為a與b的關系,2為防止負數)
54                 if(r[x]==0)
55                 {
56                     size1[y]+=size1[x];
57                     size2[y]+=size2[x];
58                 }
59                 else
60                 {
61                     size1[y]+=size2[x];
62                     size2[y]+=size1[x];
63                 }
64             }
65         }
66         else 
67         {
68             a=read();
69             x=getf(a);
70             if(r[a]==1) printf("%d\n",size2[x]-1);
71             else printf("%d\n",size1[x]-1);
72         }
73     }
74     return 0;
75 }

這道題多了一個隨時詢問並查集中朋友的個數,我們用兩個數組來維護,一個是size1,一個是size2,size1[x]表示在以X為標識元素的集合中x朋友的個數,size2相反,便是x的敵人了。在合並時X到y時,需要判斷一下,如果r[x]=0即X與y是朋友關系,那x的朋友也是y的朋友,x的敵人也是y的敵人。合並執行size1[y]+=size1[x];size2[y]+=size2[x];即可,反之如果r[x]=1即X與y是敵人關系,那x的朋友是y的敵人,x的敵人是y的朋友。合並執行size1[y]+=size2[x];size2[y]+=size1[x];在

在最后輸出時,也要判斷一下,根據所求元素a與a的標識元素x的關系來確定,究竟是輸出size1還是size2(勿忘減一,不算自己)

最后再提一句,食物鏈還有高級版本:就是不止有三種動物了,有n種,在這個題目下精神分裂法就顯得不是那么厲害了,需要兩兩合並,十分麻煩,用帶權並查集就快多了,只需在原題中改幾個數就可以了,幸虧在n<=10.哈哈哈哈


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM