Fork me on GitHub

《数据结构与算法分析》学习笔记-第八章-不相交集


对于每一对元素(a,b), a, b属于S,aRb或者为true或者为false,则称在集合S上定义关系R,如果aRb是true。那么我们说a与b有关系。

8.1 等价关系

等价关系是满足下列三个性质的关系R:

  1. (自反性)对于所有的a属于S,aRa
  2. (对称性)aRb当且仅当bRa
  3. (传递性)若aRb且bRc,则aRc

关系≤不是等价关系,因为他不是对称的,因为从a≤b,不能得出b≤a;电气连通性是一个等价关系。如果两个城市位于同一个国家,那么定义他们是有关系的,容易验证这是一个等价关系。

8.2 动态等价性问题

  • Find运算返回包含给定元素的集合,即等价类的名字。Union运算即求并运算,可以将a和b的两个等价类合并成一个新的的等价类。Sk = Si U Sj。该算法是动态的,因为在算法执行的过程中,集合可以通过Union运算而发生改变。这个运算还必然是联机操作,当Find执行时,它必须给出答案算法才能继续进行,另一种是脱机算法,它需要观察全部的Union和Find序列。他对每个Find给出的答案必须和所有执行到该Find的Union一致。而该算法在看到所有的问题以后再给出它的所有的答案。这种差别类似于参加一次笔试(它一般是脱机的,你只能在规定的时间用完之前给出的答卷)和一次口试(它是联机的,因为你必须回答当前的问题,然后才能继续下一个问题)。
  • 解决动态等价问题的方案有两种。一种方案保证指令Find能够以常数最坏情形运行时间执行;另一种方案则保证指令Union能够以常数最坏情形时间执行。二者不能同时做到。为使Find运算快,可以在一个数组中保存每个元素的等价类的名字,此时Find就是简单的O(1)查找。设我们想要执行Union(a,b),并设a在等价类i中而b在等价类j中。然后扫描数组,将所有的i变成j.这个扫描耗费O(N)时间。于是,连续N-1次Union操作(这是最大值,因为此时每个元素都在一个集合中)就要花费O(N^2) 时间,如果存在Ω(N^2) 次Find运算,那么性能会很好。因为在整个算法进行过程中每个Find或Union运算的总的运行时间为O(1),如果Find运算没有那么多,那么这个界是不可接受的。一种想法是将所有在同一个等价类中的元素放到一个链表中,者在更新的时候会节省时间,因为我们不必搜索整个数组,但由于他在算法过程中仍然可能执行Θ(N^2)次等价更新。每个元素可能将他的等价类最多改变logN次,因为每次它的等价类改变时它的新的等价类至少是他的原来等价类的两倍大,任意顺序的M次Find和直到N-1次的Union最多话费O(M + NlogN)时间。

8.3 基本数据结构

为了执行两个集合的Union运算,我们使一个结点的根指针指向另一棵树的根节点。这种操作花费常数时间。对元素X的一次Find(X)操作通过返回包含X的树的根而完成。执行这次操作花费时间与表示X的节点的深度成正比。这要假设我们以常数时间找到表示X的节点。用这种方法能够建立一棵深度为N-1的树,使得一次Find的最坏情形运行时间是O(N),一般情况,运行时间是对连续混合使用M个指令来计算的。这种情况下,M次连续操作在最坏情形下可能话费O(MN)时间

实现

  1. 类型声明
#ifndef _DisjSet-H
typedef int DisjSet[NumSets + 1];
typedef int SetType;
typedef int ElementType;

void Initialize(DisjSet S);
void SetUnion(DisjSet S, SetType Root1, SetType Root2);
SetType Find(ElementType X, DisjSet S);

#endif
  1. Initialize
void
Initialize(DisjSet S)
{
    int i;
    for (i = NumSets; i > 0; i--)
    {
        S[i] = 0;
    }
}
  1. SetUnion
void
SetUnion(DisjSet S, SetType Root1, SetType Root2)
{
    S[Root2] = Root1;
}
  1. Find
