1. 程式人生 > >POJ-1182-食物鏈 (並查集)

POJ-1182-食物鏈 (並查集)

原題連結https://vjudge.net/contest/235901#problem/O
動物王國中有三類動物A,B,C,這三類動物的食物鏈構成了有趣的環形。A吃B, B吃C,C吃A。
現有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
第一行是兩個整數N和K,以一個空格分隔。
以下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
題意:
中文題
題解:
這道題目是一個比普通並查集更深層次的並查集,普通並查集都只有一層關係(即我倆是不是一個類別),而這道題目則有多層關係(不僅有是否為同類別,還有不同類別之間的關係),這道題目重在思想。
主要思想:
把之前所有的真話裡的動物的類別以及關係當成一個整體的大集合(下文稱為真話集合),可以有一個共同的根節點,因為與根節點僅有三種關係:同類,吃,被吃。
之後每輸入一個新的指令後(條件2,3很好判斷,不細說了),判斷a,b是否已經在真話集合裡:
如果不在則合併到真話集合裡;(注意:如果兩個都不在原有的真話集合裡,則相當於新增加一個真話集合2號,但如果在之後的指令中與之前的真話集合1號有關係,則可以在整體上合併成新的真話集合1號)
如果已經在真話集合裡了,就判斷兩個人之間的關係與之前的真話是否衝突。
細節:
細節主要有兩個方面:壓縮路徑、更新關係域。

  • 壓縮路徑:這個很簡單,通過遞迴找到最頂層的根節點
  • 關係域更新:這個是重點,如果不更新關係域的話,壓縮路徑是存在問題的。那麼應該怎麼更新關係域呢?既然我壓縮路徑時是直接找到最頂層的根節點,那麼可以在每向前壓縮一次路徑的時候,更新與此時的根節點的關係,這樣在路徑壓縮完成時,關係域也恰好更新完成
下面時壓縮路徑與更新關係域的具體程式碼:
int findf(int i)//查詢父節點,同時壓縮路徑並且更新與根節點的關係
{
    if(i==ani[i].pre)
        return i;
    int tmp=ani[i].pre;//定義tmp為其根節點
    ani[i].pre=findf(tmp);//壓縮路徑(注意遞迴過程中同時也在不斷更新關係)
    ani[i].rel=(ani[i].rel+ani[tmp].rel)%3;//更新與根節點關係-----*式
    return ani[i].pre;
}

在這裡具體解釋一下*式:
這裡設x為子節點,tmp為x的根節點,root為tmp的根節點。
路徑壓縮由:x->tmp->root—>>x->root
所以自然關係也應該更新為x與root的關係:

  1. 如果x與tmp的關係是同類即0,則x與root的關係就是tmp與root的關係
  2. 如果x與tmp的關係是吃與被吃的關係即2或1,由於動物之間的關係是個迴圈,所以直接模3相加即可:
這裡拿1(即tmp吃x)來舉例:
  • 如果tmp與root同類(rel=0),模三相加後得到的依舊是1(即root吃x)
  • 如果root吃tmp(rel=1),模三相加後得到的是2(即x吃root)
  • 如果tmp吃root(rel=2),模三相加後得到的是0(即x與root是同類)
這裡很巧妙的用了模三加法,其中靈感也應該來自於關係是個環。(更多細節見程式碼)

附上AC程式碼:

#include <iostream>

using namespace std;

int n,k,fakes=0;

struct node
{
    int pre;//父節點
    int rel;//與父節點的關係:0 同類,1 被吃,2 吃
}ani[50005];

int findf(int i)//查詢父節點,同時壓縮路徑並且更新與根節點的關係
{
    if(i==ani[i].pre)//如果根節點就是自己則證明此節點就是根節點
        return i;
    int tmp=ani[i].pre;//定義tmp為其根節點
    ani[i].pre=findf(tmp);//壓縮路徑(注意遞迴過程中同時也在不斷更新關係)
    ani[i].rel=(ani[i].rel+ani[tmp].rel)%3;//更新與根節點關係
    return ani[i].pre;//返回根節點
}
int main()
{
    ios::sync_with_stdio(false);
    int oper,a,b,root1,root2;
    cin>>n>>k;
    for(int i=1;i<=n;++i)//初始化
    {
        ani[i].pre=i;//父節點為自己
        ani[i].rel=0;//關係為同類
    }
    for(int i=1;i<=k;++i)
    {
        cin>>oper>>a>>b;
        if(a>n||b>n)//比n大:假話
        {
            fakes++;
            continue;
        }
        if(oper==2&&a==b)//x吃x:假話
        {
            fakes++;
            continue;
        }
        root1=findf(a);
        root2=findf(b);
        if(root1!=root2)//根節點不同,合併到之前所說的所有真話構成的集合裡
        {
            ani[root2].pre=root1;//更新根節點
            ani[root2].rel=(oper-1+ani[a].rel-ani[b].rel+3)%3;//更新關係
        }
        else//以已經在之前的真話集合裡,則判斷是否有衝突
        {
            if(oper==1&&ani[a].rel!=ani[b].rel)//如果是同類,但是與根節點的關係不一樣(與前面的話衝突):假話
            {
                fakes++;
                continue;
            }
            if(oper==2&&((ani[b].rel-ani[a].rel+3)%3!=oper-1))//如果是吃與被吃的關係,但與之前在真話集合裡的關係不同:假話
            {
                fakes++;
                continue;
            }
        }
        cout<<fakes<<endl;
    }
    return 0;
}

歡迎評論!