数据结构-并查集

并查集原理   

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

  一般可以用数组来表示并查集,数据的下标就是每个数据的编号,对应的值如果是负数,那么就代表它自成一个集合,也就是一个根结点。如图,初始值一般都是负数,也就是每个元素都自成一个集合,如果一个元素是根,那么负数的绝对值则代表这个树有多少结点;如果不是负数,那么则说明这个元素不是根,且对应的值存的是父亲结点的下标。

  实际应用中也可以通过加入哈希表来设置更多的映射关系,这样的话可以将数组的下标作为key值,然后可以存人名这些。

  所以并查集其实也是多棵树组成的森林。

数据结构-并查集_第1张图片

用并查集表示就是

数据结构-并查集_第2张图片 

 元素之间可以合并,并且可以制定合并规则。集合与集合之间也可以合并。

数据结构-并查集_第3张图片

 综上,我们可以知道并查集可以解决以下问题:

1.查找某个元素属于哪个集合。沿着数组表示的树形关系向上可以一直找到根。

2.查看两个元素是否属于同一个集合。可以看找到的根是否相同,以此来判断是否属于同一个集合。

3.可以将两个集合归并成一个集合。

4.求集合的个数。其实也就是根的个数,可以遍历数组,记录下标为负数的元素即是集合的个数。

并查集简单实现

class UnionFindSet
{
public:
	UnionFindSet(int size)
		:_set(size, -1)
	{}
	~UnionFindSet()
	{}
	size_t FindRoot(int x)
	{
		while (_set[x] >= 0)
			x = _set[x];  // 向上找到根
		return x;
	}

	void Union(int x1, int x2)
	{
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);

		if (root1 != root2)  // 如果相等说明在一个集合内,没必要合并
		{
			_set[root1] += _set[root2];
			_set[root2] = root1;    // 这里默认合并到root1
		}
	}

	int SetCount()  // 找根结点个数
	{
		size_t count = 0;
		for (size_t i = 0; i < _set.size(); ++i)
		{
			if (_set[i] < 0)
				count++;
		}
		return count;
	}
private:
	std::vector _set;
};

void TestUFS()
{
	UnionFindSet u(10);

	u.Union(0, 6);
	u.Union(7, 6);
	u.Union(7, 8);

	u.Union(1, 4);
	u.Union(4, 9);

	u.Union(2, 3);
	u.Union(2, 5);

	std::cout << u.SetCount() << std::endl;
}

 另外说下压缩路径的问题,当并查集的数据非常大时,我们要找到这个元素的根,可能就需要向上找很久,有一种解决方案就是,在每次查找的时候,如果它的父亲结点不是根结点的话,就将它放到根结点的儿子结点。这样就能减少遍历的次数。

  不过压缩路径通常还是只有在数据量非常大的情况下使用。

并查集虽然是一种数据结构,但是有时候又可以是一种解决问题的思路。

数据结构-并查集_第4张图片

比如这道题,可以直接手搓一个简单的并查集,很容易就秒掉。

class Solution {
public:
    int findCircleNum(vector>& isConnected) {
        int n = isConnected.size();
        vector set(n,-1);
        auto findRoot = [&set](int x)
        {
            while(set[x] >= 0)
                x = set[x];
            return x;
        };

        for(int i = 0; i < n; i++)
        {
            for(int j = 0; j < n; j++)
            {
                if(isConnected[i][j] == 1)
                {
                    int root1 = findRoot(i);
                    int root2 = findRoot(j);
                    if(root1 != root2)
                    {
                        set[root1] += set[root2];
                        set[root2] = root1;
                    }
                }
            }
        }

        int count = 0;
        for(auto &x : set)
        {
            if(x < 0)
                count++;
        }

        return count;
    }
};

 总结并查集的特点:

1.一个位置的值是负数,那么它就是树的根,这个负数的绝对值就是这个树结点的个数。

2.一个位置的值是正数,那么这个正数就是它父亲结点的下标。

你可能感兴趣的:(数据结构)