問題:
一個含有N整數的序列,它們相互獨立。
現在要輸入若干整數對,每輸入一個整數對,表示將該兩個整數連。
當輸入某一個整數對時,如果根據已有的連通情況,判斷該對是連通的,那麼就繼續輸入下一對。否則將該對打印出來,表示產生一個新的並集。
請編寫這樣的一個C算法。
分析:
我們用數組的引索表示這N個數 id[N]。其實這裏的引索表示的是內存的不同位置。這樣就把問題轉換爲內存位置的連通性問題。
可以用內存位置的值相等來表示這兩個位置是連通的。這樣就有下面的算法:
while(scanf("%d %d/n", &p, &q) == 2){
if(id[p] == id[q]) continue;
for(t = id[p], i = 0; i < N; ++i)
if(id[i] == t) id[i] = id[q];
printf(" %d %d/n", p, q);
}
遍歷整個數組,然後將所有等於id[p]的位置的值設置爲id[q]這樣就實現了並集。
快速查找算法和測試函數如下:
//創建連通樹,輸入對時,打印出未連通的。
#include<stdio.h>
#define N 10
void quick_find( int *id );
int main(){
//不同的內存位置來存放整數,那麼兩個內存位置的值相同表示這兩個內存位置連通。
int i, id[N];
for(i = 0; i < N; ++i){
id[i] = i;
printf(" %d ", id[i]);
}
printf("/n");
quick_find(id);
}
void quick_find( int *id ){
int i, p, q, t;
//輸入的兩個值表示
while(scanf("%d %d", &p, &q) == 2){
if(id[p] == id[q]) continue;
//並集操作
for(t = id[p], i = 0; i < N; ++i)
if(id[i] == t) id[i] = id[q];
for(i = 0; i < N; ++i){
printf(" %d ", id[i]);
}
printf("/n");
}
}
關於這裏的快速查找算法,我們傳入的是數組,其實就是對這樣的一個數組作一個並集操作,並且輸入要連通的位置,然後打印出這個數組沒每一次並集後的狀態,這裏暫且不分析它的時間複雜度。但是可以想一下,每一次並集合都要遍歷整個數組。能不能不要這樣遍歷數組呢?
下面是快速並集的算法:
for(i = p; i != id[i]; i = id[i]);
for(j = q; j != id[j]; j = id[j]);
if(i == j) continue;
id[i] = j;//i指向j,連接兩個根節點。
當輸入兩個位置後,先要找到它的根節點,當兩個跟節點不相等的時候,讓i指向j,即將j作爲新的根節點。
下面是完整的代碼:
#include<stdio.h>
#define N 10
void quick_union(int *id);
int main(){
int id[N], i;
for(i = 0; i < 10; i++){
id[i] = i;
printf(" %d ", id[i]);
}
printf("/n");
quick_union(id);
}
void quick_union(int *id){
int p, q, i, j;
while(scanf("%d %d", &p, &q)){
//當i等於id[i]的時候,表示i位置是一個根節點。
//這裏的for循環的判斷條件是i,j位置是否是一個根節點。最終肯定能找到根節點。
for(i = p; i != id[i]; i = id[i]);
for(j = q; j != id[j]; j = id[j]);
if(i == j) continue;
id[i] = j;//i指向j,連接兩個根節點。
for(i = 0; i < 10; i++){
printf(" %d ", id[i]);
}
printf("/n");
}
}
這裏的快速並集算法,之所以叫做快速並集算法,我可以看到當我們進行並集操作的時候,只需要把輸入位置的兩個根節點作合併爲一個根節點。顯然這裏的查找過程是緩慢的。
當我們把最後的兩個根節點合併爲一個根節點的時候,可以考慮兩個節點的權重問題。
if(sz[i] < sz[j]){
id[i] = j;//當i節點的權重小時,讓i指向j
sz[j] += sz[i];
}else{
id[j] = i;
sz[i] += sz[j];
}
下面是完整的加權快速並集算法和測試的代碼:
#include<stdio.h>
#define N 10
void quick_union(int *id);
int main(){
int id[N], i;
for(i = 0; i < 10; i++){
id[i] = i;
printf(" %d ", id[i]);
}
printf("/n");
quick_union(id);
}
void quick_union(int *id){
int p, q, i, j;
while(scanf("%d %d", &p, &q)){
//當i等於id[i]的時候,表示i位置是一個根節點。
//這裏的for循環的判斷條件是i,j位置是否是一個根節點。最終肯定能找到根節點。
for(i = p; i != id[i]; i = id[i]);
for(j = q; j != id[j]; j = id[j]);
if(i == j) continue;
if(sz[i] < sz[j]){
id[i] = j;//當i節點的權重小時,讓i指向j
sz[j] += sz[i];
}else{
id[j] = i;
sz[i] += sz[j];
}
for(i = 0; i < 10; i++){
printf(" %d ", id[i]);
}
printf("/n");
}
}
我們可以修改樹的結構來使算法更加有效,可以採用分路徑壓縮。分路徑壓縮是很容易實現的:
可以將i指向id[i]所指向的位置。
for(i = p; i != id[i]; i = id[i]) id[i] = id[id[i]];
for(j = q; j != id[j]; j = id[j]) id[j] = id[id[j]];
下面是加權快速並集分路徑壓縮算法的完整測試代碼:
#include<stdio.h>
#define N 10
void quick_union(int *id);
int main(){
int id[N], i;
for(i = 0; i < 10; i++){
id[i] = i;
printf(" %d ", id[i]);
}
printf("/n");
quick_union(id);
}
void quick_union(int *id){
int p, q, i, j;
while(scanf("%d %d", &p, &q)){
//當i等於id[i]的時候,表示i位置是一個根節點。
//這裏的for循環的判斷條件是i,j位置是否是一個根節點。最終肯定能找到根節點。
for(i = p; i != id[i]; i = id[i]) id[i] = id[id[i]];
for(j = q; j != id[j]; j = id[j]) id[j] = id[id[j]];
if(i == j) continue;
if(sz[i] < sz[j]){
id[i] = j;//當i節點的權重小時,讓i指向j
sz[j] += sz[i];
}else{
id[j] = i;
sz[i] += sz[j];
}
for(i = 0; i < 10; i++){
printf(" %d ", id[i]);
}
printf("/n");
}
}
關於不同的解決方案的優越性,這裏不討論。這裏只是給出這3中解決方案。