問題描述
問題輸入是一對整數對,每個整數都代表一個物件,一對整數”p,q“表示 ”p與q相連“(具有自反性,傳遞性,對稱性,被歸到一個等價類裡),要求編寫程式來篩除在輸入時就已經在一個等價類裡的整數對。這個演算法可以在計算機網路連結方面發揮作用:每個整數相當於計算機,整數對相當於網路間的連結,我們的程式可以判斷為了使p,q兩個計算機連結,需不需要新增一個新的線路。
具體思想
1.根節點判斷
我們可以通過一個“概念上”的森林來實現我們的程式。我們把union-find演算法打包成一個類,在其中設定一個名為id的陣列用來存放每個節點的下一個連結物件,這樣可以通過接受一個陣列的秩來不斷訪問它所連結的下一個物件,直至到一個秩和所儲存物件節點號相同的節點(根節點)。而比較兩個節點的根節點就可以判斷他們是否連在同一個根節點上,進而判斷出兩個節點是否已經連結。
2.加權連結
如果判斷兩個節點沒有連結到一個根節點,我們就對他們的根節點進行連結。這時就會產生一個問題:p去連q還是q去連p。這裡我們採用加權的演算法:引入一個陣列sz(size)來記錄每個節點作為根節點時樹中的節點數,把它作為節點的權值。每次進行根節點連結時,我們總是把權值小的根節點連結到權值大的根節點上。這樣的好處是可以極大地降低樹的高度的增加速度(最大程度降速的方式是把樹高度作為權值的加權連結,但經過路徑壓縮後,這種方式變得沒有必要),從而降低查詢根節點時的時間量級。在最壞的情況下,要連結的樹的大小總是相等的,且都是2的冪,則把所有的N個節點合成一個樹,這個樹的高度是底數為2的N的對數。
致使查詢要檢索的高度也達到O(logN)。
PS:本圖取自Algorithm(4th)中譯版P146(原版P229)
實現程式碼
#include<iostream>
#include<vector>
#include<random>
using namespace std;
//WeightedQuickUnion(加權快速連結)
class WQU {
vector<int> id;
vector<int> sz;
int count = 0;
int numberOfSite = 0;
public:
int find(int p);
void Union(int p, int q);
int get_count() { return count; }
bool connected(int p, int q);
int Count(int n);
int newSite();
};
bool WQU::connected(int p, int q) {
if (p >= numberOfSite || q >= numberOfSite) {
throw runtime_error("Site does not exist");
}
return find(p) == find(q);
}
//壓縮路徑find,遞迴,只修改查詢時經歷節點的連結
int WQU::find(int p) {
if (p >= numberOfSite) {
throw runtime_error("Site does not exist");
}
if (p == id[p]) { return p; }
return id[p] = find(id[p]);
}
void WQU::Union(int p, int q) {
if (p >= numberOfSite || q >= numberOfSite) {
throw runtime_error("Site does not exist");
}
int rp = find(p);
int rq = find(q);
if (rp == rq) { return; }
if (sz[rp] < sz[rq]) {
id[rp] = rq;
sz[rq] += sz[rp];
}
else {
id[rq] = rp;
sz[rp] += sz[rq];
}
--count;
}
//測試隨機生成資料最終連在一棵樹上所需的連結條數
int WQU::Count(int n) {
while (n > numberOfSite) { newSite(); }
while (get_count() != 1) {
int p = rand() % n;
int q = rand() % n;
cout << p << ' ' << q << endl;
if (!connected(p, q)) {
Union(p, q);
}
}
int cnt = 0;
for (size_t i = 0; i < id.size(); ++i) {
if (i != id[i]) {
++cnt;
}
}
return cnt;
}
//建立一個參與連結的新節點,返回它的值
int WQU::newSite() {
id.push_back(numberOfSite);
sz.push_back(1);
++count;
return numberOfSite++;
}
int main() {
int n;
cin >> n;
WQU temp;
cout << temp.Count(n);
return 0;
}
find採用遞迴壓縮路徑。在不改變根節點的情況下,令被查詢過的節點指向其根節點,從而降低被查詢過的節點的深度,進而降低再次查詢時的時間複雜度。