并查集与十度好友

一、问题简介

    首先说下问题吧,这里就不做广告了,在社交网络中,假设A和B是好友,B和C是好友,如果A和C不是好友,那么就说C是A的二度好友,在一个有10万人人际网络中,如何在时间O(n)时间里,找到某个人的十度好友。

    查了下网上人们对这个问题解法的思路,大致有两种观点:(1)BFS;(2)并查集。对BFS解法的解释挺多,思路都比较相近,现总结如下:

    (1)设要查找十度好友的人为A,则首先通过BFS对A的邻接好友进行遍历,并定义一个变量degree记录当前BFS的深度;

    (2)设置一个集合prefriend用来记录在BFS过程中A的一度至九度的所有好友集合prefriend,然后找到十度好友后要剔除那些存在于prefriend中的好友,设置prefriend的原因是集合中如果有环,则会出现如果X是A的三度好友,同时X也有可能是A的十度好友;

    (3)当degree==10时,从当前BFS的集合中再剔除prefriend集合中的好友,则剩下的好友便为A的十度好友了。

    采用BFS时间复杂度是BFS的复杂度O(V+E)。当集合中边很多时,可能复杂度接近于二次复杂度。

    网上对并查集的思路仅仅是提笔带过,这里较详细的介绍下并查集的基本概念和对此问题解法的思路,体现下并查集这一单纯又强大的算法的神奇。

二、并查集

    一种简单的用途广泛的集合. 并查集是若干个不相交集合,能够实现较快的合并和判断元素所在集合的操作,应用很多,如其求无向图的连通分量个数等。最完美的应用当属:实现Kruskar算法求最小生成树。

    实现并查集主要实现其三个基本操作:

    (1)Make_Set(x) 把每一个元素初始化为一个集合,初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身。

    (2)Find_Set(x) 查找一个元素所在的集合,查找一个元素所在的集合,即找到这个元素所在集合的祖先!这个才是并查集判断和合并的依据。判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。

    (3)Union(x,y) 合并x,y所在的两个集合,合并两个集合,也就是使一个集合的祖先成为另一个集合的祖先,具体见下图所示:


三、并查集的优化

    (1)Find_Set(x)时 路径压缩

    寻找祖先时我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度,这里我们采用路径压缩,即当我们经过递归地寻找祖先节点时,在"回溯"的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示;可见,路径压缩方便了以后的查找。


    (2)Union(x,y)时 按秩合并

    即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。

四、代码实现

int father[MAX];   /* father[x]表示x的父节点*/
int rank[MAX];     /* rank[x]表示x的秩*/
 
/* 初始化集合*/
 
void Make_Set(int x)
{
    father[x] = x; //根据实际情况指定的父节点可变化
    rank[x] = 0;   //根据实际情况初始化秩也有所变化
}
 
/* 查找x元素所在的集合,回溯时压缩路径*/
 
int Find_Set(int x)
{
    if (x != father[x])
    {
        father[x] = Find_Set(father[x]); //这个回溯时的压缩路径是精华
    }
    return father[x];
}
 
/*
按秩合并x,y所在的集合
下面的那个if else结构不是绝对的,具体<strong>根据实际情况</strong>变化
但是,宗旨是不变的即,按秩合并,实时更新秩。
*/
 
void Union(int x, int y)
{
    x = Find_Set(x);
    y = Find_Set(y);
    if (x == y) return;
    if (rank[x] > rank[y])
    {
        father[y] = x;
        rank[x] += rank[y];
    }else
    {
        if (rank[x] == rank[y])
        {
            rank[y]++;
        }
        father[x] = y;
    }
}
    注意:代码中路径压缩时秩不需变化的,秩只是表示节点高度的一个上界,如果用秩进行计数,路径压缩也是不需要变化的,因为所属集合的根节点的秩在合并时已经更新,其他子节点的秩不用到也无需再变化;在某些应用中,可根据问题的不同来定义表示不同含义的数据体现集合的大小等属性。

五、并查集解决十度好友问题

    下面说下使用并查集解决十度好友问题的基本思路:

    (1)设数据集合中的数据时以邻接表的形式存储的,如1->2->4->6->7,表示编号1的好友的邻接表,以此类推;

    (2)同样设要查找十度好友的人为A,合并A的一度好友,即1、2、4、6、7,这些好友的根节点都相同,为了便于计算,我们应该维护一条记录,记录每次新加入集合S的节点集合NFriend(NFriend中每个点其实就是第N度好友的集合);

    (2)针对NS中每个点的好友集合进行合并,当然合并的过程中只需对要合并的点i进行判断findSet(i)是否属于集合S,如果属于,则不用加入集合NFriend了;

    (3)最后十步合并后,NFriend便是所求的集合。

发布了61 篇原创文章 · 获赞 19 · 访问量 7万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章