Leetcode每日一题打卡 1061

题意

       给出长度相同的两个字符串s1 和 s2 ,还有一个字符串 baseStr 。其中  s1[i] 和 s2[i]  是一组等价字符。举个例子,如果 s1 = "abc" 且 s2 = "cde",那么就有 'a' == 'c', 'b' == 'd', 'c' == 'e'

       等价字符遵循任何等价关系的一般规则:

  •  自反性 'a' == 'a'
  •  对称性 'a' == 'b' 则必定有 'b' == 'a'
  •  传递性 :'a' == 'b' 且 'b' == 'c' 就表明 'a' == 'c'

       例如, s1 = "abc" 和 s2 = "cde" ,那么 baseStr = "eed" , "acd" 或 "aab",这三个字符串都是等价的,而 "aab" 是 baseStr 的按字典序最小的等价字符串。

利用 s1 和 s2 的等价信息,找出并返回 baseStr 的按字典序排列最小的等价字符串。

思考

        本题乍一眼看上去没看明白,但是照着样例写了写意思就是:s1和s2对应位置字符等价,可以进行替换。根据s1和s2字符串建立的等价规则,将baseStr改成最小字符串。

        一开始思考采用map的[key, value]键值对实现,但后来发现可以有多个字母对应等价,并不是一对一。测试样例给的s1 = "parker", s2 = "morris", baseStr = "parser"可以看出,k、r、s可互相转换。抽象一下,实际上是s1[i] = s2[i],s2[i] = s1[j],s1[j] = s2[j]。连等就有s1[i] = s2[j]。这个写法博主有些熟悉,但是年代久远有点忘了。后来没思考出来看了题解发现是并查集(。

知识点:并查集

原理

        那我们来回顾一下并查集的相关知识,博主也顺带复习一下。当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。并查集主要有两个功能:

  • 将两个元素添加到一个集合中。
  • 判断两个元素在不在同一个集合。

        我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢?只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了,实际上是一个有向连通图。这样我们便可以通过一个find函数来寻根。

// 将v,u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

        那么find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根。

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u; // 如果根就是自己,直接返回
    else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}

        如何表示 C 也在同一个元素里呢? 我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。所以father数组初始化时要 father[i] = i,默认自己指向自己。

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}

        最后我们如何判断两个元素是否在同一个集合里?如果通过 find函数找到两个元素属于同一个根的话,那么这两个元素就是同一个集合。

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

路径压缩 

        在实现 find 函数的过程中,我们知道,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。搜索过程像是一个多叉树中从叶子到根节点的过程。如果这棵多叉树高度很深的话,每次find函数去寻找根的过程就要递归很多次。我们的目的只需要知道这些节点在同一个根下就可以。所以可以适当简化

↓  简化 ↓

// 并查集里寻根的过程
int find(int u) {
    if (u == father[u]) return u;
    else return father[u] = find(father[u]); // 路径压缩
}

 代码模板

那么此时并查集的模板就出来了, 整体模板C++代码如下:

int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构

// 并查集初始化
void init() {
    for (int i = 0; i < n; ++i) {
        father[i] = i;
    }
}
// 并查集里寻根的过程
int find(int u) {
    return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}

// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
    u = find(u);
    v = find(v);
    return u == v;
}

// 将v->u 这条边加入并查集
void join(int u, int v) {
    u = find(u); // 寻找u的根
    v = find(v); // 寻找v的根
    if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
    father[v] = u;
}

通过模板,我们可以知道,并查集主要有三个功能。

  1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点

Leetcode1061解题代码

class Solution 
{
    public:
        string smallestEquivalentString(string s1, string s2, string baseStr) 
        {
        // 1. 初始化并查集数组fa:存储每个字符(0-25)的父节点
            int fa[26];  
            ranges::iota(fa, 0);  // 填充fa为[0,1,2...,25],即每个节点的父节点初始为自身

        // 2. 定义带路径压缩的函数find:递归查找x的根节点,并将路径上所有节点直接连接到根节点
            auto find = [&](this auto&& find, int x) -> int 
            {
                if (fa[x] != x) // 若当前节点不是根节点
                {
                    fa[x] = find(fa[x]);  // 递归查找根节点并压缩路径
                }
                return fa[x];  // 返回根节点
            };

        // 3. 定义合并函数merge:将x和y所在集合合并,保证小字符成为根节点
                 auto merge = [&](int x, int y) 
            {
                int rootX = find(x);  // 查找x的根节点
                int rootY = find(y);  // 查找y的根节点
                if (rootX != rootY)  // 若两集合不相同
                {  
                    if (rootX > rootY) // 确保小字符的根节点成为新根
                    {  
                        fa[rootX] = rootY;
                    } 
                    else 
                    {
                        fa[rootY] = rootX;
                    }
                }
            };

            // 4. 遍历s1和s2,处理所有等价关系:将对应位置的字符合并到同一集合
            for (int i = 0; i < s1.size(); i++) 
            {
                merge(s1[i] - 'a', s2[i] - 'a');  // 字符转索引(如'a'→0)后合并
            }

            // 5. 遍历baseStr,将每个字符替换为其所在集合的最小字符(根节点)
            for (char& c : baseStr) 
            {
                c = find(c - 'a') + 'a';  // 索引转回字符(如0→'a')
            }
        
            return baseStr;  // 返回转换后的字符串
        }
};

致谢

        感谢代码随想录提供的图片与知识点,不用手画丑图。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值