【LeetCode_547_990】并查集的应用——省份数量 + 等式方程的可满足性

刷爆LeetCode系列

LeetCode:省份数量 + 等式方程的可满足性

github地址

有梦想的电信狗

前言

本文用C++实现LeetCode并查集的应用相关题目

  • 省份数量
  • 等式方程的可满足性

一、省份数量

题目描述

题目链接https://leetcode.cn/problems/bLyHh0/description/


题目与思路分析

目标分析

  1. 题目本质共有n个城市

    给定一个 n × n 的邻接矩阵 isConnected,其中:

    • isConnected[i][j] == 1 表示城市 i 和城市 j 直接相连
    • 相连具有传递性
      i ~ jj ~ ki ~ k

    要求求出有多少个互相连通的城市集合(省份) :给定一组城市,直接相连间接相连 的城市属于同一个省份,不相连的城市属于不同省份

  2. 最终返回省份的数量


思路转化为并查集问题

  • 每个城市是一个节点
  • 直接相连或间接相连的城市属于同一个集合
  • 最终省份数量 = 集合的个数

操作借助并查集结构维护连通关系

  • 初始化:每个城市自成一个集合
  • 遍历矩阵
    • isConnected[i][j] == 1,且 i 和 j 不属于同一个集合时 ,合并城市 ij用下标 i 和 j 标识城市
  • 最后统计并查集中根节点的个数,即为省份数量

代码实现

之前写好的并查集

cpp 复制代码
class UnionFindSet
{
public:
	// 用 n 个数字构造并查集, 0-n 为下标, vector 中存数字
	// 存负数: 代表当前 下标所代表的元素 为根
	// 存 >=0 的数: 该数为 当前元素的父节点的 下标
	UnionFindSet(size_t n)
		:_ufs(n, -1)
	{}

	// 提供如下成员函数
	
	void Union(int index1, int index2)	// 将两个成员合并成一个集合
	{
		int root1 = FindRoot(index1);
		int root2 = FindRoot(index2);

		if (root1 == root2)		// 两元素的根相同,即本身就在同一个集合,则不合并
			return;

		// 数据量小的集合 合并到  数据量大的集合    _ufs[root] 越小(越负),集合越大
		// 作为子树合并的集合,层数会增加一层,因此要将 大集合  合并到  小集合
		if (_ufs[root1] > _ufs[root2])
			std::swap(root1, root2);

		// 合并的逻辑
		_ufs[root1] += _ufs[root2];		// 
		_ufs[root2] = root1;
	}

	// 非压缩路径版本
	//// 可以在边找的过程中,加一个压缩路径的过程
	//int FindRoot(int index)	// 找当前元素的根结点的下标
	//{
	//	int parent = index;		// 刚开始,每个元素都是一个独立的的根
	//	// 找根小于0的位置
	//	while (_ufs[parent] >= 0)
	//	{
	//		parent = _ufs[parent];
	//	}

	//	return parent;
	//}

	// 压缩路径版
	int FindRoot(int index)	// 找当前元素的根结点的下标
	{
		// 1. 找根 的下标
		int root = index;
		while (_ufs[root] >= 0)
		{
			root = _ufs[root];
		}

		// 2. 压缩路径
		int cur = index;
		while (_ufs[cur] >= 0)
		{
			int parent = _ufs[cur];		// 保存父亲
			_ufs[cur] = root;			// 将父亲改成跟
			cur = parent;				// 更新 cur
		}
		return root;
	}

	bool IsInSet(int index1, int index2)	// 判断两个元素是否在同一个集合
	{
		int root1 = FindRoot(index1);
		int root2 = FindRoot(index2);
		return root1 == root2;		// 两个元素有相同的根,则在同一个集合
	}

	// 数组中有几个值是负数,就有几个集合
	size_t SetCount() const  // 返回当前并查集中 集合的个数
	{
		size_t count = 0;
		for (size_t i = 0; i < _ufs.size(); ++i)
		{
			if (_ufs[i] < 0)
				++count;
		}
		return count;
	}
private:
	vector<int> _ufs;	
};

