当前位置: 首页 > news >正文

【高阶数据结构】并查集的实现(含压缩路径)及其应用-C++版本

并查集原理

并查集,在一些有N个元素的集合应用问题中,我们通常是在开始时让每个元素构成一个单元素的集合,然后按一定顺序将属于同一组的元素所在的集合合并,其间要反复查找一个元素在哪个集合中

并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。常常在使用中以森林来表示

适合于描述这类问题的抽象数据类型称为并查集(union-findset)


小例子-编号和人

如果我们想让编号和人构成关系, 即可以通过编号找到对应的人,也可以通过人找到对应的编号怎么做?

template<class T>  
class UnionFindSet
{
public:
	UnionFindSet(const T* a, size_t n)	
	{
		for (int i = 0; i < n; i++)
		{
			_a.push_back(a[i]);
			_indexMap[a[i]] = i;
		}
	}
private:
	vector<T> _a;  //编号找人  下标对应的就是人名
	map<T, int> _indexMap;//人找编号  key:人名   value:编号
};

#include"UnionFind.h"
int main()
{
	string a[] = { "张三","李四","王五" };
	UnionFindSet<string> ufs(a, 3);
	return 0;
}

image-20220720141711664


并查集的实现

#include<iostream>
#include<vector>
class UnionFindSet
{
public:
    UnionFindSet(size_t n);
    size_t FindRoot(int x);
    void Union(int x1, int x2);
    bool Inset(int x1,int x2);
    size_t SetCount();
private:
	std::vector<int> _set;
};

首先我们要知道如下规则:

  1. 数组的下标对应集合中元素的编号
  2. 数组中的值如果为负数,负号代表根,数字代表该集合中元素个数
  3. 数组中的值如果为非负数,代表该元素双亲在数组中的下标

例如:

image-20220519202323686


基本结构

class UnionFindSet
{

private:
	std::vector<int> _set;
};

构造函数

最初每个元素各自构成一个集合, 初始状态就是-1,表示size个集合

image-20220519201613340

UnionFindSet(int size)
    :_set(size, -1)//size个元素都最初初始化为-1
    {}

查找根节点

image-20220519203308754

返回根节点所在下标

//如果_set[x]是负数,那么x就是某个集合的根
//如果_set[x]不是负数,那么x的父亲节点就是_set[x]值对应的位置
size_t FindRoot(int x)
{
    while (_set[x] >= 0)
    {
        x = _set[x];
    }
    //来到这,_set[x] <0, 此时x就是根节点
    return x;
}

路径压缩技巧

路径压缩实际上是在数据量太大的时候,访问一些数据可能在位于叶子位置,导致访问的效率不高

本质上我们查找的代表节点的时候,可以对该路径上的节点进行路径压缩

注意:最好不要用递归的时候进行路径压缩,因为路径深可能导致栈溢出

image-20220810110024770


做法:

1)先找到当前路径节点的代表节点

2)然后从当前位置_set[x]开始的节点,沿途的节点的父亲位置都改为代表节点的位置

只需要根据下标关系往上迭代即可x节点的父亲位置就是_set[x]位置

注意:要提前保存x节点的父亲节点位置,因为更改当前位置的父亲节点, 会丢失以前的父亲节点

size_t FindRoot(int x)
{
    int root = x;
    while (_set[root] >= 0)
    {
        root = _set[root];
    }
    //此时root就是x这条路径的头节点(代表节点)
    //把沿途的节点的父亲都改为root
    while (_set[x] >= 0)
    {
        int parent = _set[x];//先保存x节点的父节点位置
        _set[x] = root;//将x节点的父亲位置改为root位置
        x = parent;//往上迭代
    }
    return root;
}

合并集合

image-20220519202238913


这里选择合并到x2所在的集合合并到x1所在的集合

void Union(int x1, int x2)
{
    //找到x1和x2的代表节点(根节点)
    size_t root1 = FindRoot(x1);
    size_t root2 = FindRoot(x2);

    //两个节点不在一个集合才合并
    //把root2合并到root1所在集合
    if (root1 != root2)
    {
        _set[root1] += _set[root2];//root2所在集合的元素累加到root1所在集合
        _set[root2] = root1; //root2的父亲变为root1
    }
}

如果我们希望是小集合合并到大集合呢? 即元素少的集合合并到元素多的集合