SetType
Find(ElementType X, DisjSet S)
{
    if (S[X] <= 0)
        return X;
    else
        return Find(S[X], S);
}

8.4 灵巧求并算法

  • 使用一种方法,总是让较小的树成为较大的树的子树,即按大小求并。这样会避免形成较深的树。如果按大小求并,那么任何结点的深度均不会超过logN。节点初始处于深度0的位置,当它的深度随着一次Union的结果而增加,该节点则被置于至少是它以前所在树两倍大的一棵树上。因此,它的深度最多可以增加logN次。Find操作的运行时间是O(logN),而连续M次操作则花费O(MlogN)。需要记住每一棵树的大小,由于实际上只使用一个数组,因此可以让每个根的数组元素包含它的树的大小的负值。初始时,树的数组表示就都是-1了。当执行一次Union时,要检查树的大小,新的大小是老的大小的和。这样,按大小求并的实现根本不存在困难。并且不需要额外的空间,其平均速度也很快。若使用按大小求并则连续M次运算需要O(M)平均时间。这是因为当随机的诸Union执行时整个算法一般只有一些很小的集合(通常含一个元素)与大集合合并。
  • 另一种实现方法为按高度求并。它同样保证所有的树的深度最多是O(logN)。我们跟踪每棵树的高度而不是大小并执行那些Union使得浅的树成为深的树的子树。这是一种平缓的算法,因为只有当两棵相等深度的树求并时树的高度才增加。按高度求并是按大小求并的简单修改。
void
SetUnion(DisjSet S, SetType Root1, SetType Root2)
{
    if (S[Root2] < S[Root1]) /* Root2 is deeper set */
    {
        S[Root1] = Root2;
    }
    else
    {
        if (S[Root1] == S[Root2]) /* Same height */
        {
            S[Root1]--; /* so update */
        }
        S[Root2] = Root1;
    }
}

8.5 路径压缩

路径压缩在一次Find操作期间执行,而与用来执行Union的方法无关。设操作为Find(X),此时路径压缩的效果是,从X到根的路径上的每一个节点都使它的父节点变成根。

SetType
Find(ElementType X, DisjSet S)
{
    if (S[X] <= 0)
    {
        return X;
    }
    else
    {
        return S[X] = Find(S[X], S);
    }
}

当执行一些Union操作的时候,路径压缩是个好的想法,因为存在许多的深层节点并通过路径压缩将他们移近根节点。在这种情况下进行路径压缩时,连续M次操作最多需要O(MlogN)的时间。路径压缩与按大小求并完全兼容,与按高度求并不完全兼容

8.7 一个应用

我们有一个计算机网络和一个双向连接表。每一个连接可将文件从一台计算机传送到另一台计算机。如何将任意一个文件传送给任意一个计算机。解决该问题的一个算法时开始时把每一台计算机放到它自己的集合中。我们要求两台计算机可以传输文件当且仅当他们在同一个集合中。可以看出,传输文件的能力形成一个等价关系。我们一次一个的读入连接,当读入某个链接(u, v),则测试是否u和v在一个集合中。如果在一个集合中则什么都不做,如果在不同的集合中,那我们将他们所在的两个集合合并。在算法的最后,所得到的图联通当且仅当恰好存在一个集合。如果存在M个连接和N台计算机,那么空间的需求则是O(N)。使用按大小求并和路径压缩的方法,我们得到最坏情形运行时间为O(M a(M, N)),因为存在2M次Find和至多N-1次Union.这个运行时间在实用中是线性的

参考文献

  1. Mark Allen Weiss.数据结构与算法分析[M].America, 2007

本文作者: CrazyCatJack

本文链接: https://www.cnblogs.com/CrazyCatJack/p/14408186.html

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

关注博主:如果您觉得该文章对您有帮助,可以点击文章右下角推荐一下,您的支持将成为我最大的动力!


posted @ 2021-02-21 16:39  CrazyCatJack  阅读(566)  评论(0编辑  收藏  举报