直接复用写好的并查集

cpp 复制代码
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.SetCount();
    }
};

算法代码优化

  • 难道做这道题我们要先写一个并查集吗,显然有些麻烦。

  • 我们可以将并查集的核心思想和函数提取出来,直接在题解中使用


手动控制并查集

cpp 复制代码
class Solution 
{
public:
    int findCircleNum(vector<vector<int>>& isConnected) 
    {
        vector<int> ufs(isConnected.size(), -1);
        auto FindRoot = [&ufs](int index)
        {
            // 找根
            int root = index;
            while(ufs[root] >= 0)
                root = ufs[root];
            
            // 路径压缩
            int cur = index;
            while(ufs[cur] >= 0)
            {
                int parent = ufs[cur];
                ufs[cur] = root;
                cur = parent;
            }
            return root;
        };

        // 相连或间接相连的城市属于一个省份,即属于同一个集合
        // 返回省份的数量,即:返回集合的数量
        for(int i = 0;i < isConnected.size();++i)
        {
            for(int j = 0;j < isConnected[i].size();++j)
            {
                if(isConnected[i][j] == 1)
                {
                    // 第 i 个城市和第 j 个城市直接相连,表明同属于一个集合,则合并
                    // 手动实现并查集的合并逻辑
                    int root1 = FindRoot(i);
                    int root2 = FindRoot(j);
                    // 根不同时才合并
                    if(root1 != root2)
                    {
                        ufs[root1] += ufs[root2];
                        ufs[root2] = root1;
                    }
                }
            }
        }
        // 最终返回 并查集中 负数的个数
        int count = 0;
        for(auto e:ufs)
        {
            if(e < 0)
                ++count;
        }
        return count;
    }
};

解释

  • vector<int> ufs(isConnected.size(), -1);并查集存储结构,初始化数组全为-1

  • FindRoot函数为Lambda表达式,引用捕获并查集ufs ,实现路径压缩版本的查找根的函数

  • 相连或间接相连的城市属于一个省份,即属于同一个集合;

  • 返回省份的数量,即:返回集合的数量

  • 手动控制合并逻辑

    cpp 复制代码
      if(isConnected[i][j] == 1)
      {
          // 第 i 个城市和第 j 个城市直接相连,表明同属于一个集合,则合并
          // 手动实现并查集的合并逻辑
          int root1 = FindRoot(i);
          int root2 = FindRoot(j);
          // 根不同时才合并
          if(root1 != root2)
          {
              ufs[root1] += ufs[root2];
              ufs[root2] = root1;
          }
      }
  • 并查集中负数的个数,即为集合的个数,即为省份数量

    cpp 复制代码
      // 最终返回 并查集中 负数的个数
      int count = 0;
      for(auto e:ufs)
      {
          if(e < 0)
              ++count;
      }
      return count;

二、等式方程的可满足性

题目描述

题目链接https://leetcode.cn/problems/bLyHh0/description/



题目与思路分析

目标分析

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

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

  2. 判断所有的字符串等式所表示的关系,是否有违背

  3. **所有字符串表示的关系均成立时,为 true,有一个违背时,为 false **


思路并查集

  • 首先遍历所有的等式,构造并查集。相等的字母,合并到同一个集合中
  • 然后遍历所有的不等式,不等式两端的字母一定不属于同一个集合

通过观察得知

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

