并查集基础与习题
目錄
并查集
例題:
A:POJ-2236 Wireless Network
B:POJ-1611 The Suspects
C:HDU-1213 How Many Tables
D:HDU-3038 How Many Answers Are Wrong
E:POJ-1182 食物鏈
F:POJ-1456 Supermarket
G:POJ-1733 Parity game
I:POJ-2492 A Bug's Life
J:POJ-2912 Rochambeau
K:ZOJ-3261 Connections in Galaxy War
L:HDU-1272 小希的迷宮
M:POJ-1308 Is It A Tree?
先推薦大家去看個(gè)視頻并查集視頻(看完就不用看我的講解了,直接看習(xí)題)
并查集轉(zhuǎn)自(并查集詳解)
一、問題引入
原題:杭電hdu1232暢通工程?
題意:首先在地圖上給你若干個(gè)城鎮(zhèn),這些城鎮(zhèn)都可以看作點(diǎn),然后告訴你哪些對(duì)城鎮(zhèn)之間是有道路直接相連的。最后要解決的是整幅圖的連通性問題。比如隨意給你兩個(gè)點(diǎn),讓你判斷它們是否連通,或者問你整幅圖一共有幾個(gè)連通分支,也就是被分成了幾個(gè)互相獨(dú)立的塊。像暢通工程這題,問還需要修幾條路,實(shí)質(zhì)就是求有幾個(gè)連通分支。如果是1個(gè)連通分支,說明整幅圖上的點(diǎn)都連起來了,不用再修路了;如果是2個(gè)連通分支,則只要再修1條路,從兩個(gè)分支中各選一個(gè)點(diǎn),把它們連起來,那么所有的點(diǎn)都是連起來的了;如果是3個(gè)連通分支,則只要再修兩條路……
說明:輸入4 2 ? 1 3 ? 4 3。即一共有4個(gè)點(diǎn),2條路。下面兩行告訴你,1、3之間有條路,4、3之間有條路。那么整幅圖就被分成了1-3-4和2兩部分。只要再加一條路,把2和其他任意一個(gè)點(diǎn)連起來,暢通工程就實(shí)現(xiàn)了,那么這個(gè)這組數(shù)據(jù)的輸出結(jié)果就是1。好了,現(xiàn)在編程實(shí)現(xiàn)這個(gè)功能吧,城鎮(zhèn)有幾百個(gè),路有不知道多少條,而且可能有回路。 這可如何是好? 我以前也不會(huì)呀,自從用了并查集之后,嗨,效果還真好!
?
二、故事描述
并查集由一個(gè)整數(shù)型的數(shù)組和兩個(gè)函數(shù)構(gòu)成。數(shù)組pre[]記錄了每個(gè)點(diǎn)的前導(dǎo)點(diǎn)是什么,函數(shù)find是查找,函數(shù)join是合并。
話說江湖上散落著各式各樣的大俠,有上千個(gè)之多。他們沒有什么正當(dāng)職業(yè),整天背著劍在外面走來走去,碰到和自己不是一路人的,就免不了要打一架。但大俠們有一個(gè)優(yōu)點(diǎn)就是講義氣,絕對(duì)不打自己的朋友。而且他們信奉“朋友的朋友就是我的朋友”,只要是能通過朋友關(guān)系串聯(lián)起來的,不管拐了多少個(gè)彎,都認(rèn)為是自己人。這樣一來,江湖上就形成了一個(gè)一個(gè)的群落,通過兩兩之間的朋友關(guān)系串聯(lián)起來。而不在同一個(gè)群落的人,無論如何都無法通過朋友關(guān)系連起來,于是就可以放心往死了打。但是兩個(gè)原本互不相識(shí)的人,如何判斷是否屬于一個(gè)朋友圈呢? 我們可以在每個(gè)朋友圈內(nèi)推舉出一個(gè)比較有名望的人,作為該圈子的代表人物,這樣,每個(gè)圈子就可以這樣命名“齊達(dá)內(nèi)朋友之隊(duì)”“羅納爾多朋友之隊(duì)”……兩人只要互相對(duì)一下自己的隊(duì)長是不是同一個(gè)人,就可以確定敵友關(guān)系了。 但是還有問題啊,大俠們只知道自己直接的朋友是誰,很多人壓根就不認(rèn)識(shí)隊(duì)長,要判斷自己的隊(duì)長是誰,只能漫無目的的通過朋友的朋友關(guān)系問下去:“你是不是隊(duì)長?你是不是隊(duì)長?” 這樣一來,隊(duì)長面子上掛不住了,而且效率太低,還有可能陷入無限循環(huán)中。于是隊(duì)長下令,重新組隊(duì)。隊(duì)內(nèi)所有人實(shí)行分等級(jí)制度,形成樹狀結(jié)構(gòu),我隊(duì)長就是根節(jié)點(diǎn),下面分別是二級(jí)隊(duì)員、三級(jí)隊(duì)員。每個(gè)人只要記住自己的上級(jí)是誰就行了。遇到判斷敵友的時(shí)候,只要一層層向上問,直到最高層,就可以在短時(shí)間內(nèi)確定隊(duì)長是誰了。由于我們關(guān)心的只是兩個(gè)人之間是否連通,至于他們是如何連通的,以及每個(gè)圈子內(nèi)部的結(jié)構(gòu)是怎樣的,甚至隊(duì)長是誰,并不重要。所以我們可以放任隊(duì)長隨意重新組隊(duì),只要不搞錯(cuò)敵友關(guān)系就好了。于是,門派產(chǎn)生了。
下面我們來看并查集的實(shí)現(xiàn)。?int pre[1000]; ?這個(gè)數(shù)組,記錄了每個(gè)大俠的上級(jí)是誰。大俠們從1或者0開始編號(hào)(依據(jù)題意而定),pre[15]=3就表示15號(hào)大俠的上級(jí)是3號(hào)大俠。如果一個(gè)人的上級(jí)就是他自己,那說明他就是掌門人了,查找到此為止。也有孤家寡人自成一派的,比如歐陽鋒,那么他的上級(jí)就是他自己。每個(gè)人都只認(rèn)自己的上級(jí)。比如胡青牛同學(xué)只知道自己的上級(jí)是楊左使。張無忌是誰?不認(rèn)識(shí)!要想知道自己的掌門是誰,只能一級(jí)級(jí)查上去。 find這個(gè)函數(shù)就是找掌門用的,意義再清楚不過了(路徑壓縮算法先不論,后面再說)。
int find(int x) //查找x的掌門 {int r=x; //委托 r 去找掌門while(pre[r] != r) //如果r的上級(jí)不是r自己(也就是說找到的大俠他不是掌門 = =)r = pre[r] ; // r 接著找他的上級(jí),直到找到掌門為止。return r ; //掌門駕到~~~ }再來看看join函數(shù),就是在兩個(gè)點(diǎn)之間連一條線,這樣一來,原先它們所在的兩個(gè)板塊的所有點(diǎn)就都可以互通了。這在圖上很好辦,畫條線就行了。但我們現(xiàn)在是用并查集來描述武林中的狀況的,一共只有一個(gè)pre[]數(shù)組,該如何實(shí)現(xiàn)呢? 還是舉江湖的例子,假設(shè)現(xiàn)在武林中的形勢(shì)如圖所示。虛竹小和尚與周芷若MM是我非常喜歡的兩個(gè)人物,他們的終極boss分別是玄慈方丈和滅絕師太,那明顯就是兩個(gè)陣營了。我不希望他們互相打架,就對(duì)他倆說:“你們兩位拉拉勾,做好朋友吧?!彼麄兛丛谖业拿孀由?#xff0c;同意了。這一同意可非同小可,整個(gè)少林和峨眉派的人就不能打架了。這么重大的變化,可如何實(shí)現(xiàn)呀,要改動(dòng)多少地方?其實(shí)非常簡單,我對(duì)玄慈方丈說:“大師,麻煩你把你的上級(jí)改為滅絕師太吧。這樣一來,兩派原先的所有人員的終極boss都是師太,那還打個(gè)球啊!反正我們關(guān)心的只是連通性,門派內(nèi)部的結(jié)構(gòu)不要緊的?!毙纫宦牽隙ɑ鸫罅?#xff1a;“我靠,憑什么是我變成她手下呀,怎么不反過來?我抗議!”抗議無效,上天安排的,最大。反正誰加入誰效果是一樣的,我就隨手指定了一個(gè)。這段函數(shù)的意思很明白了吧?
void join(int x,int y) //我想讓虛竹和周芷若做朋友 {int fx=find(x), fy=find(y); //虛竹的老大是玄慈,芷若MM的老大是滅絕if(fx != fy) //玄慈和滅絕顯然不是同一個(gè)人pre[fx]=fy; //方丈只好委委屈屈地當(dāng)了師太的手下啦 }再來看看路徑壓縮算法。建立門派的過程是用join函數(shù)兩個(gè)人兩個(gè)人地連接起來的,誰當(dāng)誰的手下完全隨機(jī)。最后的樹狀結(jié)構(gòu)會(huì)變成什么樣,我也完全無法預(yù)計(jì),一字長蛇陣也有可能。這樣查找的效率就會(huì)比較低下。最理想的情況就是所有人的直接上級(jí)都是掌門,一共就兩級(jí)結(jié)構(gòu),只要找一次就找到掌門了。哪怕不能完全做到,也最好盡量接近。這樣就產(chǎn)生了路徑壓縮算法。 設(shè)想這樣一個(gè)場景:兩個(gè)互不相識(shí)的大俠碰面了,想知道能不能揍。 于是趕緊打電話問自己的上級(jí):“你是不是掌門?” 上級(jí)說:“我不是呀,我的上級(jí)是誰誰誰,你問問他看看?!?一路問下去,原來兩人的最終boss都是東廠曹公公。 “哎呀呀,原來是記己人,西禮西禮,在下三營六組白面葫蘆娃!” “幸會(huì)幸會(huì),在下九營十八組仙子狗尾巴花!” 兩人高高興興地手拉手喝酒去了。 “等等等等,兩位同學(xué)請(qǐng)留步,還有事情沒完成呢!”我叫住他倆。 “哦,對(duì)了,還要做路徑壓縮?!眱扇诵盐?。 白面葫蘆娃打電話給他的上級(jí)六組長:“組長啊,我查過了,其習(xí)偶們的掌門是曹公公。不如偶們一起直接拜在曹公公手下吧,省得級(jí)別太低,以后查找掌門麻環(huán)?!?“唔,有道理?!?白面葫蘆娃接著打電話給剛才拜訪過的三營長……仙子狗尾巴花也做了同樣的事情。 這樣,查詢中所有涉及到的人物都聚集在曹公公的直接領(lǐng)導(dǎo)下。每次查詢都做了優(yōu)化處理,所以整個(gè)門派樹的層數(shù)都會(huì)維持在比較低的水平上。路徑壓縮的代碼,看得懂很好,看不懂也沒關(guān)系,直接抄上用就行了??傊鶎?shí)現(xiàn)的功能就是這么個(gè)意思。
?
三、算法描述
關(guān)鍵特征:
①用集合中的某個(gè)元素來代表這個(gè)集合,該元素稱為集合的代表元;
②一個(gè)集合內(nèi)的所有元素組織成以代表元為根的樹形結(jié)構(gòu);
③對(duì)于每一個(gè)元素 pre[x]指向x在樹形結(jié)構(gòu)上的父親節(jié)點(diǎn)。如果x是根節(jié)點(diǎn),則令pre[x] = x;
④對(duì)于查找操作,假設(shè)需要確定x所在的的集合,也就是確定集合的代表元??梢匝刂鴓re[x]不斷在樹形結(jié)構(gòu)中向上移動(dòng),直到到達(dá)根節(jié)點(diǎn)。
判斷兩個(gè)元素是否屬于同一集合,只需要看他們的代表元是否相同即可。
路徑壓縮: ?
為了加快查找速度,查找時(shí)將x到根節(jié)點(diǎn)路徑上的所有點(diǎn)的pre(上級(jí))設(shè)為根節(jié)點(diǎn),該優(yōu)化方法稱為壓縮路徑。使用該優(yōu)化后,平均復(fù)雜度可視為Ackerman函數(shù)的反函數(shù),實(shí)際應(yīng)用中可粗略認(rèn)為其是一個(gè)常數(shù)。
用途:
1、維護(hù)無向圖的連通性。支持判斷兩個(gè)點(diǎn)是否在同一連通塊內(nèi),和判斷增加一條邊是否會(huì)產(chǎn)生環(huán)。
2、用在求解最小生成樹的Kruskal算法里。
一般來說,一個(gè)并查集對(duì)應(yīng)三個(gè)操作:初始化+查找根結(jié)點(diǎn)函數(shù)+合并集合函數(shù)
【初始化】
包括對(duì)所有單個(gè)的數(shù)據(jù)建立一個(gè)單獨(dú)的集合(即根據(jù)題目的意思自己建立的最多可能有的集合,為下面的合并查找操作提供操作對(duì)象)。
在每一個(gè)單個(gè)的集合里面,有三個(gè)東西。
①集合所代表的數(shù)據(jù)(這個(gè)初始值根據(jù)需要自己定義,不固定) ;
②這個(gè)集合的層次通常用rank表示(一般來說,初始化的工作之一就是將每一個(gè)集合里的rank置為1);
③這個(gè)集合的類別pre(其實(shí)就是一個(gè)指針,用來指示這個(gè)集合屬于那一類,合并過后的集合,他們的pre指向的最終值一定是相同的) (有的簡單題里面集合的數(shù)據(jù)就是這個(gè)集合的標(biāo)號(hào),也就是說只包含2和3,1省略了)。
初始化的時(shí)候,每一個(gè)集合的pre都是這個(gè)集合自己的標(biāo)號(hào)。沒有跟它同類的集合,那么這個(gè)集合的源頭只能是自己了。
最簡單的集合就只含有這三個(gè)東西了,當(dāng)然,復(fù)雜的集合就是把3指針這一項(xiàng)添加內(nèi)容,如PKU食物鏈那題,我們還可以添加enemy指針,表示這個(gè)物種集合的天敵集合;food指針,表示這個(gè)物種集合的食物集合。隨著指針的增加,并查集操作起來也變得復(fù)雜,題目也就顯得更難了。
數(shù)組表示法
設(shè)置很多相同大小的數(shù)組,如:
int pre[max]; //集合index的類別,或者用parent表示 int rank[max]; //集合index的層次,通常初始化為0 int data[max]; //集合index的數(shù)據(jù)類型 //初始化集合 void Make_pre(int i) { pre[i]=i; //一個(gè)集合的pre都是這個(gè)集合自己的標(biāo)號(hào)。沒有跟它同類的集合,那么這個(gè)集合的源頭只能是自己了。 rank[i]=0; }【查找函數(shù)】
就是找到pre指針的源頭,可以把函數(shù)命名為find_pre,如果集合的pre等于集合的編號(hào)(即還沒有被合并或者沒有同類),那么自然返回自身編號(hào)。 如果不同(即經(jīng)過合并操作后指針指向了源頭(合并后選出的rank高的集合))那么就可以調(diào)用遞歸函數(shù),如下面的代碼:
//查找集合i(一個(gè)元素是一個(gè)集合)的源頭(遞歸實(shí)現(xiàn)) int Find_pre(int i) {//如果集合i的父親是自己,說明自己就是源頭,返回自己的標(biāo)號(hào)if(pre[i]==i)return pre[i];//否則查找集合i的父親的源頭return Find_pre(pre[i]); }【合并集合函數(shù)】
這就是所謂并查集的并了。至于怎么知道兩個(gè)集合是可以合并的,那就是題目的條件了。先看代碼:
void Union(int i,int j) {i=Find_pre(i);j=Find_pre(j);if(i==j) return ;if(rank[i]>rank[j]) pre[j]=i;else{if(rank[i]==rank[j]) rank[j]++; pre[i]=j;} }四、代碼實(shí)現(xiàn)
#define N 105 int pre[N]; //每個(gè)結(jié)點(diǎn) int rank[N]; //樹的高度 //初始化 int init(int n) //對(duì)n個(gè)結(jié)點(diǎn)初始化 {for(int i = 0; i < n; i++){pre[i] = i; //每個(gè)結(jié)點(diǎn)的上級(jí)都是自己rank[i] = 1; //每個(gè)結(jié)點(diǎn)構(gòu)成的樹的高度為1} }int find_pre(int x) //查找結(jié)點(diǎn)x的根結(jié)點(diǎn) {if(pre[x] == x){ //遞歸出口:x的上級(jí)為x本身,即x為根結(jié)點(diǎn)return x; }return find_pre(pre[x]); //遞歸查找 }//改進(jìn)查找算法:完成路徑壓縮,將x的上級(jí)直接變?yōu)楦Y(jié)點(diǎn),那么樹的高度就會(huì)大大降低 int find_pre(int x) //查找結(jié)點(diǎn)x的根結(jié)點(diǎn) {if(pre[x] == x){ //遞歸出口:x的上級(jí)為x本身,即x為根結(jié)點(diǎn)return x; }return pre[x] = find_pre(pre[x]); //遞歸查找 此代碼相當(dāng)于 先找到根結(jié)點(diǎn)rootx,然后pre[x]=rootx }bool is_same(int x, int y) //判斷兩個(gè)結(jié)點(diǎn)是否連通 {return find_pre(x) == find_pre(y); //判斷兩個(gè)結(jié)點(diǎn)的根結(jié)點(diǎn)(亦稱代表元)是否相同 }void unite(int x,int y) {int rootx, rooty;rootx = find_pre(x);rooty = find_pre(y);if(rootx == rooty){return ;}if(rank(rootx) > rank(rooty)){pre[rooty] = rootx; //令y的根結(jié)點(diǎn)的上級(jí)為rootx}else{if(rank(rootx) == rank(rooty)){rank(rooty)++;}pre[rootx] = rooty;} }例題:
A:POJ-2236 Wireless Network:題目大意是說有一些電腦,編號(hào)為1到N,現(xiàn)在這些電腦壞了,無法相互連通,我們需要維修,輸入首先輸入N和d,N表示有多少臺(tái)電腦,d表示兩臺(tái)已維修好的電腦若它們之間的距離小于等于d,則兩臺(tái)電腦可以互通。接下來輸入N行,每行輸入a,b兩個(gè)數(shù),N行中的第i行表示編號(hào)為i的電腦的坐標(biāo)(用來求兩臺(tái)電腦的距離),在接下來的輸入各種操作,O a表示編號(hào)為a的電腦被維修好了,S a b則表示詢問編號(hào)為a和b的電腦能不能互通,若能則輸出SUCCESS,若不能則輸出FAIL。簡單的并查集問題。AC代碼:
#include <iostream> #include <cmath> using namespace std;struct coordinate{int x,y; }coo[1010]; int set1[1010] = { 0 }, map1[1010][1010] = { 0 }; int n,d,x,y,i,j; string s;int FindSet(int x){if (set1[x]!=x)set1[x]=FindSet(set1[x]);return set1[x]; } void Union(int x, int y){y = FindSet(y);set1[y] = x;return; } int main() {cin>>n>>d;for (i=1;i<=n;++i)cin>>coo[i].x>>coo[i].y;for (i=1;i<n;++i)for (j=i+1;j<=n;++j)if (sqrt((coo[i].x-coo[j].x)*(coo[i].x-coo[j].x) + (coo[i].y-coo[j].y)*(coo[i].y-coo[j].y))<=d){map1[i][j] = 1;map1[j][i] = 1;}while (cin>>s)if (s == "O"){cin>>x;set1[x]=x;for (i=1;i<=n;++i)if((map1[x][i]==1)&&(set1[i])) //set1[i]不為0即已經(jīng)修好Union(x,i);}else{cin>>x>>y;if (FindSet(x)==FindSet(y))cout<<"SUCCESS\n";elsecout<<"FAIL\n";}return 0; }B:POJ-1611 The Suspects:該題的意思是,給你幾個(gè)圈子,把與0直接相關(guān)和間接相關(guān)的圈子都找出來,輸出這些圈子有多少個(gè)人。簡單的模板題,看代碼即可:
#include<stdio.h> #include<string.h> #include<stdlib.h> #define maxn 30001 int set1[maxn]; int sum[maxn]; int findset(int x) {if(set1[x]!=x)set1[x]=findset(set1[x]);return set1[x]; } int main() {int x,y,m,n,w,i;while(scanf("%d%d",&m,&n)&&(m!=0||n!=0)){memset(sum,0,sizeof(sum));for(i=0;i<m;i++){set1[i]=i;sum[i]=1;}for(i=1;i<=n;i++){scanf("%d",&w);if(w>0){scanf("%d",&x);w--;}while(w--){scanf("%d",&y);x=findset(x);y=findset(y);if(x!=y){set1[x]=y;sum[y]+=sum[x];}}}/*for(i=0;i<m;i++)if(findset(i)==findset(0))k++;printf("%d\n",k);*/printf("%d\n",sum[findset(0)]);}return 0; }C:HDU-1213 How Many Tables:題目大致意思是,有n個(gè)客人,有m組兩個(gè)客人之間的關(guān)系,代表兩個(gè)客人之間相互認(rèn)識(shí),然后要給客人們安排桌子,客人們與其他人坐同一張桌子的條件是這張桌子上至少有一個(gè)人是他認(rèn)識(shí)的,問你最少要安排多少張桌子。比如第一組樣例,1和2認(rèn)識(shí),2又和3認(rèn)識(shí),那么1,2,3就可以坐同一張桌子,而4,5和1,2,3互相不認(rèn)識(shí),所以他們不能坐同一張桌子,而4,5相互認(rèn)識(shí),所以4和5可以坐同一張桌子,所以總共需要兩張桌子。又是一道模板題,代碼:
#include<bits/stdc++.h> using namespace std; #define maxn 30001 int set1[maxn]; int sum[maxn]; int findset(int x) {if(set1[x]!=x)set1[x]=findset(set1[x]);return set1[x]; } int main() {int t,x,y,m,n;cin>>t;while(t--){int ans=0;cin>>n>>m;for(int i=0;i<=n;i++)set1[i]=i;for(int i=1;i<=m;i++){cin>>x>>y;x=findset(x);y=findset(y);if(x!=y){set1[x]=y;}}for(int i=1;i<=n;i++)if(set1[i]==i)ans++;cout<<ans<<endl;}return 0; }D:HDU-3038 How Many Answers Are Wrong:給出一個(gè)數(shù)組的區(qū)間和,如果后面與前面矛盾認(rèn)為是假話。帶權(quán)并查集判錯(cuò)問題,很經(jīng)典的問題,后面好多題和本題也是一個(gè)思路??梢韵瓤催@個(gè)題解給出的思路和推出的公式,后面很多題都會(huì)用到,代碼:
#include<stdio.h> #include<stdlib.h> #include<string.h> #define maxn 200000+10 int pre[maxn],rank[maxn];//rank[]表示到根節(jié)點(diǎn)的距離 int n,m,sum;void init() {for(int i=0; i<=n; i++){pre[i]=i;rank[i]=0;}sum=0; }int Find(int x) {int temp=pre[x];if(x==pre[x])return x;pre[x]=Find(temp);rank[x]=rank[x]+rank[temp];return pre[x]; }void join(int x,int y,int k) {int fx=Find(x),fy=Find(y);if(fx!=fy){pre[fx]=fy;rank[fx]=rank[y]+k-rank[x];}else{if(rank[x]-rank[y]!=k)sum++;} }int main() {while(~scanf("%d%d",&n,&m)){init();int x,y,z,i;for(i=1; i<=m; i++){scanf("%d%d%d",&x,&y,&z);join(x-1,y,z);}printf("%d\n",sum);}return 0; }E:POJ-1182 食物鏈:就是一個(gè)思維題,具體看代碼:
/*一定要仔細(xì)讀題讀題讀題啊,A吃B B吃C C吃A這是規(guī)則!!!一定要遵循的,A吃B,B吃C,C一定吃A。唯一需要注意的地方就是這個(gè)規(guī)則,其他的按照并查集思想很容易解決。*/ #include <iostream> #include <cstdio> #include <string> #include <algorithm> #include <queue> #include <map> #include <cstring> #include <cmath>const int maxn=50005;using namespace std;int n,k,father[3*maxn]; //數(shù)組開大點(diǎn)來存儲(chǔ)同類,吃,被吃的關(guān)系。//father[a]存同類,father[a+n]存吃哪種種類,father[a+2*n]存被哪種種類吃int Find(int x) {return x == father[x] ? x : Find(father[x]); }int main() {cin >> n >> k;int d,x,y,num=0;for(int i=1;i<=3*n;i++){father[i]=i;}for(int i=1;i<=k;i++){scanf("%d%d%d",&d,&x,&y);if(x>n || y>n){num+=1;}else if(d == 1){if(Find(x)==Find(y+n) || Find(x)==Find(y+2*n))num+=1;else{ //所有的吃與被吃,x與y要統(tǒng)一father[Find(x+n)]=Find(y+n);father[Find(x)]=Find(y);father[Find(x+2*n)]=Find(y+2*n);}}else{if(Find(x+n)==Find(y+n) || Find(x+n)==Find(y+2*n) || Find(x)==Find(y) || Find(x)==Find(y+n) || Find(x+2*n)==Find(y) || Find(x+2*n)==Find(y+2*n))num+=1;else{ //遵循A吃B,B吃C,C吃A的規(guī)則進(jìn)行修改x。father[Find(x+n)]=Find(y);father[Find(x+2*n)]=Find(y+n);father[Find(x)]=Find(y+2*n);}}}printf("%d\n",num);return 0; }F:POJ-1456 Supermarket:超市里有N件商品,每個(gè)商品都有利潤pi和過期時(shí)間di,每天只能賣一件商品,過期商品(即當(dāng)天di<=0)不能再賣。求合理安排每天賣的商品的情況下,可以得到的最大收益是多少。這個(gè)題可以用貪心,并查集,優(yōu)先隊(duì)列做,具體看這里,只給出并查集代碼,思路為用并查集將時(shí)間加入集合中,先令pre[dx]=dx,如果這一天要被占用就將其pre[dx]改為pre[dx]=dx-1,表示日期和dx相同的物品最晚只能從dx-1天售賣。代碼:
#include<stdio.h> #include<string.h> #include<algorithm> using namespace std;#define maxn 10000+10 struct node {int px,dx;friend bool operator<(node a,node b){return a.px>b.px;} }q[maxn]; int pre[maxn];void init(int n) {for(int i=0;i<=n;i++)pre[i]=i; }int Find(int x) {if(x!=pre[x])pre[x]=Find(pre[x]);return pre[x]; }int main() {int n;while(~scanf("%d",&n)){int maxx=0,i;for(i=0;i<n;i++){scanf("%d%d",&q[i].px,&q[i].dx);if(q[i].dx>maxx)maxx=q[i].dx;}init(maxx);sort(q,q+n);int sum=0;for(i=0;i<n;i++){int k=Find(q[i].dx);if(k>0){sum+=q[i].px;pre[k]=k-1;}}printf("%d\n",sum);}return 0; }G:POJ-1733 Parity game:給了n個(gè)描述,從l到r區(qū)間內(nèi)數(shù)的和是奇數(shù)還是偶數(shù),給出第一個(gè)矛盾是哪句話。如果沒有矛盾,就是n;思路和D題沒有太大區(qū)別,就是本題中的區(qū)間范圍很大,但是輸入的點(diǎn)卻最多只有5000*2個(gè),所以可以從這里入手,建立映射關(guān)系比如建立一個(gè)f(x)的映射關(guān)系,把每一個(gè)輸出的點(diǎn)都存入f(x)函數(shù)中,所以f(x)函數(shù)最多只有5000*2個(gè)數(shù),而后我們查詢的時(shí)候只需要借用他們映射的位置,通過其位置來實(shí)現(xiàn)對(duì)數(shù)據(jù)的處理,也就是說我們所用的只是這5000*2個(gè)位置與數(shù)據(jù)的關(guān)系,這樣就實(shí)現(xiàn)了離散化(暫時(shí)我的離散化就是這樣的),代碼:
#include<stdio.h> #include<stdlib.h> #include<string.h> #include<algorithm> using namespace std;#define maxn 10000+10 struct node {int u,v,w; } a[maxn]; int pre[maxn],rank[maxn]; int f[maxn];void init(int n) {for(int i=0; i<n; i++){pre[i]=i;rank[i]=0;} }int Find(int x) {int temp=pre[x];if(x==pre[x])return x;pre[x]=Find(temp);rank[x]=(rank[x]+rank[temp])%2;return pre[x]; }int main() {int n,m;while(~scanf("%d%d",&n,&m)){int i,cnt=0;char s[10];for(i=1; i<=m; i++){scanf("%d%d %s",&a[i].u,&a[i].v,s);a[i].u--;if(!strcmp(s,"even"))a[i].w=0;elsea[i].w=1;f[cnt++]=a[i].u;f[cnt++]=a[i].v;}sort(f,f+cnt);n=unique(f,f+cnt)-f;init(n);int sum=0;for(i=1; i<=m; i++){int x=lower_bound(f,f+n,a[i].u)-f;//查詢其位置int y=lower_bound(f,f+n,a[i].v)-f;//int fx=Find(x),fy=Find(y);if(fx!=fy){pre[fx]=fy;rank[fx]=(rank[y]+a[i].w-rank[x])%2;//通過其位置來建立關(guān)系}else{if((abs(rank[x]-rank[y])%2)!=a[i].w)break;}sum++;}printf("%d\n",sum);}return 0; }H:POJ-1984 Navigation Nightmare:題意:給你n個(gè)點(diǎn)和m操作,每次操作都有a b d op ,表示a點(diǎn)在b點(diǎn)的op方向(有東南西北)距離為d, 那么之后會(huì)有K次操作,a b c ,表示在第c次操作之前a和b之間的距離。思路:首先他說的是第c次操作之前,也就是我們沒有辦法直接把并查集全部的建立好,所以首先我們要把操作存起來, 之后就是如何運(yùn)用并查集了,首先有四個(gè)方向,東南西北,我們可以建立兩個(gè)并查集一個(gè)是東西方向一個(gè)是南北方向, 之后就是規(guī)定東西和南北方向的正方向,比如我們規(guī)定東是正方向那么a在b的南方d米其實(shí)就是a在b的北方-d方向, 我們維護(hù)兩個(gè)Rank數(shù)組就好了,還有一些具體的細(xì)節(jié)看代碼:
#include <stdio.h> #include <string.h> #include <algorithm> #include <iostream> using namespace std; const int maxn = 40000+10; int p[maxn],D[maxn],B[maxn];struct node {int x,y,d;char op[5]; }edg[maxn];void init() {for(int i = 0 ; i < maxn ; i ++){p[i] = i ;D[i] = 0 ;B[i] = 0 ;edg[i].x = 0 , edg[i].y = 0 ,edg[i].d = 0 ;} } int getf(int x) {if(x == p[x])return p[x];int t = p[x];p[x] = getf(p[x]);D[x] += D[t];B[x] += B[t];return p[x]; } // 東 east 南 south 西 west 北 north void merge(int x,int y,int d,char opr[]) {int dx = getf(x);int dy = getf(y);if(dx != dy){p[dy] = dx;if(opr[0] == 'E'||opr[0] == 'W'){//這里雖然是在東西方向,但是南北方向的權(quán)值數(shù)組也是要更新的因?yàn)閍在b的正西方向d米//其實(shí)是a在b東西方向,向西移動(dòng)d米加上a在b南北方向上移動(dòng)0米if(opr[0] == 'E')D[dy] = D[x] - D[y] + d;//如果和正方向相同就是加上這個(gè)距離if(opr[0] == 'W')D[dy] = D[x] - D[y] - d;// 減去這個(gè)距離B[dy] = B[x] - B[y];//什么都不加}else if(opr[0] == 'N'||opr[0] == 'S'){if(opr[0] == 'N') B[dy] = B[x] - B[y] + d;if(opr[0] == 'S') B[dy] = B[x] - B[y] - d;D[dy] = D[x] - D[y];}} } int main() {int n,m;while(scanf("%d%d",&n,&m)!=EOF){init();for(int i = 1 ; i <= m ; i++)cin>>edg[i].x>>edg[i].y>>edg[i].d>>edg[i].op;int t,a,b,cnt = 1,c;int ans;scanf("%d",&t);for(int i = 0 ; i < t ; i ++){//這里他輸入的c其實(shí)就是按照從小到大的方式輸入的所以我們可以邊輸入邊建立scanf("%d%d%d",&a,&b,&c);while(cnt<=c){//在c之前的我們都給他建立了merge(edg[cnt].x,edg[cnt].y,edg[cnt].d,edg[cnt].op);cnt++;}int da = getf(a);int db = getf(b);if(da != db) ans = -1;else{ans = abs(D[a] - D[b]) + abs(B[a] - B[b]);}printf("%d\n",ans);}} }I:POJ - 2492 A Bug's Life:并查集問題,輸入互相交配的昆蟲,找是否存在同性戀的昆蟲,在這里會(huì)將昆蟲看作節(jié)點(diǎn)。把同性的放在一個(gè)集合內(nèi),若檢測到有異性存在同一集合內(nèi)則有同性戀,AC代碼:
#include <cstdio> #include <queue> #include <cstring> #include <algorithm> using namespace std;const int MAXN = 2010; int set[MAXN<<1];int find(int p) {if(set[p] < 0) return p;return set[p] = find(set[p]); }void join(int p, int q) {p = find(p); q = find(q);if(p != q) set[p] = q; }int main() {int t, m, n, w = 1;scanf("%d", &t);while(t--){memset(set, -1, sizeof(set));scanf("%d%d" , &n, &m);bool flag = false;//沒有矛盾情況while(m--){int a, b;scanf("%d%d", &a, &b);join(a, b+n);join(b, a+n);if(find(a)==find(a+n) || find(b)==find(b+n))flag = true;}if(1 != w) printf("\n");printf("Scenario #%d:\n", w++);printf("%s\n", flag ? "Suspicious bugs found!" : "No suspicious bugs found!");}return 0; }J:POJ-2912 Rochambeau:有幾個(gè)小孩在做石頭剪刀布游戲,他們當(dāng)中有一個(gè)裁判(裁判可以隨機(jī)出,其他人只能出一種),下面給出幾組剛開始他們的勝負(fù)情況,讓你判斷誰是裁判,并輸出最少的步數(shù)。思路:用種類并查集存儲(chǔ)各個(gè)人之間的關(guān)系,然后從0編號(hào)開始假設(shè)其為裁判,若其中有一步與前面沖突,則代表他不是裁判,依次類推,這道題關(guān)鍵是每次判斷一個(gè)人是不是裁判時(shí),需要初始化pre,與v數(shù)組。代碼:
#include <iostream> #include<stdio.h> #include<algorithm> #include<string.h> #define MAX 2010 using namespace std; int v[MAX],pre[MAX]; int n,m; char cc; int a,b; struct node {int u,v,w; } k[MAX]; void init() {for(int i=0; i<=n; i++){pre[i]=i;v[i]=0;} } int fa(int root) {if(root==pre[root])return root;int tep=fa(pre[root]);v[root]=(v[root]+v[pre[root]]+3)%3;pre[root]=tep;return tep; } int judge(int aa,int bb,int c) {int root1=fa(aa);int root2=fa(bb);if(root1==root2){if((v[aa]-v[bb]+3)%3==c)return 1;elsereturn 0;}else{pre[root1]=root2;v[root1]=(v[bb]+c-v[aa]+3)%3;return 1;} } int main() {while(~scanf("%d%d",&n,&m)){for(int i=1; i<=m; i++){scanf("%d%c%d",&a,&cc,&b);k[i].u=a;k[i].v=b;if(cc=='=')k[i].w=0;else if(cc=='>')k[i].w=1;elsek[i].w=2;}int kk,ans=0;int dis[MAX];memset(dis,0,sizeof(dis));for(int i=0; i<n; i++){init();int flag=0;for(int j=1; j<=m; j++){if(k[j].u==i||k[j].v==i)continue;else{if(!judge(k[j].u,k[j].v,k[j].w)){dis[i]=j;flag=1;break;}}}if(!flag){ans++;kk=i;}}if(ans==0)printf("Impossible\n");else if(ans>=2)printf("Can not determine\n");else{int maxx=-1;for(int i=0; i<n; i++)if(dis[i]>maxx)maxx=dis[i];printf("Player %d can be determined to be the judge after %d lines\n",kk,maxx);}}return 0; }K:ZOJ-3261 Connections in Galaxy War:n個(gè)星球組成一個(gè)連接的網(wǎng)絡(luò),把星球看成點(diǎn),通訊連線看成邊相連,接下來有m個(gè)操作,有查詢操作,添加邊的操作和銷毀邊的操作,同時(shí)每個(gè)星球有一個(gè)權(quán)值,對(duì)一個(gè)點(diǎn)的查詢操作返回的是與它間接或直接相連的星球取權(quán)值最大者的序號(hào)(權(quán)值要比這個(gè)點(diǎn)的權(quán)值大)。傳統(tǒng)的做法是先把輸入的點(diǎn)加入并查集建立關(guān)系,然后開始判斷詢問,遇到destroy就將那兩個(gè)點(diǎn)之間的關(guān)系斷開,但是很明顯,這樣很難做到將其關(guān)系斷開 因此,我們可以逆向想一下了,我們可以先把所有的輸入數(shù)據(jù)都存起來,然后先把未destroy的點(diǎn)與點(diǎn)之間建立關(guān)系,這樣我們就可以從最后一個(gè)詢問開始判斷并記錄下結(jié)果,當(dāng)遇到destroy時(shí)再把這兩個(gè)點(diǎn)建立關(guān)系,這樣destroy前面的詢問就可以正確地判斷了。代碼:
#include<iostream> #include<algorithm> #include<map> #include<stdio.h> #include<string.h> #define N 10005 #define M 20005 #define Q 50005 using namespace std; int set1[N]; int ans[Q]; //星球 struct node{int id;int power; }star[N]; //隧道 struct link{int x;int y; }tunnel[M]; //查詢 struct ask{int x;int y;char s[10]; }query[Q]; //記錄邊 map<int, bool> mp; //查找根節(jié)點(diǎn) int find1(int x){return set1[x]==x?x:set1[x]=find1(set1[x]); // while(set[x]!=x) // x=set[x]; // return x; } void merge1(int a,int b){int ax=find1(a);int by=find1(b);if(ax!=by){//能量大的做根節(jié)點(diǎn)if(star[ax].power>star[by].power)set1[by]=ax;else if(star[ax].power<star[by].power)set1[ax]=by;//在能量相同下,編號(hào)小的做跟節(jié)點(diǎn)else{if(star[ax].id>star[by].id)set1[ax]=by;elseset1[by]=ax;}} } int main(){int n,m,t;int flag=1;while(cin>>n){mp.clear();for(int i=0;i<n;i++){scanf("%d",&star[i].power);star[i].id=i;set1[i]=i;}scanf("%d",&m);for(int i=0;i<m;i++){scanf("%d%d",&tunnel[i].x,&tunnel[i].y);//交換邊更有助于判斷if(tunnel[i].x>tunnel[i].y)swap(tunnel[i].x,tunnel[i].y);標(biāo)記沒被毀掉的邊mp[tunnel[i].x*10000+tunnel[i].y]=false;}scanf("%d",&t);for(int i=0;i<t;i++){scanf("%s",query[i].s);if(query[i].s[0]=='q')scanf("%d",&query[i].x);else{scanf("%d%d",&query[i].x,&query[i].y);//交換邊更有助于判斷if(query[i].x>query[i].y)swap(query[i].x,query[i].y);//標(biāo)記被毀掉的邊mp[query[i].x*10000+query[i].y]=true;}}//把沒被毀掉的邊合并起來for(int i=0;i<m;i++)if(!mp[tunnel[i].x*10000+tunnel[i].y])merge1(tunnel[i].x,tunnel[i].y);//逆向查找啦int sum=0;for(int i=t-1;i>=0;i--)if(query[i].s[0]=='q'){int k=find1(query[i].x);//根節(jié)點(diǎn)能量較小if(star[k].power>star[query[i].x].power)ans[sum++]=star[k].id;elseans[sum++]=-1;}//如果邊被毀壞,則毀壞之前肯定聯(lián)通,就加進(jìn)去elsemerge1(query[i].x,query[i].y);if(flag)flag=0;elseprintf("\n");//逆向輸出for(int i=sum-1;i>=0;i--)printf("%d\n", ans[i]);}return 0; }L:HDU-1272 小希的迷宮:判斷是否存在樹,一個(gè)圖若是樹需滿足兩個(gè)條件:連通分量為一;圖中無環(huán),包括自環(huán)和非自環(huán)。剩下的就是并查集的應(yīng)用問題了,代碼:
#include<iostream> #include<cstdio> #include<cstring> const int MAX=100001; int father[MAX]; int mark[MAX]; int n; using namespace std; void init()//初始化 {int i;for(i=1;i<=MAX;++i)father[i]=i,mark[i]=0; } int find_father(int x) {if(x!=father[x])father[x]=find_father(father[x]);return father[x]; } void join_tree(int x,int y) {mark[x]=1;//只要加進(jìn)來的都標(biāo)記mark[y]=1;int a,b;a=find_father(x);b=find_father(y);if(a!=b)father[a]=b; } int main() {int i,x,y,flag,sum;while(cin>>x>>y&&(x!=-1&&y!=-1)){init();memset(mark,0,sizeof(mark));if(find_father(x)==find_father(y)){if(x==0&&y==0)//特殊測試數(shù)據(jù),0 0是符合的所以輸出Yescout<<"Yes"<<endl;elsecout<<"No"<<endl;}else{flag=1;join_tree(x,y);//新節(jié)點(diǎn)加進(jìn)樹里面while(cin>>x>>y&&(x||y)){if(find_father(x)==find_father(y))//如果有環(huán)的話,就會(huì)相等,因?yàn)闆]環(huán)會(huì)逐漸加進(jìn)樹里面flag=0;elsejoin_tree(x,y);}sum=0;for(i=1;i<MAX;++i){if(father[i]==i&&mark[i])//一棵樹只有一個(gè)根節(jié)點(diǎn),如果多了根節(jié)點(diǎn)那么就不是樹,也不符合題意所有的通道都是連通的sum+=1;}if(sum>1)//根節(jié)點(diǎn)不是1就輸出Noflag=0;if(flag==1)cout<<"Yes"<<endl;elsecout<<"No"<<endl;}}return 0; }M:POJ-1308 Is It A Tree?:和上一題一樣,只不過無向圖換成了有向圖:
#include<iostream> #include<cstdio> #include<cstring> const int MAX=100001; int father[MAX]; int mark[MAX]; int n,flag; using namespace std; void init()//初始化 {int i;for(i=1;i<=MAX;++i)father[i]=i,mark[i]=0; } int find_father(int x) {if(x!=father[x])father[x]=find_father(father[x]);return father[x]; } void join_tree(int x,int y) {mark[x]=1;//只要加進(jìn)來的都標(biāo)記mark[y]=1;int a,b;a=find_father(x);b=find_father(y);if(a==b)flag=0;else if(a!=b)father[b]=a; //a要是b的祖先 } int main() {int i,x,y,sum,t=0;while(cin>>x>>y&&(x!=-1&&y!=-1)){init();memset(mark,0,sizeof(mark));if(x==0&&y==0){printf("Case %d is a tree.\n",++t);continue;}else{flag=1;join_tree(x,y);//新節(jié)點(diǎn)加進(jìn)樹里面while(cin>>x>>y&&(x||y)){if(x==0&&y==0)break;if(find_father(x)==find_father(y)||find_father(y)!=y)//如果有環(huán)的話,就會(huì)相等,因?yàn)闆]環(huán)會(huì)逐漸加進(jìn)樹里面flag=0;elsejoin_tree(x,y);}sum=0;for(i=1;i<MAX;++i){if(father[i]==i&&mark[i])//一棵樹只有一個(gè)根節(jié)點(diǎn),如果多了根節(jié)點(diǎn)那么就不是樹,也不符合題意所有的通道都是連通的sum+=1;}if(sum>1)//根節(jié)點(diǎn)不是1就輸出Noflag=0;if(flag==1)printf("Case %d is a tree.\n",++t);elseprintf("Case %d is not a tree.\n",++t);}}return 0; }?
總結(jié)
- 上一篇: 2018百度之星程序设计大赛资格赛
- 下一篇: BackTrack 4 新功能