路径压缩技巧

两颗树合并的时候,节点少的数往节点多的树合并

目的:为了使节点层数增多的节点相对减少


做法:

1)判断哪颗树的节点更多, 让root1变为是较大的集合, root2往root1合并

如何判断呢? 因为root1和root2是根,_set[root]的值是负数,代表该集合的元素个数, _set[root]值越大,集合元素个数越少

//x1和x2合并 ->本质是代表节点(根节点)合并
void Union(int x1, int x2)
{
    //找到x1和x2的代表节点(根节点)
    size_t root1 = FindRoot(x1);
    size_t root2 = FindRoot(x2);
    //我们让root1的集合是大集合,小集合合并到大集合中
    //因为root1和root2是根,所以_set[root]的值为负数,代表集合的元素个数,值越小,元素越多

    if (_set[root1] > _set[root2]) //说明root2集合元素多
    {
        ::swap(root1, root2);
    }
    //两个节点不在一个集合才合并
    if (root1 != root2)
    {
        _set[root1] += _set[root2];//root2所在集合的元素累加到root1所在集合
        _set[root2] = root1; //root2的父亲变为root1
    }
}

求集合个数

遍历_set,查看有多少元素是负数,就代表有多少个根节点, 即有多少个集合

size_t SetCount()
{
    size_t count = 0;
    for (auto e : _set)
    {
        if (e < 0) count++;
    }
    return count;
}

判断两个元素是否在同一个集合

只需要判断两个元素的代表节点的位置是否相同即可

bool Inset(int x1,int x2)
{
    return FindRoot(x1) == FindRoot(x2);
}

并查集的应用

省份数量

https://leetcode.cn/problems/number-of-provinces/

image-20220720144559026

做法:遍历矩阵, 因为n[i][j] =1 表示第i城市和第j个城市相连 -> 所以我们可以把i和j合并到一个集合里面

遍历完成后,返回集合个数

class Solution {
public:
    class UnionSet
    {
    public:
        UnionSet(size_t n = 0)
        {
            //最初自己就是一个集合
            us.resize(n, -1);//开辟n个空间,初始化为-1
        }
        
        //查找x对应的根节点下标
        int FindRoot(int x)
        {
            //下标对应的值为负数的就是根节点
            //要保证x下标的合法性
            assert(x < us.size());
            while (us[x] >= 0)
            {
                x = us[x];
            }
            //退出循环时:us[x]<0,此时x下标就是对应的根节点
            return x;
        }

        //求集合个数->看us中有多少个元素是<0的
        int GetSize()
        {
            int count = 0;
            for (auto& x : us)
            {
                if (x < 0) count++;
            }
            return count;
        }

        //小集合合并到大集合
        void Union(int x1, int x2)
        {
            //要保证x1和x2下标的合法性
            assert(x1 < us.size());
            assert(x2 < us.size());
            //先找到二者对应的根节点所在下标
            int root1 = FindRoot(x1);
            int root2 = FindRoot(x2);

            //让root1是大集合的根
            //因为负数是表示元素个数,us[root1]>us[root2] 表示root2的集合元素个数比root1多
            if(us[root1]<us[root2])
            {
                swap(root1,root2);
            }
            
            //如果root1 == root2,说明x1和x2就是在一个集合中,不需要合并
            if (root1 != root2)
            {
                //将root2所在集合的个数累加到root1中
                //root2的父亲的下标变为root1
                us[root1] += us[root2];
                us[root2] = root1;
            }
        }
    private:
        vector<int> us;
    };
    int findCircleNum(vector<vector<int>>& isConnected) {
        //相连的省份他们都在一个集合里,所以只需要用并查集统计一共有几个集合即可
        int n = isConnected.size();
        //初始化每一个集合! {0},{1},.....
        UnionSet us(n);        
        //因为上下对称只需要考察上三角形即可
        for(int i = 0;i<n;i++)
        {
            for(int j = i+1;j<n;j++)
            {
                //当前位置为1,就把j和i位置合并
                if(isConnected[i][j] == 1)
                {
                    us.Union(i,j);
                }
            }
        }
        //最后返回并查集中有多少个元素
        return us.GetSize();
    }
};

当然,我们不必把整个并查集实现出来, 可以采取下面的方式做: 手动控制并查集