操作

  • 造26个小写字母的并查集并初始化vector<int> ufs(26, -1);

  • 实现并查集的查找函数 FindRoot注意对两端字母查找时传参为 FindRoot(str[0] - 'a'); FindRoot(str[3] - 'a');

  • 首先遍历表示相等的字符串if(str[1] == '=')查找时查找字母所对应的 ASCII不属于同一个集合的两个字母进行合并

    cpp 复制代码
      for(auto& str : equations)
      {
          if(str[1] == '=')
          {
              int root1 = FindRoot(str[0] - 'a');		// 存每个小写字母的ASCII码
              int root2 = FindRoot(str[3] - 'a');
              if(root1 != root2)
              {
                  ufs[root1] += ufs[root2];
                  ufs[root2] = root1;
              }
          }
      }
  • 然后遍历表示不等的字符串if(str[1] == '!')判断不相等两端的字母的 ASCII ,是否在同一个集合,在的话返回false

    cpp 复制代码
      for(auto& str : equations)
      {
          if(str[1] == '!')
          {
              // 存每个小写字母的ASCII码
              int root1 = FindRoot(str[0] - 'a');
              int root2 = FindRoot(str[3] - 'a');
              if(root1 == root2)
                  return false;
          }
      }
  • 遍历结束后,返回true


代码实现

cpp 复制代码
class Solution 
{
public:
    bool equationsPossible(vector<string>& equations) 
    {
        // 先遍历相等的等式,把所有相等的字母放入一个集合
        // 再遍历不相等的等式,如果两端字母属于一个集合,则 false
        
         // 26 个字母的 ASCII 码的并查集
        vector<int> ufs(26, -1);    // 初始时,26个字母各自成一个集合
        auto FindRoot = [&ufs](int index)
        {
            // 找跟
            int root = index;
            while(ufs[root] >= 0)
                root = ufs[root];

            // 压缩路径
            int cur = index;
            while(ufs[cur] >= 0)
            {
                int parent = ufs[cur];
                ufs[cur] = root;
                cur = parent;
            }
            return root;
        };
		
        // 0 1 2 3
        // a ! = b
        // 先把相等的字母的 ASCII码 放入一个集合
        for(auto& str : equations)
        {
            if(str[1] == '=')
            {
                int root1 = FindRoot(str[0] - 'a');		// 存每个小写字母的ASCII码
                int root2 = FindRoot(str[3] - 'a');
                if(root1 != root2)
                {
                    ufs[root1] += ufs[root2];
                    ufs[root2] = root1;
                }
            }
        }
         // 再把不相等的,判断是否在同一个集合
        for(auto& str : equations)
        {
            if(str[1] == '!')
            {
                // 存每个小写字母的ASCII码
                int root1 = FindRoot(str[0] - 'a');
                int root2 = FindRoot(str[3] - 'a');
                if(root1 == root2)
                    return false;
            }
        }
        return true;
    }
};

算法代码优化

  • 有了第一道题的经验,这道题中我们直接使用手动控制并查集的方式实现,因此该题目无需优化

以上就是本文的所有内容了,如果觉得文章对你有帮助,欢迎 点赞⭐收藏 支持!如有疑问或建议,请在评论区留言交流,我们一起进步

分享到此结束啦
一键三连,好运连连!

你的每一次互动,都是对作者最大的鼓励!


征程尚未结束,让我们在广阔的世界里继续前行! 🚀

相关推荐
鱼跃鹰飞2 小时前
Leetcode会员尊享100题:270.最接近的二叉树值
数据结构·算法·leetcode
Queenie_Charlie2 小时前
小陶的疑惑2
数据结构·c++·树状数组
梵刹古音3 小时前
【C语言】 函数基础与定义
c语言·开发语言·算法
筵陌4 小时前
算法:模拟
算法
Queenie_Charlie4 小时前
小陶与杠铃片
数据结构·c++·树状数组
We་ct4 小时前
LeetCode 205. 同构字符串:解题思路+代码优化全解析
前端·算法·leetcode·typescript
renhongxia14 小时前
AI算法实战:逻辑回归在风控场景中的应用
人工智能·深度学习·算法·机器学习·信息可视化·语言模型·逻辑回归
CoderCodingNo4 小时前
【GESP】C++四级/五级练习题 luogu-P1223 排队接水
开发语言·c++·算法
民乐团扒谱机4 小时前
【AI笔记】精密光时频传递技术核心内容总结
人工智能·算法·光学频率梳