POJ 並查集 題目匯總 ——czyuan原創

來源:互聯網
上載者:User

繼續資料結構的複習,本次的專題是:並查集。
      並查集,顧名思義,乾的就是“並”和“查”兩件事。很多與集合相關的操作都可以用並查集高效的解決。
       兩個作業碼:
       int Find(int x)
       {
          if (tree[x].parent != x)
          {
              tree[x].parent = Find(tree[x].parent);
          }
          return tree[x].parent;
       }
       void Merge(int a, int b, int p, int q, int d)
       {
          if (tree[q].depth > tree[p].depth) tree[p].parent = q;
          else
          {
              tree[q].parent = p;
              if (tree[p].depth == tree[q].depth) tree[p].depth++;
          }
       }
       其中Find()函數用了路徑壓縮最佳化,而Merge()函數用了啟發學習法合并的最佳化(個人感覺有了路徑壓縮,啟發學習法合并最佳化的效果並不明顯,而經常因為題目和代碼的限制,啟發學習法合并會被我們省略)。
       提到並查集就不得不提並查集最經典的例子:食物鏈。
       POJ 1182 食物鏈
       http://acm.pku.edu.cn/JudgeOnline/problem?id=1182
       題目告訴有3種動物,互相吃與被吃,現在告訴你m句話,其中有真有假,叫你判斷假的個數(如果前面沒有與當前話衝突的,即認為其為真話)
這題有幾種做法,我以前的做法是每個集合(或者稱為子樹,說集合的編號相當於子樹的根結點,一個概念)中的元素都各自分為A, B, C三類,在合并時更改根結點的種類,其他點相應更改位移量。但這種方法公式很難推,特別是位移量很容易計算錯誤。
下面來介紹一種通用且易於理解的方法:
首先,集合裡的每個點我們都記錄它與它這個集合(或者稱為子樹)的根結點的相對關係relation。0表示它與根結點為同類,1表示它吃根結點,2表示它被根結點吃。
那麼判斷兩個點a, b的關係,我們令p = Find(a), q = Find(b),即p, q分別為a, b子樹的根結點。
       1. 如果p != q,說明a, b暫時沒有關係,那麼關於他們的判斷都是正確的,然後合并這兩個子樹。這裡是關鍵,如何合并兩個子樹使得合并後的新樹能保證正確呢?這裡我們規定只能p合并到q(剛才說過了,啟發學習法合并的最佳化效果並不那麼明顯,如果我們用啟發學習法合并,就要推出兩個式子,而這個推式子是件比較累的活...所以一般我們都規定一個子樹合到另一個子樹)。那麼合并後,p的relation肯定要改變,那麼改成多少呢?這裡的方法就是找規律,列出部分可能的情況,就差不多能推出式子了。這裡式子為 : tree[p].relation
= (tree[b].relation - tree[a].relation + 2 + d) % 3; 這裡的d為判斷語句中a, b的關係。還有個問題,我們是否需要遍曆整個a子樹並更新每個結點的狀態呢?答案是不需要的,因為我們可以在Find()函數稍微修改,即結點x繼承它的父親(注意是前父親,因為路徑壓縮後父親就會改變),即它會繼承到p結點的改變,所以我們不需要每個都遍曆過去更新。
       2. 如果p = q,說明a, b之前已經有關係了。那麼我們就判斷語句是否是對的,同樣找規律推出式子。即if ( (tree[b].relation + d + 2) % 3 != tree[a].relation ), 那麼這句話就是錯誤的。
       3. 再對Find()函數進行些修改,即在路徑壓縮前紀錄前父親是誰,然後路徑壓縮後,更新該點的狀態(通過繼承前父親的狀態,這時候前父親的狀態是已經更新的)。
       核心的兩個函數為:
       int Find(int x)
       {
           int temp_p;
          if (tree[x].parent != x)
          {
              // 因為路徑壓縮,該結點的與根結點的關係要更新(因為前面合并時可能還沒來得及更新).
              temp_p = tree[x].parent;
              tree[x].parent = Find(tree[x].parent);
              // x與根結點的關係更新(因為根結點變了),此時的temp_p為它原來子樹的根結點.
              tree[x].relation = (tree[x].relation + tree[temp_p].relation) % 3;
          }
          return tree[x].parent;
       }
       void Merge(int a, int b, int p, int q, int d)
       {
          // 公式是找規律推出來的.
          tree[p].parent = q; // 這裡的下標相同,都是tree[p].
          tree[p].relation = (tree[b].relation - tree[a].relation + 2 + d) % 3;
       }
       而這種紀錄與根結點關係的方法,適用於幾乎所有的並查集判斷關係(至少我現在沒遇到過不適用的情況…可能是自己做的還太少了…),所以向大家強烈推薦~~
       搞定了食物鏈這題,基本POJ上大部分基礎並查集題目就可以順秒了,這裡僅列個題目編號: POJ 1308 1611 1703 1984 1986(LCA Tarjan演算法 + 並查集) 1988 2236 2492 2524。
       下面來講解幾道稍微提高點的題目:
       POJ 1456 Supermarket
       http://acm.pku.edu.cn/JudgeOnline/problem?id=1456
       這道題貪心的思想很明顯,不過O(n^2)的複雜度明顯不行,我們可以用堆進行最佳化,這裡講下並查集的最佳化方法(很巧妙)。我們把連續的被佔用的區間看成一個集合(子樹),它的根結點為這個區間左邊第一個未被佔用的區間。