class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        vector<int> ufs(isConnected.size(),-1);//每个元素各为一个集合,初始化为-1
        //查找根节点 
        auto findRoot = [&ufs](int x)	//Lamber表达式
        {
            while(ufs[x]>=0) 
                x = ufs[x];
            return x;
        };

        //遍历矩阵
        for(size_t i = 0;i<isConnected.size();i++)
        {
            for(size_t j = 0;j<isConnected[i].size();j++)
            {
                if(isConnected[i][j] == 1)
                {
                    //合并集合
                    int root1 = findRoot(i);
                    int root2 = findRoot(j);
                    if(root1 != root2) //合并到root1上
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }
                }
            }
        }
        //统计集合个数
        int count = 0;
        for(auto e:ufs)
        {
            if(e<0) count++;
        }
        return count;
    }
};

等式方程的可满足性

https://leetcode.cn/problems/satisfiability-of-equality-equations/

image-20220720150009485


函数参数是:vector<string>& equations 每一个元素是一条长度为4的方程

image-20220720152942963

怎么做呢? 可以根据str[1]的字符判断这条方程是!= 还是 ==

  • 相等的值放到一个集合中, 例如: a ==b b == c,那么a,b,c就在一个集合中!

  • 不相等的值不能在一个集合中, 所以当我们遍历到 '!'字符时.判断左右两个字符是否在一个集合,如果在,就是相悖的,返回false!


方法:先遍历一遍字符串,把相等的值放到一个集合,然后再遍历一遍集合,看不相等的值是否在一个集合,如果在,就是相悖的,返回false 因为只有小写字母.所以可以只开辟26个空间进行映射!

等式左侧的字符str[0]映射为: str[0] - 'a' 等式右侧的字符str[3]映射为:str[3] - 'a'


class Solution {
public:
    class UnionSet
    {
    public:
        UnionSet(size_t n = 0)
        {
            //最初自己就是一个集合
            us.resize(n, -1);//开辟n个空间,初始化为-1
        }
        
        //查找x对应的根节点下标
        int FindRoot(int x)
        {
            //下标对应的值为负数的就是根节点
            //要保证x下标的合法性
            assert(x < us.size());
            while (us[x] >= 0)
            {
                x = us[x];
            }
            //退出循环时:us[x]<0,此时x下标就是对应的根节点
            return x;
        }

        //求集合个数->看us中有多少个元素是<0的
        int GetSize()
        {
            int count = 0;
            for (auto& x : us)
            {
                if (x < 0) count++;
            }
            return count;
        }

        //将x2合并到x1
        void Union(int x1, int x2)
        {
            //要保证x1和x2下标的合法性
            assert(x1 < us.size());
            assert(x2 < us.size());
            //先找到二者对应的根节点所在下标
            int root1 = FindRoot(x1);
            int root2 = FindRoot(x2);
            //如果root1 == root2,说明x1和x2就是在一个集合中,不需要合并
            if (root1 != root2)
            {
                //将root2所在集合的个数累加到root1中
                //root2的父亲的下标变为root1
                us[root1] += us[root2];
                us[root2] = root1;
            }
        }
    private:
        vector<int> us;
    };
    
    
    
    //如果两个字符相等,则放入同一个集合之中
    //如果两个字符不相等,它们应当在不同的集合中,此时查找它们所在集合的根
    //如果相同则说明这两个字符在同一个集合中,返回false。
    bool equationsPossible(vector<string>& equations) {
        UnionSet us(26);//只有小写字母->开26个空间足矣
        //第一遍先把相等的值合并到一个集合
        for(auto& str:equations)
        {
            if(str[1]=='=')
            {
                us.Union(str[0]-'a',str[3]-'a');
            }
        }
        //第二遍把不相等的值判断在不在一个集合,如果在就逻辑相悖,返回false
        for(auto& str:equations)
        {
            if(str[1]=='!')
            {
                if(us.FindRoot(str[0]-'a')==us.FindRoot(str[3]-'a'))
                {
                    return false;
                }
                
            }
        }
        return true;
    }
};

