基础算法学习(02)-Union-Find问题

部分内容参考http://www.cnblogs.com/SeaSky0606/p/4752941.html

Union-Find问题

1、算法作用

在一堆数据集合中找到两个触点是否被链接在一起,如果不在一起将这两个点链接在一起,并且最后得出这种链接关系一共有多少个。

2、C++实现

2.1.quick-find算法

能实现快速查找是否在一起。原理可以简单理解为:将每个点都赋予一个链接的标号,链接的过程就是把两个触点的标号设为同一个,判断两个点是否被链接就通过查看两者的链接标号是否相同。缺点是:如果要将所有触点都连接,至少要调动 n-1 次unionn(),即至少 (n+3)(n-1)~n^2 次数组访问,find()操作的时间复杂度为:O(l),Unionn()的时间复杂度为:O(N)。
代码如下

#include "test.h"
using namespace std;

//首先构造一个类以实现quick-find算法
class UF
{
    int *id;//链条id数组(以触点作为数组下标)
    int count;//链条数量

public:
    UF(int n)//初始化n个触点
    {
        count=n;
        id=new int[n];
        for(int i=0;i<n;i++)
        {
            id[i]=i;
        }
        cout<<"初始化了"<<n<<"个触点"<<endl;
    }
    ~UF()
    {
        delete [] id;
    }

//-----------------以下关键代码部分-------------------------//  
public:
    //在p和q之间添加连接时,检查数组中id和p一样的所有触点,将这些触点的id改为q的链条标识符,这里会将数组遍历一遍
    void unionn(int p, int q)
    {
        int pID=find(p);
        int qID=find(q);
        for(int i=0;i<m;i++)
        {
            if(find(i)==pID)
            {
                id[i]=qID;
            }
        }
        count--;
    }
    //查找时,只需要直接返回触点对应的id即可
    int find(int p)
    {
        return id[p];
    }
//-----------------以上关键代码部分-----------------------//

    bool connected(int p,int q)//如果p和q存在于同一个链接则返回true
    {
        return find(p)==find(q);
    }

     int getCount()//获取连通的链条数目
    {
        return count;
    }  
};

int main() {
    cout << "Hello Myself!" << endl;
    cout << "输入触点数量n:" ;
    int n;
    cin >>n;
    cout << endl ;
    UF uf(n);
    while(getchar()!='y')
    {
        int p,q;
        cout<<"输入要连接的p和q"<<endl;
        cin>>p>>q;
        if(uf.connected(p,q))
        {
            cout<<p<<"和"<<q<<"已经连接"<<endl;
            continue;
        }
        uf.unionn(p,q);
        cout<<p<<"连接上了"<<q<<endl<<"要结束连接吗?(结束请按y,继续请按回车)"<<endl;
        getchar();
    }
    int a=uf.getCount();
    cout<<"共有"<<a<<"条链接"<<endl;


    cout<<endl<<endl<<endl<<endl;
    system("pause");
    return 0;
}

2.2.quick-union算法

unionn的复杂度为O(1),find()的复杂度为O(lgN),整体降低了一级。
关键部分代码如下(替换刚才的关键部分即可直接运行):

public:
//在p和q之间添加连接时,将q的根节点的id改为p的根节点的id,这样在查找时,直接查看q的id是否被修改,如果被修改了,查看根即可知id
    void unionn(int p, int q)
    {
        int pID=find(p);
        int qID=find(q);
        if(qID==pID)
            return;
        id[qID]=pID;//把q的根节点并到p的根上,最开始的链接的这部分有问题,见下面注释
        count--;
    }
//而查找时,先确定本身的id有没有被更改,如果更改了往根部回溯查找,这个函数可以理解为一个找根的函数
    int find(int p)
    {
        while(p!=id[p]) 
            p= id[p];  //若找不到,则往根root回溯
        return p;
    }

注释:原文是将本节点q的id改为父树的根的id,实际上是错的,这样的话这个q节点会从原来的树剥离。

