并查集知识点



以为自己已经会用并查集了呢,碰到了一个带全路径并查集的问题,发现自己居然都没太理解具体思想是什么

啊渣渣



一、举个例子

在讲并查集之前我们先举一个例子。(这个例子是出自别人之手,大都知道的例子)
话说江湖上散落着各式各样的大侠,有上千个之多。他们没有什么正当职业,整天背着剑在外面走来走去,碰到和自己不是一路人的,就免不了要打一架。但大侠们有一个优点就是讲义气,绝对不打自己的朋友。而且他们信奉“朋友的朋友就是我的朋友”,只要是能通过朋友关系串联起来的,不管拐了多少个弯,都认为是自己人。这样一来,江湖上就形成了一个一个的群落,通过两两之间的朋友关系串联起来。而不在同一个群落的人,无论如何都无法通过朋友关系连起来,于是就可以放心往死了打。但是两个原本互不相识的人,如何判断是否属于一个朋友圈呢?
我们可以在每个朋友圈内推举出一个比较有名望的人,作为该圈子的代表人物,这样,每个圈子就可以这样命名“齐达内朋友之队”“罗纳尔多朋友之队”……两人只要互相对一下自己的队长是不是同一个人,就可以确定敌友关系了。
但是还有问题啊,大侠们只知道自己直接的朋友是谁,很多人压根就不认识队长,要判断自己的队长是谁,只能漫无目的的通过朋友的朋友关系问下去:“你是不是队长?你是不是队长?”这样一来,队长面子上挂不住了,而且效率太低,还有可能陷入无限循环中。于是队长下令,重新组队。队内所有人实行分等级制度,形成树状结构,我队长就是根节点,下面分别是二级队员、三级队员。每个人只要记住自己的上级是谁就行了。遇到判断敌友的时候,只要一层层向上问,直到最高层,就可以在短时间内确定队长是谁了。由于我们关心的只是两个人之间是否连通,至于他们是如何连通的,以及每个圈子内部的结构是怎样的,甚至队长是谁,并不重要。所以我们可以放任队长随意重新组队,只要不搞错敌友关系就好了。于是,门派产生了。

这里写图片描述

 

二、引入并查集

1、主要函数以及数组定义

用到的数组为 pre[MAX],用来存 i 的上级(即父亲节点)

(1)、初始化函数

int fat[N];

void init(int n)
{
    for(int i=1; i<=n; i++) 
        fat[i]=i;//初始化每个人的上级都是自己
}

 (2)、查找祖先函数 —— 朴素做法(适合数据量较小的情况)

               1)、循环版

int find(int x)           //查找x的掌门
{
    int root = x;      
    while(pre[root] != root)   //如果root 的上级不是root 自己(也就是说找到的大侠不是掌门)
        root = pre[root];      //root 就接着找他的上级,直到找到掌门为止
    return root;               //返回掌门
}

               2)、递归版

// 此递归版的利用了路径压缩

int find(int x)
{
    if(x != pre[x]) 
        pre[x] = find(pre[x]);
    return pre[x];
}

(3)、合并函数 —— 意思可以理解为两个点之间连一条线,这样他们原先所在的板块的所有点就都可以互通了

void UNION(int a,int b)       // 想让a与b做朋友
{
    int root = find(a);       // 然后去找他们的上级去判断他们的上级是不是一个人
    int root1 = find(b);
    if(root != root1)         // 如果上级不是一个人,则让两人属于一个掌门
        pre[root] = root1;
}

2、提升—— 引入路径压缩与按秩合并

(1)路径压缩:就是在递归找到根节点的时候,把当前节点到根节点间所有节点的父节点都设置为根节点。

         (解决了树深度很大的情况,路径压缩之后查找节省了时间)

è¿éåå¾çæè¿°

 

      1)循环版:   “两步一跳”

// 路径压缩的意思就是让 x 与 根节点 之间的结点的父亲节点都设置为根节点
// 可以理解为       两步一跳

int find(int x)
{
    int root = x,temp = x,t;
    
    while(root != pre[root])      // 首先找到根节点 root
        root = pre[root];
    
    while(temp != root){          // 还没到根节点之前
        t = pre[temp];            // 用 t 记录下 temp 的父亲节点
        pre[temp] = root;         // t 与根节点相连
        temp = t;                 // 继续下一次两步一跳,将这次的 t 传给 temp
    } 
    return root;
}

       2)递归版:

int find(int x)
{
    if(x != pre[x])         // 只要不是根节点,就把父亲节点指向父亲节点的父亲节点
        pre[x] = find(pre[x]);
    return pre[x];
}

 

(2)、按秩合并:就是将短的连到长的上树枝上去

             该方法使用秩来表示树高度的上界,在合并时,总是将具有较小秩的树根指向具有较大秩的树根。

             简单的说,就是总是将比较矮的树作为子树,添加到较高的树中。

这里写图片描述

void UNION(int a,int b)
{
    int root = find(a);
    int root1 = find(b);
    if(root == root1)
        return ;
        
    // a 的深度比 b 的深度大,所以将 b 的父亲节点设置为 a 的父亲节点
    if(rank[root] > rank[root1])  
        pre[root1] = root; 
    else{
        if(rank[root] == rank[root1]) // 如果深度相同的话加入一个之后,深度增加一个
            rank[root1]++;
        pre[root] = root1; 
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值