poj1182:食物鏈
聽說是poj中最經典的一道並查集題目。我一做,果然很經典呢!好難啊!!!真的琢磨了很久還弄懂。這道題的重點就在於怎麼用並查集表示題目中的關係環。
1. 題幹
2. 思路詳解
實際上,在做這道題之前,我對並查集的瞭解就只停留在權重選擇和壓縮路徑上。也就是大家司空見慣的那種模板。順帶再默寫一遍複習一下:
#include <vector>
using namespace std;
class unionfind {
public:
vector<int> id, rank;
void init(int n){
id.clear(); rank.clear();
id.resize(n + 1); id.assign(n + 1, 1);
for (int i = 0; i < n + 1; i++) {id[n] = n;}
}
int find(int i){
while (id[i] != i){
id[i] = id[id[i]];
i = id[i];
}
return i;
}
void Union(int id1, int id2){
id1 = find(id1); id2 = find(id2);
if (rank[id1] > rank[id2]) {id[id2] = id1; rank[id1] += rank[id2];}
else {id[id1] = id2; rank[id2] += rank[id1];}
}
}uf;
經典的權值+壓縮路徑對吧?這應對模板題就夠用了,模板題一套就出來了。
但是這道題不可以用這個方向去思考。這道題的rank數組不能是權重,而應該是關係約束。
2.1 題意抽象
題目的意思很簡單,現在有三個物種,如果分別命名爲 A,B,C,那麼他們之間關係則爲:A喫B,B喫C,C喫A,然後判斷一個人說的話一是數據是否合法,二是是否前後矛盾。
首先要明確的是,如何用並查集代表三個物種?我們事先並沒有辦法給他們下一個定義,自然沒有辦法直接的將它們劃分成對應的ABC。所以直接劃分成這條路走不通。
不過其實我們可以換一個角度,從另一個視角去分析怎麼劃分種類。這個視角有點像是物理裏的相對運動。
我們把焦點放在某個物種上。假設我是其中一個生物,在我的視角看來,我和其他的所有生物的種類的關係應該是怎麼樣的呢?
思考一下,不難發現,對於我來說,我面前的種類應該劃分成3部分:
- 和我是同類的
- 會來喫我的種類
- 我會去喫的種類
不管我是A,B還是C,我總能把所有我能看到的生物分成這三個部分,而我並不需要在意自己從屬於哪個種類。也就是說,如果以我自己爲中心進行觀察,我並不需要關心自己或者別人到底是A,B還是C,我只需要根據關係就能把其他所有物種分成3個部分。
我們回到並查集,看看並查集的特點。並查集的每一個集合,是不是選一個點作爲根,其他所有的點都指向這個根節點?
發現了並查集的集合和上面題意抽象的關係嗎?我相當於集合裏的根,其他所有的動物相當於點,都指向根,根只需要確定根和某個點關係,就能完整地把環狀關係描述出來。
所以,這裏的rank數組,指代的就不再是權重,而是子節點與父節點的關係。爲了方便編程,我們規定,在rank中,0代表同類,1代表父節點捕食子節點,2代表子節點捕食父節點。
這麼規定的好處是,如果 (rank1 + 1) % 3 == rank2
可以說明兩者是捕食關係,利用了相鄰的性質得出來的。
2.2 find函數的相關分析
find
函數我們當然就要考慮路徑壓縮。這裏的路徑壓縮有不同的地方。因爲rank數組不再代表爲權重而是關係,在壓縮路徑的同時,子父節點的關係很有可能是會發生改變的。
我們函數的設計採用遞歸式設計。這樣比較方便,可以只考慮子節點直接以爺爺節點爲父節點時關係的變化。
我們先思考一下,子節點與爺爺節點要怎麼確定?我們知道子節點與父節點的關係,知道父節點與爺爺節點的關係,是不是可以嘗試用這個條件作跳板推導出子節點與爺爺節點之間的關係?
實際上呢就是可以的。我們直接將左右情況都找出來看看,實際上是有9種情況,很容易就找到了。
比如說,如果父節點與子節點的關係是同類,父節點與爺爺節點的關係是捕食,那麼可以得出子節點與爺爺節點的關係是捕食。以此類推,可以推出以下表格。
0 | 1 | 2 | |
---|---|---|---|
0 | 0 | 1 | 2 |
1 | 1 | 2 | 0 |
2 | 2 | 0 | 1 |
注:列爲 r1
,表示父節點與子節點的關係,行爲 r2
,表示爺爺節點與父節點的關係。假設 r3
表示爺爺節點與子節點的關係。
如果我說根據表格的規律,可以直接推出來, $r_3 = (r_1 + r_2) mod 3 $ ,能接受嗎?實際上就找個規律就出來了。
所以, find
函數就很好寫了。在子節點掛到爺爺節點上之後,按上面那個公式更新一下關係數組。
2.3 union函數的相關分析
union
函數最難的地方還是在關係的更新啊~~
雖然我們可以通過 find
函數找到根節點,但是兩個根節點合併的時候,我們其實是不能直接得到兩個根節點的關係的,需要經過推導。自己可以舉例試試,關係的更新還要推導一下的。
首先我們先分析一下,如果告訴我們X和Y的關係,他們倆關係數組要怎麼更新。題目有提到,1爲同類,2爲A喫B,可以簡單討論一下。
當 type == 1
時,XY爲同類,rank更新爲 rx = ry = type - 1 = 0
.
當 type == 2
時,輸入爲 X Y,表示X喫Y,(如果輸入反過來就是Y喫X),rank更新爲 rx or ry = type - 1
-->
指的是前者掛在後者上。掛完之後子節點的rank
會有更新。
所以X-->Y
之間的關係表達式就是 type - 1
。反過來是 (4 - type)%3
反過來求這個操作實際上就是已知X和Y是捕食,那麼Y和X是被捕食,然後用算式表達式表示一下。
我們接下來考慮X和fx(X的根節點)之間關係的轉換。
X-->fx
因爲 rx
存的本來就是對應的關係,所以表達式爲 rx
。fx-->X
需要推導一下,得到 (3 - rx) % 3
按照同樣的推導,可以得到 Y-->fy
fy-->Y
。分別是 ry
(3 - ry) % 3
。重點是 fy -- > fx
我們先把它假設成 rxy
吧!
這樣的話,我們可以列出一個轉換式子:fy --> Y --> X == fy --> X
重點是怎麼化簡。還記得 find
函數推出來的結論嗎?子節點掛到爺爺節點的更新關係是 (r1 + r2) % 3
,所以 fy --> X
很自然就變成了 (d - 1 + 3 - ry) % 3
由於有fy --> X --> fx == fy --> fx
可以得到是 (d - 1 + 3 - ry + rx) % 3
,於是更新函數就這樣搞定了……
2.3 轉化成符合題意的代碼
最首要的條件是數據是否合法,不合法直接算說謊。題目已經指出所有不合法的情況,直接判斷即可。
接下來是在同一集合的情況。
- 如果輸入爲同類但是
rank
的值不同,那就是說謊。 - 如果輸入爲捕食,如果
(rank[x] + 1) % 3 != rank[y]
說明說謊
如果不在一個集合,說明之前沒有定義過,將兩個合併。
最後輸出即可。
3. AC代碼(C++)
#include <cstdio>
#include <vector>
using namespace std;
class uf{
public:
vector<int> id, rank;
void init(int n){
id.resize(n); rank.resize(n);
for (int i = 0; i < n; i++) {id[i] = i;}
}
int find(int i){
if (id[i] == i) return i;
int t = id[i];
id[i] = find(id[i]);
rank[i] = (rank[i] + rank[t]) % 3;
return id[i];
}
void Union(int x, int y, int type){
int fx = find(x), fy = find(y);
id[fy] = fx;
rank[fy] = (3 - rank[y] + type - 1 + rank[x]) % 3;
}
}uf;
int main(){
int num = 0, k = 0, cnt = 0;
int type = 0, id1 = 0, id2 = 0;
scanf("%d%d", &num, &k);
uf.init(num + 1);
for (int i = 0; i < k; i++){
scanf("%d%d%d", &type, &id1, &id2);
if (id1 > num || id2 > num || (id1 == id2 && type == 2)) {cnt++;}
else if (uf.find(id1) == uf.find(id2)){
if (type == 1 && uf.rank[id1] != uf.rank[id2]) {cnt++;}
if (type == 2 && (uf.rank[id1] + 1) % 3 != uf.rank[id2]) {cnt++;}
}
else {uf.Union(id1, id2, type);}
}
printf("%d\n", cnt);
return 0;
}