1. 程式人生 > >並查集-05-樹8 File Transfer

並查集-05-樹8 File Transfer

  • 分析
    這道題考察的是並查集的基本操作,核心操作就兩個:
    1.查詢元素所在的集合
    2.並集,即合併兩個集合
    首先可以用一個數組來儲存這N個計算機和它們的集合。
    陣列下標i表示計算機i,S[i] 來體現所在的集合。當 S[i] 為負數時,表示它就是集合的根,S[i] 為正數時,表示父結點是S[i]。
  • 基本的程式碼
#include<stdio.h>
#include<string.h>
#define maxn 10001

int S[maxn];    //S[i]中,負數表示是根結點,正數表示父結點 
int connected;

int myFind(int
x) { int parent = x; while((parent=S[x]) > 0) { x = parent; } return x; } void myUnion(int x1, int x2) { int root1 = myFind(x1); int root2 = myFind(x2); if(root1 != root2) { S[root2] = root1; connected--; } } int main() { //freopen("set.txt","r",stdin);
int N; char op; int num1,num2; int root1,root2; scanf("%d",&N); memset(S,-1,sizeof(int)*N); connected = N; while(1) { scanf("%c", &op); if(op == 'S') break; else if(op == 'I'){ scanf("%d%d",&num1,&num2); myUnion(num1,num2); }else
if(op == 'C'){ scanf("%d%d",&num1,&num2); root1 = myFind(num1); root2 = myFind(num2); if(root1 == root2) printf("yes\n"); else printf("no\n"); } } if(connected == 1){ printf("The network is connected.\n"); }else{ printf("There are %d components.\n",connected); } return 0; }

程式基本就是這樣子的,提交之後是部分正確,因為沒有對歸併(並集)操作進行優化,會超時。
我們具體來看一下並集操作

void myUnion(int x1, int x2)
{
    int root1 = myFind(x1);
    int root2 = myFind(x2);
    if(root1 != root2)
    {
        S[root2] = root1; 
        connected--;
    }
}

我們每次歸併都是將 第二個集合 貼到 第一個集合上面,如果每次都是正好將 高度很高的集合 貼到了 高度比較低的集合上,那麼集合樹的高度一直在增加,甚至可能高度就是N,所以每次myFind操作都需要遍歷很高的樹,對N次歸併,時間複雜度是O(N^2)。N是10的4次方數量級,平方之後顯然會超時。

如何進行優化呢?
我們首先可以想到可以將矮樹貼到高樹上面去。或者將規模小的樹貼到規模大的樹。
那麼如何儲存樹的高度或規模呢?我們之前,S[root]中儲存的都是-1,負數就表示是根結點,那麼我們可以用這個負數來儲存樹高或規模。
根據樹高進行歸併虛擬碼:

//S[i] 都初始化為 -1
if(root1高度 > root2高度)
    S[root2] = root1;
else{
    if(root2高度 == root1高度) 樹高++
    S[root1] = root2;
}

根據樹高進行歸併具體程式碼:

if(S[root1] < S[root2]){//1樹高 
    S[root2] = root1; 
}else{
    if(S[root1] == S[root2])    S[root2]--;     //樹高加一 
    S[root1] = root2;
}

同理很容易得出根據樹的規模進行歸併的程式碼

if(S[root1] < S[root2]){//1規模大
    S[root1] += S[root2];   //更新規模 
    S[root2] = root1; 
}else{
    S[root2] += S[root1]; 
    S[root1] = root2;
}

這兩種方式都屬於 按秩歸併,按秩歸併最壞情況下的樹高是O(logN),那麼N次歸併,時間複雜度就是O(N*logN)

除了這樣按秩歸併的優化外,我們還可以壓縮路徑

路徑壓縮

這樣使得查詢更快,因為壓縮路徑之後樹高明顯降低了。
具體程式碼:

//路徑壓縮 
int myFind(int x)
{
    if(S[x] < 0)    return x; 
    return S[x] = myFind(S[x]);
}

是一種尾遞迴的方式來寫的,雖然壓縮路徑的時候會稍微花一些時間,但是之後的查詢速度快了很多。
數學可以證明,無論N有多大(int範圍內),樹高不會超過4。那麼N次歸併,時間複雜度就是O(N*一個常數),相當於O(N)。不過這道題不進行路徑壓縮也可以通過。

  • 只按秩歸併(根據樹高)的程式碼
#include<stdio.h>
#include<string.h>
#define maxn 10001

int S[maxn];    //S[i]中,負數表示高度,即S[root]=-樹高,正數表示父結點 
int connected;

int myFind(int x)
{
    int parent = x;
    while((parent=S[x]) > 0)
    {
        x = parent;
    } 
    return x;
}

void myUnion(int x1, int x2)
{
    int root1 = myFind(x1);
    int root2 = myFind(x2);
    if(root1 != root2)
    {
        if(S[root1] < S[root2]){//1樹高 
            S[root2] = root1; 
        }else{
            if(S[root1] == S[root2])    S[root2]--;     //樹高加一 
            S[root1] = root2;
        }

        connected--;
    }
}

int main()
{
    //freopen("set.txt","r",stdin);

    int N;
    char op;
    int num1,num2;
    int root1,root2;
    scanf("%d",&N);
    memset(S,-1,sizeof(int)*N);
    connected = N;
    while(1)
    {
        scanf("%c", &op);
        if(op == 'S')   break;
        else if(op == 'I'){
            scanf("%d%d",&num1,&num2);
            myUnion(num1,num2);
        }else if(op == 'C'){
            scanf("%d%d",&num1,&num2);
            root1 = myFind(num1);
            root2 = myFind(num2);
            if(root1 == root2)  printf("yes\n");
            else                printf("no\n");
        }
    }

    if(connected == 1){
        printf("The network is connected.\n");
    }else{
        printf("There are %d components.\n",connected);
    }

    return 0;
}
  • 既按秩歸併(根據樹的規模),也壓縮路徑的程式碼
#include<stdio.h>
#include<string.h>
#define maxn 10001

int S[maxn];    //S[i]中,負數表示高度,即S[root]=-規模,正數表示父節點 
int connected;

//路徑壓縮 
int myFind(int x)
{
    if(S[x] < 0)    return x; 
    return S[x] = myFind(S[x]);
}

void myUnion(int x1, int x2)
{
    int root1 = myFind(x1);
    int root2 = myFind(x2);
    if(root1 != root2)
    {
        if(S[root1] < S[root2]){//1規模大
            S[root1] += S[root2];   //更新規模 
            S[root2] = root1; 
        }else{
            S[root2] += S[root1]; 
            S[root1] = root2;
        }

        connected--;
    }
}

int main()
{
    //freopen("set.txt","r",stdin);

    int N;
    char op;
    int num1,num2;
    int root1,root2;
    scanf("%d",&N);
    memset(S,-1,sizeof(int)*N);
    connected = N;
    while(1)
    {
        scanf("%c", &op);
        if(op == 'S')   break;
        else if(op == 'I'){
            scanf("%d%d",&num1,&num2);
            myUnion(num1,num2);
        }else if(op == 'C'){
            scanf("%d%d",&num1,&num2);
            root1 = myFind(num1);
            root2 = myFind(num2);
            if(root1 == root2)  printf("yes\n");
            else                printf("no\n");
        }
    }

    if(connected == 1){
        printf("The network is connected.\n");
    }else{
        printf("There are %d components.\n",connected);
    }

    return 0;
}

一般來說,按規模歸併與壓縮路徑配合使用更方便,想一想為什麼?