数据结构<6> 并查集

本文详细介绍了并查集的概念、原理和两种优化策略——路径压缩和按秩合并,以及如何使用并查集解决实际问题。通过代码实现展示了并查集在数组和单词映射场景下的应用,并提供了两个实战题目示例,帮助读者理解并查集在合并集合和查询元素所属集合中的应用。
摘要由CSDN通过智能技术生成

并查集原理

先看一个场景把,现在有十个人坐火车,这十个人一开始是来自三个地区的互不认识的。但是通过交流他们来自相同地区的人就凑到了一起。假设三人来自成都,第三人来自北京,四人来西安。

所以现在这十个人的状态从每个人都是独立的集合,合并成了三个集合。
现在给出操作就是:1,查询某两个人是不是属于一个集合的。2,将a同学和b同学所在的集合合并。3,查询一个集合中有多少人。

上述的这种情况我们就可以使用并查集来解决,并且在合并集合,查询两个人是不是在同一个集合,这两种操作上的时间复杂度接近O(1)

首先上面的问题我们可以使用朴素算法来接近,就是开一个和人数相同的数组,然后每个人对应一个位置,这个位置存放的数就是代表这个人属于那个集合。
这时候我们查询的时间复杂度是O(1),但是合并两个集合的时候时间复杂度就是O(N)了需要遍历整个数组。

下面来讲一下并查集的原理和如何接近上述问题。
首先并查集是通过一个数组的形式来组织的。
用树的方式维护每一个集合。
数组里面的每个节点位置都是存放他的根节点。
在这里插入图片描述

数组初始化全都是-1,其实也可以初始化成自己的下标。都是可以的,初始化成-1,有一个好处就是-1可以当作记录额外的信息。比如abs(-1)就是集合里面的元素个数,可以通过这个来维护
在这里插入图片描述

现在着些人合并成了三个集合。此时他们的数组表示形式就是下面这种情况。
在这里插入图片描述

总结:

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

并查集代码实现1

class UnionFindSet
{
public:
	UnionFindSet(int n)
		:_ufs(n,-1)
	{}
	
	int find(int x)
	{
		int root = x;
		while (_ufs[root] >= 0) root = _ufs[root];
		//路径压缩
		while (_ufs[x] >= 0)
		{
			int p = _ufs[x];
			_ufs[x] = root;
			x = p;
		}
		return root;
	}

	void Union(int a, int b)
	{
		int root1 = find(a);
		int root2 = find(b);

		if (root1 == root2)
			return;
		//按秩合并
		if (_ufs[root2] < _ufs[root1])
			swap(root1, root2);
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	bool InOneSet(int a, int b)
	{
		return find(a) == find(b);
	}

	int size()const
	{
		int count = 0;
		for (auto item : _ufs)
			if (item <= 0)
				count++;
		return count;
	}

	int Usize(int a)
	{
		return abs(_ufs[find(a)]);
	}


private:
	vector<int> _ufs;//元素存放到数组里面
	//unordered_map<T, int> _IndexMap;//下标与元素的映射
};

这里使用的并查集的写法初始情况是全都置为-1的。这个并查集不仅仅可以用来合并结合,查询两元素是不是一个集合,而且还可以得到这一个集合里面有多少个元素。
就是数组里面存放的数字就是该集合的元素个数。

并查集优化

并查集这里有两个优化方式:1,路径压缩,2,按秩合并。
常用的是第一种,我在上面代码中也写出了。

路径压缩一般是与find查找节点的根节点融合在一起,将要查找的x节点一路到根节点的节点的父亲都置为根节点。

路径优化的情景,当数据量过大的时候,可能会出现并查集的一棵树深度过深导致效率下降。所以每一次查找节点的时候都进行路径压缩,下次查找的效率机会变得很高。
在这里插入图片描述

这里的绿线是查找x节点之前的链接方式,蓝线是查找完成并且有路径压缩之后的连接情况。
可能会导致单次查找的效率下降,但是大大增加了后序查找的效率。

按秩合并,意思就是说,要合并两个集合的时候,将元素个数较小的那个结合合并到元素个数较大的那个集合当中去。这种合并方式可以降低树整体的高度。
比如:原来元素少的a集合有1001个点,有两层,元素多的b集合有2001个点,也是两层。a合并到b的时候有1000个节点的高度+1。但是在b集合合并到a集合的时候就会发现,此时2000个点的高度+1,因此将节点少的集合,合并到节点多的集合有利于降低查找的时间复杂度。

并查集代码实现2

上面写的并查集是针对数字元素或者数字编号的。
下面我们写一个用单词来对应的并查集,实际也很简单,只需要增加一个unordered_map将下标与单词映射在一起,就可以通过单词查找这个单词在数组中的下标。

