並查集經典題目

還是先看兩道題:

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

現在有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的分身成爲朋友了。但是這樣連邊並不完備

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

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

按 Ctrl+C 複製代碼
按 Ctrl+C 複製代碼

 其中還有一些細節,第一個細節就是分身的存儲,分身也應該存在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.哈哈哈哈

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章