并查集簡單來說是集合的集合,其中里層集合表示的節(jié)點(diǎn)都是可互相聯(lián)通的,并查集有兩種操作:
如下圖所示,原來的并查集為
{
{
0
}
,
{
1
,
4
,
5
}
,
{
2
,
3
,
6
,
7
}
}
\{\{0\},\{1,4,5\},\{2,3,6,7\}\}
{{0},{1,4,5},{2,3,6,7}},
{
1
,
4
,
5
}
\{1,4,5\}
{1,4,5}集合表示節(jié)點(diǎn)1、4、5可互相連通,經(jīng)過合并2,5節(jié)點(diǎn)后,變成了右邊的并查集,即
{
1
,
4
,
5
}
\{1,4,5\}
{1,4,5}和
{
2
,
3
,
6
,
7
}
\{2,3,6,7\}
{2,3,6,7}合并成了一個(gè)集合
{
1
,
2
,
3
,
4
,
5
,
6
,
7
}
\{1,2,3,4,5,6,7\}
{1,2,3,4,5,6,7}
并查集的節(jié)點(diǎn)個(gè)數(shù)N可以很大,查詢操作個(gè)數(shù)M也可能很大,并且合并和查詢操作可以是混合在一起的。
接下來將介紹幾種類型的并查集,最終將介紹一個(gè)優(yōu)化的并查集,它只增加了幾行代碼就可以讓性能提高很多。
quick-find并查集我們用一個(gè)數(shù)組id來表示并查集,如果節(jié)點(diǎn)p和節(jié)點(diǎn)q是連接的,當(dāng)且僅當(dāng)它們的id相同,例如0,5,6是相連的,它們有共同的id = 0。
上述并查集的代碼如下:
#include#includeusing namespace std;
#define MAX 10004
class UnionFind {int id[MAX]; // 0 ~ 1000 記錄上級/老大
int minIndex; // 范圍
int maxIndex; // 范圍
int cnt; //連通分量的個(gè)數(shù)
public :
UnionFind() {}
UnionFind(int minIndex, int maxIndex) {this->minIndex = minIndex;
this->maxIndex = maxIndex;
this->cnt = maxIndex - minIndex + 1; // 連通分量的個(gè)數(shù)
for (int i = minIndex; i<= maxIndex ; i++) {id[i] = i; //初始化父節(jié)點(diǎn)指向自己
}
}
bool isConnected(int p, int q) {//判斷p和q是否相連
cout<< p<< "和"<< q<< (id[p]==id[q]?"":"不")<< "相連"<< endl;
return id[p] == id[q];
}
bool connect(int p, int q) {//合并x所在集合和y所在集合
cout<< "合并"<< p<< "和"<< q<< endl;
int pid = id[p], qid = id[q];
if (pid == qid) return false; // 已經(jīng)在同一個(gè)set里面了,已經(jīng)在同一個(gè)連通分量里面了
for(int i = minIndex; i<= maxIndex; i++){//將p所在集合的id改成q所在集合的id
if(id[i] == pid)
id[i] = qid;
}
cnt --; //連通分量少1
}
void show(){for(int i = minIndex; i<= maxIndex; i++){cout<< "i:"<< i<< " id:"<< id[i]<< endl;
}
}
};
int main() {class UnionFind uf(0,9); //定義節(jié)點(diǎn)為0-9
uf.connect(0,5); //連接0和5節(jié)點(diǎn)
uf.connect(5,6);
uf.connect(1,2);
uf.connect(2,7);
uf.connect(1,6);
uf.connect(3,8);
uf.connect(3,4);
uf.connect(4,9);
uf.isConnected(4,0); //查詢4和0是否相連
uf.isConnected(2,5);
uf.show();
}
quick-union并查集和quick-find的id數(shù)組表示含義不一樣,quick-union的id數(shù)組表示它的父節(jié)點(diǎn),根節(jié)點(diǎn)為
i
d
[
i
d
[
i
d
[
.
.
.
i
d
[
i
]
]
.
.
.
]
]
]
id[id[id[...id[i]]...]]]
id[id[id[...id[i]]...]]],如下圖所示,3號節(jié)點(diǎn)的父節(jié)點(diǎn),即id為4,根節(jié)點(diǎn)為
i
d
[
i
d
[
i
d
[
3
]
]
]
=
i
d
[
i
d
[
4
]
]
=
i
d
[
9
]
=
9
id[id[id[3]]] = id[id[4]]=id[9]=9
id[id[id[3]]]=id[id[4]]=id[9]=9。
union操作:合并p和q兩個(gè)節(jié)點(diǎn)只需要將p的根節(jié)點(diǎn)id設(shè)置成q的根節(jié)點(diǎn)id即可,例如,將3和5合并,只需要將3的根節(jié)點(diǎn)id設(shè)置成5的根節(jié)點(diǎn)id即可,時(shí)間復(fù)雜度為
O
(
1
)
O(1)
O(1),下圖展示了Union操作
代碼表示如下:
#include#includeusing namespace std;
#define MAX 10004
class UnionFind {int id[MAX]; // 0 ~ 1000 記錄上級/老大
int minIndex; // 范圍
int maxIndex; // 范圍
int cnt; //連通分量的個(gè)數(shù)
public :
UnionFind() {}
UnionFind(int minIndex, int maxIndex) {this->minIndex = minIndex;
this->maxIndex = maxIndex;
this->cnt = maxIndex - minIndex + 1; // 連通分量的個(gè)數(shù)
for (int i = minIndex; i<= maxIndex ; i++) {id[i] = i; //初始化父節(jié)點(diǎn)指向自己
}
}
int getRoot(int p){//獲得根節(jié)點(diǎn)id
while(p != id[p]) p = id[p]; //逐個(gè)向上遍歷
return p;
}
bool isConnected(int p, int q) {//判斷p和q是否相連
cout<< p<< "和"<< q<< (getRoot(p)==getRoot(q)?"":"不")<< "相連"<< endl;
return getRoot(p)==getRoot(q);
}
bool connect(int p, int q) {//合并x所在集合和y所在集合
cout<< "合并"<< p<< "和"<< q<< endl;
id[getRoot(p)] = getRoot(q);
cnt --; //連通分量少1
}
void show(){for(int i = minIndex; i<= maxIndex; i++){cout<< "i:"<< i<< " id:"<< id[i]<< endl;
}
}
};
int main() {class UnionFind uf(0,9); //定義節(jié)點(diǎn)為0-9
uf.connect(0,5); //連接0和5節(jié)點(diǎn)
uf.connect(5,6);
uf.connect(1,2);
uf.connect(2,7);
uf.connect(1,6);
uf.connect(3,8);
uf.connect(3,4);
uf.connect(4,9);
uf.isConnected(4,0); //查詢4和0是否相連
uf.isConnected(2,5);
uf.show();
}
最后的id圖和并查集(兩棵樹)如下兩幅圖所示,可以看出來深度還是挺大的,例如查詢1和3節(jié)點(diǎn)的根節(jié)點(diǎn)需要查詢3次,比較耗時(shí)。
這里的trick是把矮一點(diǎn)的樹拼接到高一點(diǎn)的樹,這樣樹的高度就不會增加,查詢根節(jié)點(diǎn)就會更快
我們增加一個(gè)數(shù)組contain,contain[i]表示以i為根節(jié)點(diǎn)的子樹的節(jié)點(diǎn)個(gè)數(shù),初始條件下contain值全為1。
當(dāng)我們拼接兩棵樹時(shí),我們比較兩棵樹的contain值,將contain值小的樹拼接到contain值大的樹上,并且大樹的contain值要加上小樹的contain值,用代碼表示如下:
int pRoot = getRoot(p);
int qRoot = getRoot(q);
if (qRoot == pRoot) {return false; // 已經(jīng)在同一個(gè)set里面了,已經(jīng)在同一個(gè)連通分量里面了
}
else {if(contain[p] >= contain[q]) {id[qRoot] = pRoot;
contain[pRoot] += contain[qRoot];
}
else {id[pRoot] = qRoot;
contain[qRoot] += contain[pRoot];
}
}
這樣修改以后,樹的深度最多是 l o g ? N log\ N log?N,也就是說find操作的時(shí)間復(fù)雜度為 O ( l o g ? N ) O(log\ N) O(log?N),union操作也為 O ( l o g ? N ) O(log\ N) O(log?N),因?yàn)閡nion需要尋找根節(jié)點(diǎn),這樣union操作和find操作的復(fù)雜度得到了中和。
優(yōu)化二:路徑壓縮當(dāng)我們查詢了一次節(jié)點(diǎn)p之后,我們求出了它的根節(jié)點(diǎn)root,我們可以p直接和root相連,這樣我們下一次查找p的根節(jié)點(diǎn)就只要 O ( 1 ) O(1) O(1)的時(shí)間了,我們就只需要在find操作里加兩行代碼即可,表示將當(dāng)前節(jié)點(diǎn)與其父節(jié)點(diǎn)相連,直到與其根節(jié)點(diǎn)相連。
int getRoot(int p) {//采用遞歸的方式
if (id[p] == p) {//自己就是根節(jié)點(diǎn)
return p;
}
else {int d = id[p];
int root = getRoot(d);
if (d != root) {id[p] = root; //將當(dāng)前節(jié)點(diǎn)的id設(shè)置成根節(jié)點(diǎn)
contain[d] -= contain[p]; //因?yàn)閐的子樹移到了根節(jié)點(diǎn),所以要將d的contain減去p的contain
}
return root;
}
}
初始的樹如下所示:
依次查詢節(jié)點(diǎn)9,6,3以后,將它們移到根節(jié)點(diǎn)下,整棵樹的高度就比之前少了很多,變得更平坦了。
可以驗(yàn)證當(dāng)有 1 0 9 10^9 109次union操作和 1 0 9 10^9 109次find操作,最普通的并查集需要花費(fèi)30多年,而僅加上幾行代碼的優(yōu)化過的并查集只需要6秒,由此可見一個(gè)性能好的算法是多么重要。
優(yōu)化過后的代碼#include#includeusing namespace std;
#define MAX 10004
class UnionFind {int id[MAX]; //
int contain[MAX]; // 包含多少節(jié)點(diǎn)
int minIndex; // 范圍
int maxIndex; // 范圍
int cnt; //連通分量的個(gè)數(shù)
public :
UnionFind() {}
UnionFind(int minIndex, int maxIndex) {this->minIndex = minIndex;
this->maxIndex = maxIndex;
this->cnt = maxIndex - minIndex + 1; // 連通分量的個(gè)數(shù)
for (int i = minIndex; i<= maxIndex ; i++) {id[i] = i;
contain[i] = 1;
}
}
int getRoot(int p) {//采用遞歸的方式
if (id[p] == p) {//自己就是根節(jié)點(diǎn)
return p;
}
else {int d = id[p];
int root = getRoot(d);
if (d != root) {id[p] = root; //將當(dāng)前節(jié)點(diǎn)的id設(shè)置成根節(jié)點(diǎn)
contain[d] -= contain[p]; //因?yàn)閐的子樹移到了根節(jié)點(diǎn),所以要將d的contain減去p的contain
}
return root;
}
}
bool isConnected(int p, int q) {return getRoot(q) == getRoot(p);
}
bool connect(int p, int q) {int pRoot = getRoot(p);
int qRoot = getRoot(q);
if (qRoot == pRoot) {return false; // 已經(jīng)在同一個(gè)set里面了,已經(jīng)在同一個(gè)連通分量里面了
}
else {if(contain[p] >= contain[q]) {id[qRoot] = pRoot;
contain[pRoot] += contain[qRoot];
}
else {id[pRoot] = qRoot;
contain[qRoot] += contain[pRoot];
}
}
cnt --; //連通分量少1
}
int getCnt() {return cnt;
}
void show(){cout<< "i\tid\tcontain"<< endl;
for(int i = minIndex; i<= maxIndex; i++){cout<< i<< "\t"<< id[i]<< "\t"<< contain[i]<< endl;
}
}
};
int main() {class UnionFind uf(1,6); //定義節(jié)點(diǎn)為1-6
uf.connect(3,4); //連接3和4節(jié)點(diǎn)
uf.connect(5,4);
uf.connect(1,2);
uf.connect(6,5);
uf.connect(1,5);
uf.getRoot(4); //查找4的根節(jié)點(diǎn)
uf.getRoot(2);
uf.show();
}
你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