• 数据结构----并查集


    并查集概念

    并查集

    • 在一些应用问题中,需要将n个不同的元素划分成一些不相交的集合。开始时,每个元素自成一个单元素集合,然后按一定的规律将归于同一组元素的集合合并。在此过程中要反复用到查询某一个元素归属于那个集合的运算。适合于描述这类问题的抽象数据类型称为并查集(union-findset)

    例如有10个元素:
    用vector存储:
    在这里插入图片描述
    他们的关系用树形结构表示如下:
    在这里插入图片描述
    在vector中表示如下:
    在这里插入图片描述
    可见:

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

    即相当于将子节点的-1值加到父节点的值上


    若将8和4合并到一个集合,就是将他们的根节点合并,即:
    在这里插入图片描述
    其vector就变为:
    在这里插入图片描述

    并查集的模拟实现

    模拟实现

    并查集一般实现:

    1. 查找元素属于哪个集合:沿着数组表示树形关系以上一直找到根(即:树中中元素为负数的位置)
    2. 查看两个元素是否属于同一个集合:沿着数组表示的树形关系往上一直找到树的根,如果根相同表明在同一个集合,否则不在
    3. 将两个集合归并成一个集合:将两个集合中的元素合并,将一个集合名称改成另一个集合的名称
    4. 集合的个数:遍历数组,数组中元素为负数的个数即为集合的个数

    模拟实现:
    较简单(略)

    • 注意:两集合合并没有规定必须哪个做根(一般将高度较小的树并入高度较大的树,这样使最终的高度变小,提高平均查找效率)
    class UnionFindSet
    {
    public:
    	UnionFindSet(int size)
    		: _set(size, -1)
    	{}
    	size_t FindRoot(int x)//判断是否在一个集合,直接复用FindRoot即可
    	{//此处的x为给定的下标
    		while (_set[x] >= 0)
    		{
    			x = _set[x];
    		}
    		return x;//注意返回下标,如无根就返回本身(不是返回_set[x])
    	}
    	bool IsInSet(int x1, int x2)
    	{
    		return (FindRoot(x1) == FindRoot(x2));
    	}
    	void Union(int x1, int x2)
    	{
    		int root1 = FindRoot(x1);
    		int root2 = FindRoot(x2);
    		//这里可以加一层判断,将高度较小的树并入高度较大的树
    		if (root1 != root2)
    		{
    			_set[root1] += _set[root2];
    			_set[root2] = root1;
    		}
    	}
    	size_t SetCount()
    	{
    		size_t n = 0;
    		for (auto e : _set)
    			if (e < 0)
    				n++;
    		return n;
    	}
    private:
    	vector<int> _set;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    测试:在这里插入图片描述

    并查集优化

    Union优化

    把元素少的集合根节点指向元素多的根节点,能更高概率的生成一个层数比较低的树
    较简单,见:并查集 size 的优化


    在这里利用元素多的根节点,_set[root]的负值越大,abs()转换为正数进行比较

    void Union(int x1, int x2)
    {
    	int root1 = FindRoot(x1);//4->1
    	int root2 = FindRoot(x2);//8->0
    	if (root1 != root2)
    	{
    		if (abs(_set[root1])<abs(_set[root2]))
    			swap(root1, root2);
    		_set[root1] += _set[root2];
    		_set[root2] = root1;
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    压缩路径

    在Find的过程中递归进行路径压缩,找到根就返回,置为根的子节点,路径压缩为1
    一般考虑在过程中顺带进行压缩,不一定要一次压缩完成高度为1
    如果高度较短不需要压缩

    循环

    写法1:

    size_t FindRoot(int x)//判断是否在一个集合,直接复用FindRoot即可
    	{//此处的x为给定的下标
    		while (_set[x] >= 0)//不为根节点
    		{
    			if(_set[_set[x]]>=0)
    				_set[x] = _set[_set[x]];
    			x = _set[x];
    		}
    		return x;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述


    写法二:

    int FindRoot(int x)
    {
    	int root = x;
    	while (_set[root] >= 0)
    	{
    		root = _set[root];
    	}
    	// 路径压缩
    	while (_set[x] >= 0)
    	{
    		int parent = _set[x];
    		_set[x] = root;
    		x = parent;
    	}
    	return root;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    递归 (深度过深有栈溢出的风险)
    size_t FindRoot(int x)//判断是否在一个集合,直接复用FindRoot即可
    {//此处的x为给定的下标
    	if (_set[x] >= 0)//不为根节点
    	{
    		_set[x] = FindRoot(_set[x]);//在Find的过程中进行路径压缩,找到根就返回,置为根的子节点,路径压缩为1		
    		return _set[x];
    	}
    	else//为根节点
    		return x;//注意返回下标(不是返回_set[x])
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这里是引用

    并查集例题

    剑指 Offer II 116. 省份数量
    注意:不需要重写一个并查集,直接使用并查集思想

    class Solution {
    public:
       int findCircleNum(vector<vector<int>>& isConnected) {
           //不需要重写一个并查集,直接使用并查集思想
           vector<int> ufs(isConnected.size(), -1);
           auto find = [&ufs](int x)
           {
               while(ufs[x]>=0)
               {
                   x=ufs[x];
               }
               return x;
           };
           for(int i=0;i<isConnected.size();i++)
           {
               for(int j=0;j<isConnected[0].size();j++)
               {
                   if(isConnected[i][j] == 1)
                   {
                       int root1=find(i);
                       int root2=find(j);
                       if(root1!=root2)
                       {
                           ufs[root1]+=ufs[root2];
                           ufs[root2]=root1;
                       }
                   }
               }
           }
           int ret=0;
           for(auto &e:ufs)
           {
               if(e<0)
                   ret++;
           }
           return ret;
       }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    等式方程的可满足性
    并查集思想:按照==的表达式构建并查集,再用!=的表达式判断,相悖即false

    class Solution {
    public:
       bool equationsPossible(vector<string>& equations) {
           vector<int> ufs(26, -1);// 是26个小写字母
           auto find = [&ufs](int x)
           {
               while(ufs[x]>=0)
               {
                   x=ufs[x];
               }
               return x;
           };
           for(auto &str: equations)
           {
               if(str[1]=='=')//按照输入建立并查集(==表示集合关系)
               {
                   int root1 = find(str[0]-'a');
                   int root2 = find(str[3]-'a');
                   if(root1!=root2)//注意这里是!=
                   {
                       ufs[root1]+=ufs[root2];
                       ufs[root2]=root1;
                   }
               }
           }
           for(auto &str: equations)
           {
               if(str[1]=='!')//与输入相悖返回false 
               {
                   int root1 = find(str[0]-'a');
                   int root2 = find(str[3]-'a');
                   if(root1==root2)
                   {
                       return false;
                   }
               }
           }
           return true;
       }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
  • 相关阅读:
    华为云优惠券介绍、领取入口及使用教程
    非流式语音合成和流式语音合成
    解决RequestParam.value() was empty on parameter 0
    跟 AI 学 StarRocks:简介
    本宁顿学院青年作家奖报名备赛
    2024 年的 13 个 AI 趋势
    flutter系列之:做一个下载按钮的动画
    蓝桥杯入门即劝退(八)回文数
    (Python入门)函数
    Java设计模式之单例设计模式
  • 原文地址:https://blog.csdn.net/qq_41420788/article/details/126802803