先排序,然後每次判斷Find(b[i])是否大於0,大於0說明左邊還有未被佔用的空間,則佔用它,然後合并(b[i], Find(b[i]) – 1)即可。同樣這裡我們規定只能左邊的子樹合并到右邊的子樹(想想為什麼~~)。
       POJ 1733 Parity game
       http://acm.pku.edu.cn/JudgeOnline/problem?id=1733
       這題同樣用類似食物鏈的思想。
首先我們先離散化,因為原來的區間太大了(10^9),我們可以根據問題數目離散成(10^4)。我們要理解,這裡的離散化並不影響最終的結果,因為區間裡1的奇偶個數與區間的大小無關(這句話有點奇怪,可以忽略...),然後每次輸入a, b,我們把b++,如果他倆在一個集合內,那麼區間[a, b]裡1的個數相當於b.relation ^ a.relation,判斷對錯即可。如果不在一個集合內,合并集合(這裡我們規定根結點小的子樹合并根結點大的,所以要根據不同情況推式子),修改子樹的根結點的狀態,子樹的其他結點狀態通過Find()函數來更新。
       hdu 3038 How Many Answers Are Wrong
       http://acm.hdu.edu.cn/showproblem.php?pid=3038
       上面那題的加強版,不需要離散化,因為區間的和與區間的大小有關(和上面的那句話對比下,同樣可以忽略之…),做法與上面那題差不多,只是式子變了,自己推推就搞定了。但這題還有個條件,就是每個點的值在[0, 100]之間,那麼如果a, b不在一個子樹內,我們就合并,但在合并之前還要判斷合并後會不會使得區間的和不合法,如果會說明該合并是非法的,那麼就不合并,同樣認為該句話是錯誤的。
       POJ 1417 True Liars(難)
       http://acm.pku.edu.cn/JudgeOnline/problem?id=1417
       並查集 + DP(或搜尋)。
       題目中告訴兩種人,一種只說真話,一種只說假話。然後告訴m條語句,問是否能判斷哪些人是只說真話的那類人。
       其實並查集部分跟食物鏈還是相似,而且種類變少了一種,更容易了。我們可以通過並查集把有關係的一些人合并到一個集合內(具體方法參見食物鏈講解)。
       現在的問題轉化為,有n個集合,每個集合都有a, b連個數字,現在要求n個集合中各跳出一個數(a或者b),使得他們之和等於n1(說真話的人數)。而這個用dp可以很好的解決,用f[i][j]表示到第i個集合和為j個的情況數,我們還用過pre[i][j]記錄當前選的是a還是b,用於後面判斷狀態。方程為f[i][j] = f[i – 1][j – a] + f[i – 1][j – b], j >= a, j >= b。如果最後f[n][n1] == 1說明是唯一的情況,輸出該情況,否則輸出 “no”(多解算no)
       注意點 : 
       1. 這題的m, n1, n2都有可能出現0,可以特殊處理,也可以一起處理。
       2. 按上面的dp寫法,f[i][j]可能會很大,因為n可以達到三位元。其實我們關心的只是f[i][j] 等於0,等於1,大於1三種情況,所以當f[i][j] > 1時,我們都讓它等於2即可。
       POJ 2912 Rochambeau(難)
       http://acm.pku.edu.cn/JudgeOnline/problem?id=2912
       Baidu Star 2006 Preliminary的題目,感覺出的很好,在並查集題目中算是較難的了。其實這題跟食物鏈完全一個磨子,同樣三類食物,同樣的互相制約關係。所以食物鏈代碼拿過來改都不需要改。但這題有個judge,他可以出任意手勢。於是我們的做法是,枚舉每個小孩為judge,判斷他為judge時在第幾句話出錯err[i](即到第幾句話能判斷該小孩不是judge)。
       1. 如果只有1個小孩是judge時全部語句都是正確的,說明該小孩是judge,那麼判斷的句子數即為其他小孩的err[i]的最大值。如果
       2. 如果每個小孩的都不是judge(即都可以找到出錯的語句),那麼就是impossible。
       3. 多於1個小孩是judge時沒有找到出錯的語句,就是Can not determine。

        ZOJ 3261 Connections in Galaxy War
        http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemId=3563
        nuaa 1087 聯通or不連通
        http://acm.nuaa.edu.cn/acmhome/problemdetail.do?&method=showdetail&id=1087
        兩題做法差不多,都是反過來的並查集題目,先對邊集排序,然後把要刪去的邊從二分在邊集中標記。然後並查集串連沒有標記的邊集,再按查詢反向做就可。第一題合并結點時按照題目要求的優先順序合并即可。
    

       這裡介紹的並查集題目,主要都是處理些集合之間的關係(這是並查集的看家本領~~),至於並查集還有個用處就在求最小產生樹的Kruskal演算法中,那個是圖論中求最小產生樹的問題(一般這個痛點不在於並查集,它只是用於求最小產生樹的一種方法),就不在這裡贅述了~~

轉載自:http://hi.baidu.com/czyuan_acm/item/13cbd3258c29e20d72863edf

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.