题意
给出长度相同的两个字符串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;
}
通过模板,我们可以知道,并查集主要有三个功能。
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数: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; // 返回转换后的字符串
}
};
致谢
感谢代码随想录提供的图片与知识点,不用手画丑图。

382

被折叠的 条评论
为什么被折叠?



