[C++数据结构](32)并查集

简介

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

其表示方法有以下特点:

  1. 类似于堆,用下标表示关系
  2. 双亲表示法

双亲表示法即使用数组,对每个结点各增加一个表示其双亲位置的变量。如下图:用双亲表示法表示一棵树

其中根结点无双亲,parent表示为-1

双亲表示法

  1. 一个位置的值是负数,那么它就是树的根,这个负数的绝对值就是这棵树的结点个数。
  2. 一个位置的值是非负数,那么它就是双亲的下标。

具体例子:

假设有三支小队共10个人出门春游,给这10个人分别编号0~9,然后将{0, 6, 7, 8}分到第一组,{1, 4, 9}分到第二组,{2, 3, 5}分到第三组。这样分出三个小组,然后选0, 1, 2担任各小组的组长。

树形表示:

img

数组表示:

这里的下标就是data

img

将第二组合并到第一组:

合并

并查集实现

  • 初始化:每个结点都是一棵树,数组初始化为 -1

  • 合并:将x2所在树合并到x1上

  • 找根:给定一个结点,找其根结点

  • 判断两个结点是否在一棵树上

  • 返回并查集中树的个数

具体实现也不难,关键在于找根。有时候合并可能会导致一棵树变得很高,这样会对找根的效率产生影响,为了让树变矮,需要用到路径压缩算法。

class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	// 合并
	void Union(int x1, int x2)
	{
		// 如果两个结点在同一棵树,则没必要合并
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2) return;
        // 小的往大的合并
		if (_ufs[root1] > _ufs[root2]) swap(root1, root2);

		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	// 找根
	int FindRoot(int x)
	{
		// 找双亲,直到一个位置的值为负数,则这个位置就是根
		int root = x;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		// 路径压缩
		while (_ufs[x] >= 0)
		{
			int parent = _ufs[x];
			_ufs[x] = root;
			x = parent;
		}

		return root;
	}

	// 判断两个结点是否在一棵树上
	bool InSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}

	// 有多少棵树,也就是数组中负数的个数
	size_t SetSize()
	{
		size_t size = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0) ++size;
		}
		return size;
	}
private:
	vector<int> _ufs;
};

关于合并,我们建议把小的树往大的树上并,因为被并的树每个结点的深度都会+1,我们希望让尽量少的结点深度增加。

关于路径压缩,这里选择在找根的时候进行,当我们找到某个结点的 root 后,再走一遍刚刚找根的这个路径,并把路径上的所有结点的双亲结点都设为 root

题目

并查集特别适合于做具有传递性关系的题目,如一棵树表示一个家庭,ab 是一家人,cd 是一家人,现在告诉你 bc 的结婚了,那么 a b c d 就都是一家人了,它们两个家庭可以合并为一个家庭,最后树的个数即为家庭个数。


省份数量

547. 省份数量 - 力扣(LeetCode)

n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。

省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。

给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。

返回矩阵中 省份 的数量。

示例 1:

img

输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2

示例 2:

img

输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3

提示:

  • 1 <= n <= 200
  • n == isConnected.length
  • n == isConnected[i].length
  • isConnected[i][j]10
  • isConnected[i][i] == 1
  • isConnected[i][j] == isConnected[j][i]

将我们写好的并查集拿过来。

将相连的城市在并查集中合并,最后查看有多少棵树就有多少个省份

class UnionFindSet
{
public:
	UnionFindSet(size_t n)
		: _ufs(n, -1)
	{}

	// 合并
	void Union(int x1, int x2)
	{
		// 如果两个结点在同一棵树,则没必要合并
		int root1 = FindRoot(x1);
		int root2 = FindRoot(x2);
		if (root1 == root2) return;
        // 小的往大的合并
		if (_ufs[root1] > _ufs[root2]) swap(root1, root2);
        
		_ufs[root1] += _ufs[root2];
		_ufs[root2] = root1;
	}

	// 找根
	int FindRoot(int x)
	{
		// 找双亲,直到一个位置的值为负数,则这个位置就是根
		int root = x;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		// 路径压缩
		while (_ufs[x] >= 0)
		{
			int parent = _ufs[x];
			_ufs[x] = root;
			x = parent;
		}

		return root;
	}

	// 判断两个结点是否在一棵树上
	bool InSet(int x1, int x2)
	{
		return FindRoot(x1) == FindRoot(x2);
	}

