[数据结构高阶]并查集初识、手撕、可以解决哪类问题?

标题:[数据结构高阶]并查集初识、手撕、可以解决哪类问题?
@水墨不写bug



文章目录


一、认识并查集

一个考察队要去西部开发考察,这个考察队队员有10人,其中郑州4人,北京3人,西安3人。于是这三个城市的人由于生活习惯相似分别抱团,那么如何快速确定两个人是否属于同一个城市呢?并查集就可以。

并查集 :本质是一个森林(包含多棵树)。并且与堆类似,用下标表示关系。使用双亲表示法(只存储双亲)。
如何用数据结构表示并查集?

用一个线性结构(数组)表示。每一个位置都初始化为-1,表示10棵树:

假如0,6,7,8是郑州人1,4,9是北京人2,3,5是西安人 ,那么通过树形结构 表示(默认下标最小的0,1,2作为小队长- - -根节点):

如果有新成员加入?

数组对应的队员的值存储的是队长的下标,队长的值的绝对值表示包括队长在内的小队成员个数。队长的值原来为-1,一个队员加入小队,队长的值+=队员的值(-1)|如果队员是队长,则+=(-m),然后队员的值修改为队长的值。

在线性的数组上,需要明确:

  • 1.一个位置值是负数,那它就是数的根,这个负数的绝对值就是这棵树的数据个数。
  • 2.一个位置值是正数,那这个位置存的是双亲的下标。

那么上图对应的线性结构是:

如果两队伍合并?

由于研究工作需要,北京人和郑州人需要合并,那么在数据结构上如何表示?

其实,把北京人合并入郑州,把郑州人合并入北京,没有本质区别。

假如把北京人合并入郑州,那么对应的树形结构变成:

那么对应的线性结构:

这就是并查集的加入一个成员,一个小队 的操作。想要看两个队员是否在同一组 ,只需要不停找根,如果两个队员同根,那就在同一组。


二、模拟实现并查集

并查集可以通过一个STL的vector维护,具体实现思路已经在上文有所体现,于是在这里直接给出实现:

cpp 复制代码
#pragma once
#include<iostream>
#include<vector>
namespace ddsm
{
	class UnionFindSet
	{
	public:
		//根据并查集规则,初始化为-1
		UnionFindSet(int size):_ufs(size,-1){}
		//把两个集合【队伍】合并
		void Union(int x1,int x2)
		{
			int root1 = FindRoot(x1);
			int root2 = FindRoot(x2);
			if (root1 == root2)
				return;
			
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
		}
		//找到一个元素所在的集合
		int FindRoot(int x)
		{
			int parents = x;
			while (_ufs[parents] >= 0)
				parents = _ufs[parents];

			return parents;
		}
		//判断两个元素是否在同一个集合
		bool InSameSet(int x1, int x2)
		{
			return FindRoot(x1) == FindRoot(x2);
		}
		//返回集合的个数
		size_t Size()
		{
			size_t size = 0;
			for (int i = 0; i < _ufs.size(); ++i)
			{
				if (_ufs[i] < 0)
					size++;
			}
			return size;
		}
	private:
		std::vector<int> _ufs;
	};
};

三、用并查集解决问题

通过以上例子可知,并查集一般可以解决一下问题:

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

1、省份的数量

本题通过把同一省份的城市放在同一集合中,遍历所有城市链接情况,最后统计出省份(集合)的总数。


示例代码:

cpp 复制代码
class UnionFindSet
{
public:
	//根据并查集规则,初始化为-1
	UnionFindSet(int size):_ufs(size,-1){}
	//把两个集合【队伍】合并
	void Union(int x1,int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2)
			return;
		
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}
	//找到一个元素所在的集合
	int FindRoot(int x)
	{
		int parents = x;
		while (_ufs[parents] >= 0)
			parents = _ufs[parents];

		return parents;
	}
	//判断两个元素是否在同一个集合
	bool InSameSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}
	//返回集合的个数
	size_t Size()
	{
		size_t size = 0;
		for (int i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
				size++;
		}
		return size;
	}
private:
	std::vector<int> _ufs;
};
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        UnionFindSet ufs(isConnected.size());
        for(int i = 0;i < isConnected.size();++i)
        {
            for(int j = 0;j < isConnected[i].size();++j)
            {
                if(isConnected[i][j] == 1)
                    ufs.Union(i,j);
            }
        }
        return ufs.Size();
    }
};

但是每次都手撕一个并查集,还是麻烦,那就面向过程的来维护并查集ufs,代码如下:

cpp 复制代码
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        vector<int> ufs(isConnected.size(),-1);
        auto findroot = [&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[i].size();++j)
            {
                if(isConnected[i][j] == 1)
                {
                    int rooti = findroot(i);
                    int rootj = findroot(j);
                    if(rooti == rootj)
                        continue;
                    else
                    {
                        ufs[rooti] += ufs[rootj];
                        ufs[rootj] = rooti;
                    }
                }
            }
        }
        int ret = 0;
        for(int i = 0;i < ufs.size();++i)
            if(ufs[i] < 0)
                ++ret;
        return ret;
    }
};

2、等式方程的可满足性

分两遍:

第一遍设置集合关系,哪一个元素属于哪一个集合先划分好;

第二遍查找相悖,有相悖,返回false;否则true。

cpp 复制代码
class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        vector<int> ufs(26,-1);
        auto findroot = [&ufs](int x){while(ufs[x] >= 0)x = ufs[x];return x;};
        //第一遍先设置集合关系
        for(int i = 0;i < equations.size();++i)
        {
            if(equations[i][1] == '=')
            {
                int root1 = findroot(equations[i][0] - 'a');
                int root2 = findroot(equations[i][3] - 'a');
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }
        //第二次再查找相悖关系
        for(int i = 0;i < equations.size();++i)
        {
            if(equations[i][1] == '!')
            {
                int root1 = findroot(equations[i][0] - 'a');
                int root2 = findroot(equations[i][3] - 'a');
                if(root1 == root2)
                {
                    return false;
                }
            }
        }
        //没有相悖返回真
        return true;
    }
};

四、并查集的路径压缩

随着一棵树的高度增加,并查集的查找效率逐渐降低。路径压缩目的是减小树的高度,进而提高并查集查找效率。
路径压缩一般来说有两种思路:

**第一种:**每次查找一个位置,吧一个节点的位置直接更新到根。

**第二种:**每查找一个位置,把查找路径上的节点位置都更新到根。

到这里并查集结束了,并查集是为了给图做铺垫。


完~
转载请注明出处

相关推荐
jiunian_cn1 小时前
【c++】异常详解
java·开发语言·数据结构·c++·算法·visual studio
康康这名还挺多3 小时前
鸿蒙HarmonyOS list优化一: list 结合 lazyforeach用法
数据结构·list·harmonyos·lazyforeach
Helibo443 小时前
GESPC++六级复习
java·数据结构·算法
EnticE1523 小时前
[高阶数据结构]二叉树经典面试题
数据结构·算法·面试
_星辰大海乀4 小时前
数据库约束
java·数据结构·数据库·sql·链表
爱喝茶的小茶5 小时前
构造+简单树状
数据结构·算法
悦悦子a啊5 小时前
PTA:jmu-ds-最短路径
c++·算法·图论
小王努力学编程5 小时前
高并发内存池(三):TLS无锁访问以及Central Cache结构设计
jvm·数据结构·c++·学习
草莓啵啵~6 小时前
数据结构--二叉树
数据结构