当然,我们不必把整个并查集实现出来, 可以采取下面的方式做: 手动控制并查集

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
         vector<int> ufs(26,-1);//只需要开26个空间映射
            auto findRoot = [&ufs](int x)	
            {
                while(ufs[x]>=0) 
                    x = ufs[x];

                return x;
            };
        
        //第一遍遍历,把相同的值合并到一个集合
        for(auto& str:equations)
        {
            if(str[1] == '=')
            {
                //合并两个集合
                int root1 = findRoot(str[0] -'a');//左字符的映射:str[0] -'a'
                int root2 = findRoot(str[3] -'a');//右字符的映射:str[3] -'a'
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }


        //第二遍遍历,判断不相等的是否在一个集合,如果在,就是相悖,返回false
        for(auto& str:equations)
        {
            if(str[1] == '!')
            {
                //合并两个集合
                int root1 = findRoot(str[0] -'a');//左字符的映射:str[0] -'a'
                int root2 = findRoot(str[3] -'a');//右字符的映射:str[3] -'a'
                if(root1 == root2)  //二者在一个集合中,相悖
                {
                    return false;
                }
            }
        }
        
        return true;//所有等式符合要求
    }
};

相关文章:

  • Java——线程不安全的原因(图解)
  • [数据结构]~双向+循环链表从(0~1)
  • 【开学季】再见大一,你好大二 | 完成自己的未完成
  • java毕业设计网站SSM版学生选课系统[包运行成功]
  • 【计算机网络】第六章:应用层
  • FS03MR12A6MA1LBBPSA1 1200V 400A 紧凑型 六单元模块
  • 系统篇: ubuntu 18.04 ROS1 和 ROS2 环境搭建
  • 贪心算法 - 买卖股票的最佳时机|| + 分割平衡字符串
  • ActiveReports.NET 16.2RPX 部分报告的完全支持
  • 专业英语第八章Communications and Networks测试题
  • 【Linux操作系统】-- 多线程(三)-- 线程池+单例模式+读写者模型
  • Pinia实操配置,Vuex的替代品
  • flume系列(一)部署示例及组件介绍
  • 【SpringBoot】静态资源导入探究
  • Redis cache-aside模型-分布式锁等问题研究
  • 【编码】-360实习笔试编程题(二)-2016.03.29
  • Debian下无root权限使用Python访问Oracle
  • HTTP传输编码增加了传输量,只为解决这一个问题 | 实用 HTTP
  • JavaScript 事件——“事件类型”中“HTML5事件”的注意要点
  • JavaScript实现分页效果
  • java取消线程实例
  • Linux学习笔记6-使用fdisk进行磁盘管理
  • vue 个人积累(使用工具,组件)
  • VuePress 静态网站生成
  • 编写符合Python风格的对象
  • 不用申请服务号就可以开发微信支付/支付宝/QQ钱包支付!附:直接可用的代码+demo...
  • 大主子表关联的性能优化方法
  • 简单数学运算程序(不定期更新)
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 猫头鹰的深夜翻译:Java 2D Graphics, 简单的仿射变换
  • 那些年我们用过的显示性能指标
  • 前端每日实战:61# 视频演示如何用纯 CSS 创作一只咖啡壶
  • 我建了一个叫Hello World的项目
  • Spark2.4.0源码分析之WorldCount 默认shuffling并行度为200(九) ...
  • ​io --- 处理流的核心工具​
  • ​马来语翻译中文去哪比较好?
  • ###51单片机学习(2)-----如何通过C语言运用延时函数设计LED流水灯
  • #{}和${}的区别?
  • #宝哥教你#查看jquery绑定的事件函数
  • $.ajax()方法详解
  • (C++)栈的链式存储结构(出栈、入栈、判空、遍历、销毁)(数据结构与算法)
  • (day 2)JavaScript学习笔记(基础之变量、常量和注释)
  • (四)JPA - JQPL 实现增删改查
  • (译) 函数式 JS #1:简介
  • (中等) HDU 4370 0 or 1,建模+Dijkstra。
  • (转)3D模板阴影原理
  • (转)socket Aio demo
  • .net core 源码_ASP.NET Core之Identity源码学习
  • .NET Core日志内容详解,详解不同日志级别的区别和有关日志记录的实用工具和第三方库详解与示例
  • .net/c# memcached 获取所有缓存键(keys)
  • .NET/C# 阻止屏幕关闭,阻止系统进入睡眠状态
  • .NET面试题(二)
  • .net知识和学习方法系列(二十一)CLR-枚举
  • // an array of int
  • @Query中countQuery的介绍