举例说明quick-union算法的特点:
如0,1,2三个触点对应的初始id分别为0,1,2
若1(作为p)连接到2(作为q),执行一次union后,三个id分别为0,2,2,也就是说,触点1的根是2,触点1和触点2形成了一棵树。
然后再执行一次2连接到0(注意不是0到2),执行一次union后,2号触点的id变成了0号的id,也就是三个分别为0,2,0,触点2连接到了触点0上。
这个时候,find一次触点1的id试试,会发现第一次while循环时,找到触点1的根是2,第二次while循环时,找到更上一层的根(触点2的根)是0。此时才能发现,1和0是连接的。

简单的说,这种算法的优点就是,union过程不用遍历数组所有元素,直接替换一个id即可。缺点是find过程比较复杂,假如一棵大树的根连接到了一棵小树上,查找大树的某一个节点与小树的关系时,需要while循环走很久才能找到。

于是出现了加权树算法,无论怎么union,使得小树永远连在大叔上,减少find的难度。

2.3.Weighted-Union-Find加权树算法

原理很简单,增加一个数组用来记录每棵树的节点数,节点少的(小树)不能作为节点多的树的根。
代码如下:
在int *id;下一行增加

    int *id;
    int *sz;//动态数组sz,用以记录树的节点数

在构造函数中增加

     sz=new int[n];
     for(int i=0;i<n;i++)
     {
        id[i]=i;
        sz[i] = 1;//开始时所有的链条节点都为1(都只有1个触点)
     }

同样地,析构函数中

    ~UF()
    {
        delete [] id;
        delete [] sz;
    }

关键代码中的unionn函数改为

void unionn(int p, int q)
        {
            int pID=find(p);
            int qID=find(q);
            if(qID==pID)    return;
            if (sz[pID] < sz[qID])
            {    //通过节点数量,判断树的大小并将小树的根并到大树的根下
                id[pID] = qID;
                sz[qID] += sz[pID];//将两棵树的节点数相加得到新的节点
            } 
            else 
            {
                id[qID] = pID;
                sz[pID] += sz[qID];
            }
            count--;
        }

查找函数find不变,同样是根据回溯根来查找连接关系。

同样以刚才的最简单例子说明:连接触点2和触点0时,因为触点2所在的树刚才已经连接了触点1,节点比较多,因此,触点0的id会跟着触点2变,最后一步的id将会是2,2,2,这样的话,无论是从find(1)还是find(0)都会一步查找到2,两者相等满足connected已连接的条件。加权树算法减少了find的难度,又更快一点。

总结

1、Union-Find问题的应用领域很广,一步一步从quick-find到加权树算法的优化也能让人对算法的优越性产生直观的认识,可以说是很重要的一课了。
2、Union-Find是用来处理一堆数据中的元素两两连接,最后形成的关系网是什么样子的问题的一种模型。
3、quick-find算法是一种最基本的解决方案,原理是为每一个元素(这里叫触点)设置一个标识符id用来表示连接状况,每当两个触点连接时,其中一个触点所连接的所有触点的id都修改为另一个的id。这样查找两个触点对应的id就能快速得知这两个触点是否连接。但是缺点也很明显,每次需要修改的id太多。
4、quick-union算法是进一步的解决方案,原理是将所有触点看作一棵树的节点,每次连接时,只改变子节点的id与父节点一致,实现快速union,但是在find时,针对每个触点,都要追溯他的父节点一直到根部,如果两个触点同属一个根部,说明两个触点连接。
4、Weighted-Union-Find算法是quick-union算法的改进,因为quick-union可能会遇到大树并到小树上的情况,增加树的深度,使得每次find时要追溯很久,极端情况下会完全遍历一遍。而加权树则是增加了一个数组用来记录每棵树的节点数目,使小树永远并到大树上,大大减小了树的深度,提高效率。

–本文仅供个人学习笔记之用,如有错误望请指正,谢谢!

猜你喜欢

转载自blog.csdn.net/sssaaaannnddd/article/details/78203370