並查集小結
並查集大體分爲三個:普通的並查集,帶種類的並查集,擴展的並查集(主要是必須指定合併時的父子關係,或者統計一些數據,比如此集合內的元素數目。)
POJ-1182
經典的種類並查集
POJ-1308
用並查集來判斷一棵樹。。注意空樹也是樹,死人也是人。
POJ-1611
裸地水並查集
POJ-1703
種類並查集
POJ-1988
看上去似乎和種類並查集無關,但其實仔細想想,就是種類並查集。。。
只不過是種類數目無窮大,通過合併,可以確定兩個物品之間的種類差(即高度差)
POJ-2236
裸地並查集,小加一點計算幾何
POJ-2492
裸地種類並查集
POJ-2524
又是裸地並查集
POJ-1456
常規思想是貪心+堆優化,用並查集確實很奇妙。。。下面的文章中有詳細介紹。
POJ-1733
種類並查集,先要離散化一下,不影響結果。。。
HDU-3038
上一道題的擴展,也是種類並查集,種類無窮大。。。。
POJ-1417
種類並查集,然後需要揹包原理來判斷是否能唯一確定“好人”那一堆
POJ-2912
baidu的題,AC了,不過有點亂,有時間【【【再看看】】】
ZOJ-3261 NUAA-1087
逆向使用並查集就可以了。。。
POJ-1861 POJ-2560
Kruskal並查集
另外這個文章很好:
轉自:http://hi.baidu.com/czyuan%5Facm/blog/item/531c07afdc7d6fc57cd92ab1.html
繼續數據結構的複習,本次的專題是:並查集。
並查集,顧名思義,乾的就是“並”和“查”兩件事。很多與集合相關的操作都可以用並查集高效的解決。
兩個操作代碼:
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 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算法中,那個是圖論中求最小生成樹的問題(一般這個難點不在於並查集,它只是用於求最小生成樹的一種方法),就不在這裏贅述了~~
czyuan原創,轉載請註明出處。
種類並查集報告
POJ-1703 POJ-1182 POJ-2492都是這種題。
本來要先寫搜索和DP的報告的,因爲前面做的都是搜索和DP,正好昨天做了這道並查集的題目,所以就順手寫下。這道題也讓我理解了好長時間,還是很有意義的,網上也沒有很詳細的解題報告。
題目:
http://acm.pku.edu.cn/JudgeOnline/problem?id=1182
因爲我是按網上通用分類做的,所以做之前就知道用並查集做,不過這道題是並查集的深入應用,沒有其它的那麼直觀。這裏我採用的是和網上主流方法一樣的方法,這裏先貼出源碼再深入解釋下。
#include <iostream>
using namespace std;
struct point{
int parent;
int kind;
} ufind[50010];
void init(int n);
int find(int index);
void unions(int rootx, int rooty, int x, int y, int dkind);
int main()
{
int n,k,count=0,d,x,y;
scanf(“%d%d”,&n,&k);
init(n);
while(k–)
{
scanf(“%d%d%d”,&d,&x,&y);
if(x>n || y>n)
count++;
else if(d==2 && x==y)
count++;
else
{
int rx=find(x);
int ry=find(y);
if(rx!=ry)
unions(rx,ry,x,y,d-1);
else
{
if(d==1 && ufind[x].kind!=ufind[y].kind)
count++;
if(d==2 && (ufind[x].kind-ufind[y].kind+3)%3!=1)
count++;
}
}
}
printf(“%d\n”,count);
return 0;
}
void init(int n)
{
for(int i=1;i<=n;i++)
{
ufind[i].parent=i;
ufind[i].kind=0;
}
}
int find(int index)
{
int temp;
if(index==ufind[index].parent)
return index;
temp=ufind[index].parent;
ufind[index].parent=find(ufind[index].parent);
ufind[index].kind=(ufind[temp].kind+ufind[index].kind)%3;
return ufind[index].parent;
}
void unions(int rootx, int rooty, int x, int y, int dkind)
{
ufind[rooty].parent=rootx;
ufind[rooty].kind=(-dkind+(ufind[x].kind-ufind[y].kind)+3)%3;
}
1.這裏並查集(ufind)的一個同類集合存放的是根據已有正確推斷有關聯的點,這裏的關聯包括了喫,被喫,同類三個關係;
2.關係是通過kind來表示的,其值爲0,1,2,如果ufind[1].kind==ufind[2].kind,說明1,2點同類;如果
(ufind[1].kind-ufind[2].kind+3)%3==1,說明1喫2;
3.並查集是按索引部分組織起來的,即同一類的點都有共同的根結點;
4.並查集包括初始化(init),查找(find),合併(unions)操作,其中有很多關鍵點,我都在代碼中用紅色標記。下面逐一解釋這些關鍵點:
(1)ufind[i].kind=0;種類初始化爲0,這個看似很簡單,但它其實保證了並查集中每一個類的根結點的kind屬性爲0,這是後面兩個關鍵式推導的基礎;
(2)ufind[rooty].kind=(-dkind+(ufind[x].kind-ufind[y].kind)+3)%3;這句出現在合併操作裏面,這裏要解釋的是,在合併之前每個類的集合中所有父節點爲根結點的點以及根結點,它們之間的關係都是正確的,合併之後只保證了合併前原兩個集合的根結點之間的關係正確,即在新的合併後的集合中仍保證所有父節點爲根結點的點以及根結點之間的關係正確。這樣我們在做合併操作時,是通過三個關係推到出預合併的兩個根結點(rootx,rooty)之間的正確關係的:x和rootx的關係,y和rooty的系,x和y的關係。這就是這個式子的由來,其中用到了前面說過的rootx和rooty爲0的結論。
(3)ufind[index].kind=(ufind[temp].kind+ufind[index].kind)%3;這句出現在查找操作裏,作用是將待查找的點到它的根結點所經過的所有點進行兩個操作,一是把它們的父節點都設爲根結點;二是按照從離根結點最近的那個點開始到待查找的點的順序把它們與根結點的關係設置成正確值(原先有可能是錯誤的,因爲合併操作只保證了所有父節點爲根結點的點以及根結點之間的關係正確)。這樣之後這個集合中仍然保證了所有父節點爲根結點的點以及根結點之間的關係正確,並且待考察的點的父節點爲根結點。下面來解釋下爲什麼要按照從離根結點最近的那個點開始到待查找的點的順序,這也是這個式子爲什麼這麼寫的原因:假設1爲根結點,Kind爲0,其子節點爲2,kind爲k2,2的子節點爲3,kind爲k3;因爲每次合併只合並根結點,所以3在1,2合併前的根結點一定是2,即若2的kind爲0,則3和2的關係就正確了,但合併時2的kind加上了k2,保證了1與2的關係正確而並沒有更新k3(這是因爲並查集集合中無法從父節點找到子結點,所以等到查找時,要用到該點時再更新也不遲),所以此時只要將(k2+k3)%3就可以得到正確的以1爲基準的3的kind。接下來,k3的kind修正了,k4可以以k3爲基礎一樣通過此方法修正,直到要查的結點,並把它們全部掛到根結點上。
解釋就到這裏,我想理解時只要畫個圖就能容易理解些,即以index和kind爲結點做出集合的樹狀圖。這裏恰巧是3個關係,若是4個關係我想就要更新並查集單位集合的組織形式了,即要可以從根結點遍歷全集和。