	// 有多少棵树,也就是数组中负数的个数
	size_t SetSize()
	{
		size_t size = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0) ++size;
		}
		return size;
	}
private:
	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.SetSize();
    }
};

一般做题我们不去手撸一个并查集类,代码量太大了。如下采用面向过程的思想完成题目即可。

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 root1 = findRoot(i);
                    int root2 = findRoot(j);
                    if (root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }
                }
            }
        }
        int res = 0;
        for (auto e : ufs)
        {
            if (e < 0) ++res;
        }
        return res;
    }
};

等式方程的可满足性

990. 等式方程的可满足性 - 力扣(Leetcode)

给定一个由表示变量之间关系的字符串方程组成的数组,每个字符串方程 equations[i] 的长度为 4,并采用两种不同的形式之一:"a==b""a!=b"。在这里,a 和 b 是小写字母(不一定不同),表示单字母变量名。

只有当可以将整数分配给变量名,以便满足所有给定的方程时才返回 true,否则返回 false

示例 1:

输入:["a==b","b!=a"]
输出:false
解释:如果我们指定,a = 1 且 b = 1,那么可以满足第一个方程,但无法满足第二个方程。没有办法分配变量同时满足这两个方程。

示例 2:

输入:["b==a","a==b"]
输出:true
解释:我们可以指定 a = 1 且 b = 1 以满足满足这两个方程。

示例 3:

输入:["a==b","b==c","a==c"]
输出:true

示例 4:

输入:["a==b","b!=c","c==a"]
输出:false

示例 5:

输入:["c==c","b==d","x!=z"]
输出:true

提示:

  1. 1 <= equations.length <= 500
  2. equations[i].length == 4
  3. equations[i][0]equations[i][3] 是小写字母
  4. equations[i][1] 要么是 '=',要么是 '!'
  5. equations[i][2]'='

依然是一道并查集的简单题,a == b 可以理解为 a 和 b 在一棵树上,a != b 则表示 a b不在一棵树上,我们先遍历等式方程,将所有相等的变量放在一棵树中,然后遍历不等式方程,如果出现矛盾,则返回 false。

class Solution {
public:
    bool equationsPossible(vector<string>& equations) {
        int n = equations.size();
        vector<int> ufs(26, -1);

        auto findRoot = [&ufs](int x)
        {
            while (ufs[x] >= 0)
                x = ufs[x];
            return x;
        };

        for (int i = 0; i < n; ++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 < n; ++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;
    }
};

2023-1-9 更新:

C++ 模板

上面的是并查集的递推+类封装的实现,我们平时做题的时候完全可以写得更简洁一些。

基础版

  • p[i] 表示编号为 i 的结点的父结点的编号,如果 i 是根结点,那么p[i] == i。这一点和上面的实现大不相同。

  • 下面的 find 函数是递归的写法,可以返回一个结点所在集合的根,同时实现路径压缩

const int N = 100010;

int p[N];

int find(int x) {
    return p[x] == x ? x : p[x] = find(p[x]);
}

void unite(int x, int y) {
    p[find(x)] = find(y);
}

初始化可以让n个元素各自为一个集合:

    for (int i = 0; i < n; ++i) p[i] = i;

带有查找集合元素个数的功能的版本

  • 增加一个 cnt 数组:cnt[i] 表示 i 结点所在集合的元素个数(i 为根结点)

此时 unite 需要判断 xy 是否在一个集合。否则,若 xy 在同一个以 i 为根结点的集合,cnt[i] += cnt[i],集合大小计算出错。

const int N = 100010;

int p[N], cnt[N];

int find(int x) {
    return p[x] == x ? x : p[x] = find(p[x]);
}

void unite(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return;
    cnt[x] += cnt[y];
    p[y] = x;
}

初始化 cnt 数组里的元素都初始化成 1:

    for (int i = 1; i <= n; ++i) {
        p[i] = i;
        cnt[i] = 1;
    }

启发式合并优化的版本

const int N = 100010;

int p[N], cnt[N];

int find(int x) {
    return p[x] == x ? x : p[x] = find(p[x]);
}

void unite(int x, int y) {
    x = find(x), y = find(y);
    if (x == y) return;
    if (cnt[x] < cnt[y]) swap(x, y);
    cnt[x] += cnt[y];
    p[y] = x;
}

只要在合并的时候加一行 if (cnt[x] < cnt[y]) swap(x, y); 就行了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

世真

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

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

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

打赏作者

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

抵扣说明:

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

余额充值