并查集-05-树8 File Transfer

  • 题目
    05-树8 File Transfer (25分)
  • 分析
    这道题考察的是并查集的基本操作,核心操作就两个:
    1.查找元素所在的集合
    2.并集,即合并两个集合
    首先可以用一个数组来保存这N个计算机和它们的集合。
    数组下标i表示计算机i,S[i] 来体现所在的集合。当 S[i] 为负数时,表示它就是集合的根,S[i] 为正数时,表示父结点是S[i]。
  • 基本的代码
#include
#include
#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)

除了这样按秩归并的优化外,我们还可以压缩路径

并查集-05-树8 File Transfer_第1张图片

这样使得查找更快,因为压缩路径之后树高明显降低了。
具体代码:

//路径压缩 
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
#include
#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
#include
#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;
}

一般来说,按规模归并与压缩路径配合使用更方便,想一想为什么?

你可能感兴趣的:(数据结构与算法)