	template<class T>
	class UnionFindSet
	{
	public:
		UnionFindSet(int n , const vector<T>& x)
			:_ufs(n, -1)
		{
			for (int i = 0; i < n; i++)
			{
				_IndexMap[x[i]] = i;//建立映射关系
			}
		}

		int FindIndex(const T& x)
		{
			auto it = _IndexMap.find(x);
			if (it == _IndexMap.end())
				return -1;
			return it->second;
		}

		int find(const T& x)
		{
			int root = FindIndex(x);
			while (_ufs[root] >= 0) root = _ufs[root];
			//路径压缩
			int xi = FindIndex(x);
			while (_ufs[xi] >= 0)
			{
				int p = _ufs[xi];
				_ufs[xi] = root;
				xi = p;
			}
			return root;
		}

		void Union(const T& a, const T& b)
		{
			int root1 = find(a);
			int root2 = find(b);

			if (root1 == root2)
				return;
			//按秩合并
			if (_ufs[root2] < _ufs[root1])
				swap(root1, root2);
			_ufs[root1] += _ufs[root2];
			_ufs[root2] = root1;
		}

		int Usize(const T& a)
		{
			return abs(_ufs[find(a)]);
		}

		bool InOneSet(const T& a, const T& b)
		{
			return find(a) == find(b);
		}

		int size()const
		{
			int count = 0;
			for (auto item : _ufs)
				if (item <= 0)
					count++;
			return count;
		}

	private:
		vector<int> _ufs;//元素存放到数组里面
		unordered_map<T, int> _IndexMap;//下标与元素的映射
	};

基本思路上都是一样的,就是多了一层映射的关系。

数组实现并查集

在有些比赛中,可能使用STL会比较慢,使用数组比较简单,所以这里提供了数组实现版本。

const int N = 100010;//根据数据范围
int ufs[N]; //存储每个点的父节点

    // 返回x的根节点
    int find(int x)
    {
        if (ufs[x] != x) ufs[x] = find(ufs[x]);
        return ufs[x];
    }
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ ) ufs[i] = i;

    // 将a集合,合并到b集合
    usf[find(a)] = find(b);

这里是使用递归的方式进行路径压缩,此时ufs初始化的值不是-1,而是i才可以。如果初始化时全是-1,那么路径压缩就用循环。
这里因为我们初始化用的是i,所以,数组存储的额外信息,集合的个数就没有了。此时如果我们想要使用这个信息,需要新开一个Size数组配合并查集记录元素的个数。

const int N = 100010;//根据题目数据范围拟定
int ufs[N],size[N] //存储每个点的父节点

    // 返回x的根节点
    int find(int x)
    {
        if (ufs[x] != x) ufs[x] = find(ufs[x]);
        return ufs[x];
    }
    // 初始化,假定节点编号是1~n
    for (int i = 1; i <= n; i ++ )
	{
		ufs[i] = i;
        size[i] = 1;//初始化size
    }
    // 将a集合,合并到b集合
 	size[find(b)] += size[find(a)];//合并集合之前先合并size
    usf[find(a)] = find(b);

并查集练习

下面通过两道题,来演示一下在做题中如何快速写出并查集接近问题,实际应用中用到最多的两个接口就是,将两个集合合并,查询两个元素是不是处于一个集合之内。根据题目要求看情况写。

剑指 Offer II 116. 省份数量
class Solution {
public:
    int findCircleNum(vector<vector<int>>& isConnected) {
        int sz = isConnected.size();
        vector<int> ufs(sz,-1);
        auto Find = [&ufs](int n){
            int root = n;
            while (ufs[root] >= 0)
            {
                root = ufs[root];
            }
            if (ufs[n] >= 0)
                ufs[n] = root;
            return root;
        };
        for(int i = 0;i<sz;i++)
        {
            for(int j = 0;j < isConnected[i].size(); j++)
            {
                if(isConnected[i][j] == 1)
                {
                    int root1 = Find(i);
                    int root2 = Find(j);
                    if(root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root1] = root2;
                    }
                }
            }
        }
        int count  = 0;
        for(auto e : ufs)
            if(e < 0)
            count++;
        return count;
    }
};

实际中只需要将多次使用的find函数独立出来即可,其他的直接在代码内实现就可以了。这里我是用的lambda表达式来代替函数。lambda实际就是一个类的匿名对象。这个类和less,greater的类相似,也就是和仿函数相似。通过重载了()来实现像调用函数一样的方式使用对象(可执行对象)

990. 等式方程的可满足性
class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        vector<int> ufs(26,-1);
        auto Find = [&ufs](int n)
        {
            int root = n;
            while (ufs[root] >= 0)
            {
                root = ufs[root];
            }
            if (ufs[n] >= 0)
                ufs[n] = root;
            return root;
        };
        
        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] == '!')
            {
                int root1 = Find(str[0] - 'a');
                int root2 = Find(str[3] - 'a');
                if(root1 == root2)
                    return false;
            }
        }
        return true;
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

KissKernel

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值