對應 POJ 題目:點擊打開鏈接
Time Limit: 1000MS | Memory Limit: 10000K | |
Total Submissions: 54927 | Accepted: 16104 |
Description
現有N個動物,以1-N編號。每個動物都是A,B,C中的一種,但是我們並不知道它到底是哪一種。
有人用兩種說法對這N個動物所構成的食物鏈關係進行描述:
第一種說法是"1 X Y",表示X和Y是同類。
第二種說法是"2 X Y",表示X喫Y。
此人對N個動物,用上述兩種說法,一句接一句地說出K句話,這K句話有的是真的,有的是假的。當一句話滿足下列三條之一時,這句話就是假話,否則就是真話。
1) 當前的話與前面的某些真的話衝突,就是假話;
2) 當前的話中X或Y比N大,就是假話;
3) 當前的話表示X喫X,就是假話。
你的任務是根據給定的N(1 <= N <= 50,000)和K句話(0 <= K <= 100,000),輸出假話的總數。
Input
以下K行每行是三個正整數 D,X,Y,兩數之間用一個空格隔開,其中D表示說法的種類。
若D=1,則表示X和Y是同類。
若D=2,則表示X喫Y。
Output
Sample Input
100 7 1 101 1 2 1 2 2 2 3 2 3 3 1 1 3 2 3 1 1 5 5
Sample Output
3
題意:中文題,不多說
思路:
開始時把同一類的動物並在相同的集合往死裏想,做了一堆無用功;首先要正確理解的一點是:要把相互之間有關係(喫與被喫關係,同類關係)的,或者他們之間的關係可以通過集合所構成的樹推導出來的動物(比如x, y)並在相同的集合
如上圖,在這棵以 5 爲根的樹中,其中每個結點之間都存在關係,比如 1 和 2 之間,他們的關係要麼是1 喫 2; 要麼是 2 喫 1 ;要麼1 和 2 是同類。確定他們之間關係的方法將重點講。
我們用 fa[x] = y 表示 x 的父親結點爲 y;然後用一個rela[] 數組表示 x 與 fa[x] 的關係。
rela[x] = 0 表示 x 與 fa[x] 是同類,
rela[x] = 1 表示 x 喫 fa[x],
rela[x] = 2 表示 fa[x] 喫 x。
看這樣一顆3個結點的樹,
(圖1) (圖2)
假如此時rela[x] = 1;relax[y] = 1;relax[z] = 0(也只能等於0,因爲它是根,自己跟自己是同類)
即是 x 喫 y ,y 喫 z ;由這兩點再根據題目意思還可以知道 z 喫 x
當我們對 x 調用 fx = Find(x) 時,這時必須知道什麼是並查集的路徑壓縮,不知道的先弄懂再往下看;
我們就會把(圖1 )變成(圖2),但此時rela[x] = 1 就錯了(這樣就是 x 喫 z ,與原意 z 喫 x 不符)
所以在壓縮路徑的時候要修改 rela[x] 的值,怎樣修改?!
其實我們可以根據 x 對 y 的關係,以及 y 對 z 的關係,推導出 x 對 z 的關係。枚舉這個過程就是:
rela[x] rela[y] rela[z] | 修改後的 rela[x]
0 0 0 0
0 1 0 1
0 2 0 2
1 0 0 1
1 1 0 2
1 2 0 0
2 0 0
2
2 1 0
0
2
2 0 1
通過枚舉所有情況我們發現
修改後的 rela[x] = (rela[x] + rela[y]) % 3;
因此在令 z = fa[x] 之前加上這句:rela[x] = (rela[x] + rela[y]) % 3 就能正確描述結點之間的關係。
-------------------------------------------------------------------------------------------------------------------------------------------
這樣對於一個操作 d x y;我們可以這樣處理:
令 fx = Find(x); fy = Find(y); 如果 fx == fy ;即 x 與 y 在同一個集合裏。這好辦,根據路徑壓縮我們會得到這樣的一棵樹:
即 x 與 y 都直接指向根結點(也就是集合的代表)。這時我們知道的是 x 對 root 的關係 rela[x] 和 y 對 root 的關係 rela[y];問題是我們要知道的是 x 對 y 的關係,因爲我們要以此來判斷 d x y 是否正確。
如果我們把上圖想象成(只是想象,實際並不進行這樣的操作):
即改變了root 與 y 的父子關係
rela[x] 沒有變,我們不去動它;但 rela[root] 不再是 0 ,這裏我直接說了,通過枚舉我們可以知道 rela[root] = 3 - rela[y]。接下來有沒有熟悉的感覺,就是上面(圖1)的那個過程;這裏就可以確定 x 與 y 的關係了。即 x 對 y 的關係 rxy = (rela[x] + rela[root]) % 3;展開就是 rxy = (rela[x] + (3 - rela[y])) % 3;
接下來如果 rxy != d - 1 的話,那 d x y 這句話就是假的(把 d = 1 和 d = 2 代入你會發現的確是這樣)
如果 fx != fy 呢?這就說明 x 與 y 不在同一個集合裏面 。這更好辦,說明 x 與 y 還沒有任何關係,你怎樣說都是對的。麻煩的是定義了x 與 y 的關係之後(比如 1 x y 或 2 x y),你要把 x 所在的集合跟 y 所在的集合合併,使得 x 與 y 存在關係。
我們通過 fx = Find(x) 和 fy = Find(y) 同樣可以得到這樣圖:
爲了方便觀察我們可以簡化這個圖:
這裏我們把 x 所在集合接到 y 所在集合裏,即最終令 fa[fx] = fy,即
但在這之前我們要先確定 fx 對 fy 的關係,即修改 rela[fx]。怎樣修改?我們通過 d x y 操作可以知道 x 對 y 的關係爲 d - 1 (d = 1 那 d - 1 = 0 就表示 x 與 y 是同類,d = 2 那 d - 1 = 1 就表示 x 喫 y,跟我們的定義是一樣的);即我們知道了 x 對 y 的關係,y 對 fy 的關係,那根據前面的公式, x 對 fy 的關係 x_fy = ((d - 1) + rela[y]) % 3。到這裏我們可以想象到這樣一個圖:
這裏我們知道 x 對 fx 的關係 rela[x] 和 x 對 fy 的關係 x_fy;那我們可以像前面那樣改變 x 與 fx 的父子關係,使圖變成這樣:
相應地可以求 fx 對 x 的關係爲 fx_x = 3 - rela[x];這樣就可以求 fx 對 fy 的關係了!即 fx 對 fy 的關係 fx_fy = (fx_x + x_fy) % 3。把原數代入就是:
fx_fy = ((3 - rela[x]) + ((d - 1) + rela[y]) % 3) % 3 也就是:
fx_fy = (2 - rela[x] + d + rela[y]) % 3
這樣就可以合併兩個集合了。
必須歎服!這道題把並查集運用地非常巧妙!
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 50010
int fa[N];
int rela[N];
void Init()
{
int i;
for(i = 0; i < N; i++){
fa[i] = i;
rela[i] = 0;
}
}
int Find(int x)
{
int fx;
if(x == fa[x]) return x;
fx = Find(fa[x]);
rela[x] = (rela[x] + rela[fa[x]]) % 3;
fa[x] = fx;
return fx;
}
void Union(int x, int y, int d) /* 使 x 所在集合成爲 y 所在集合的子集合 */
{
int fx, fy;
fx = fa[x];
fy = fa[y];
rela[fx] = ((3 - rela[x]) + (d - 1) + rela[y]) % 3;
fa[fx] = fy;
}
int main()
{
#if 0
freopen("in.txt","r",stdin);
#endif
int n, m, count = 0;
scanf("%d%d", &n, &m);
Init();
while(m--){
int d, x, y, fx, fy;
scanf("%d%d%d", &d, &x, &y);
if(x < 1 || x > n || y < 1 || y > n){
count++;
continue;
}
fx = Find(x);
fy = Find(y);
if(fx == fy){
if((rela[x] + (3 - rela[y])) % 3 != d - 1)
count++;
}
else Union(x, y, d);
}
printf("%d\n", count);
return 0;
}