目录

题目描述

1061. 按字典序排列最小的等效字符串

题意分析

给两个等长字符串 s1s2,规定对每个位置 i,字符 s1[i]s2[i] 等价。等价关系满足自反、对称、传递三条性质。要求把 baseStr 中的每个字符替换成与它等价的字符里字典序最小的那个,返回替换后的字符串。

题目明确写出「自反、对称、传递」,这是极强的提示:等价关系把 26 个字母切成若干个互不相交的等价类,同一类里的字母可以互相替换。所以真正要回答的问题是:每个字母属于哪一类,以及每一类里最小的字母是谁。

传递性是难点所在。s1 = "abc"s2 = "bcd" 给出的直接关系只有 a~bb~cc~d,但 ad 也必须被认作等价——关系是可以链式传播的。所以不能只看每一对,必须做合并:不断把有直接关系的两个字母所在的组并成一组。

约束里有一条决定性的信息:字符全是小写字母,也就是说参与合并的元素只有 26 个,与字符串长度无关。这让整个「分组」结构的规模变成常数,无论 s1 多长,组的数量都不超过 26。1 ≤ s1.length ≤ 10001 ≤ baseStr.length ≤ 1000 的规模在这个结构下毫无压力。

边界:s1[i]s2[i] 可能相同(自反关系,合并时会发现已在同一组,直接跳过);同一对关系可能重复出现;baseStr 里的字符可能从未在 s1s2 中出现过,此时它自成一组,替换结果就是它自己。

解法:并查集合并连通块

核心思路

自反、对称、传递的关系会把 26 个小写字母划分成若干等价类。每一对 s1[i] ~ s2[i] 都是在合并两个等价类,最终需要查询每个类中字典序最小的字符,因此并查集正好匹配这两个操作。

普通并查集只保证同组元素拥有同一个根,根本身不一定最小。本题把合并规则改为:找到两个集合的根后,始终让编号较小的根成为新根

不变量:每个集合的根都是该集合中字典序最小的字符。 初始时每个字符自成集合,不变量成立;合并两个集合时,旧根分别是各自集合的最小字符,取二者较小者作为新根,正好仍是合并后集合的最小字符。路径压缩只缩短查找路径,不改变根,因此不会破坏该性质。

所有关系合并完成后,对 baseStr 的每个字符执行 find,得到的根就是它能替换成的最小等价字符。

正确性:并查集对每一对直接关系执行合并,所以直接相连的字符在同一集合;集合合并具有传递性,因此所有通过关系链相连的字符也在同一集合。反过来,只有题目给出的关系会触发合并,不会把无关字符放到一起。结合“根是集合最小字符”的不变量,逐字符替换得到的每一位都已最小,而各位置互不约束,整个结果字符串也就字典序最小。

解题步骤

  1. 建立长度为 26 的 parent 数组,初始化 parent[i] = i
  2. 遍历 s1s2,对每一对字符先找根,再把较大根挂到较小根下。
  3. 遍历 baseStr,用带路径压缩的 find 找到每个字符所属集合的最小根,写入答案。

例如关系 c ~ db ~ ca ~ b 会依次把根从 c 更新为 b、再更新为 a。即使 d 从未与 a 直接配对,find(d) 仍会沿 d → c → b → a 找到 a,并在路径压缩后直接指向 a

s1[i] == s2[i] 或重复关系只会让两个根相同,无需额外处理;从未出现在关系中的字符始终以自己为根,替换后保持不变。

代码实现

class Solution {
    public String smallestEquivalentString(String s1, String s2, String baseStr) {
        int[] parent = new int[26];
        for (int i = 0; i < parent.length; i++) {
            parent[i] = i;
        }

        for (int i = 0; i < s1.length(); i++) {
            int root1 = find(parent, s1.charAt(i) - 'a');
            int root2 = find(parent, s2.charAt(i) - 'a');
            if (root1 != root2) {
                parent[Math.max(root1, root2)] = Math.min(root1, root2);
            }
        }

        StringBuilder answer = new StringBuilder(baseStr.length());
        for (int i = 0; i < baseStr.length(); i++) {
            int root = find(parent, baseStr.charAt(i) - 'a');
            answer.append((char) ('a' + root));
        }
        return answer.toString();
    }

    private int find(int[] parent, int x) {
        if (parent[x] != x) {
            parent[x] = find(parent, parent[x]);
        }
        return parent[x];
    }
}
func smallestEquivalentString(s1 string, s2 string, baseStr string) string {
	parent := make([]int, 26)
	for i := range parent {
		parent[i] = i
	}

	var find func(int) int
	find = func(x int) int {
		if parent[x] != x {
			parent[x] = find(parent[x])
		}
		return parent[x]
	}

	for i := 0; i < len(s1); i++ {
		root1 := find(int(s1[i] - 'a'))
		root2 := find(int(s2[i] - 'a'))
		if root1 < root2 {
			parent[root2] = root1
		} else if root2 < root1 {
			parent[root1] = root2
		}
	}

	answer := make([]byte, len(baseStr))
	for i := range baseStr {
		root := find(int(baseStr[i] - 'a'))
		answer[i] = byte(root) + 'a'
	}
	return string(answer)
}

复杂度分析

  • 时间复杂度:$O(n + m)$,其中 n = s1.lengthm = baseStr.length。并查集只有 26 个节点,单次 find、合并可视为常数时间。
  • 空间复杂度:除返回结果外为 $O(1)$,因为 parent 固定为 26 个元素;构造长度为 m 的结果需要 $O(m)$ 空间。

关键点总结

  • 等价关系对应连通分量;关系逐条加入、需要传递闭包时优先考虑并查集。
  • 让较小根成为新根,把“集合代表”直接维护成“集合最小字符”,查询时无需再扫描集合。
  • 合并前必须比较两个,不能直接修改原字符的父节点。
  • 路径压缩不改变集合代表,既保持最小根不变量,也让后续查询更直接。

易错点总结

  • 直接比较原字符而不是集合根:先合并 a ~ c,再处理 b ~ c 时若直接覆盖 parent[c] = b,会断开已有的 a ~ c 关系;正确做法是合并 find(b)find(c)
  • 任意选择新根:若固定把第二个根挂到第一个根,p ~ m 可能让 p 成为代表,无法保证输出组内最小字符。若要按秩合并,就必须额外维护每组最小值;本题只有 26 个节点,直接按字符大小合并更简单。
  • 构造答案时读取 parent[x] 而不是 find(x)关系 c ~ db ~ c 后,parent[d] 可能仍是 c,但真正的最小根已是 b
  • 忘记初始化 parent[i] = iJava 的整型数组默认全为 0,会把所有字母错误地归入 a 所在集合。
  • 把关系当成单向替换:题目给的是对称等价关系,不是 s1[i] → s2[i] 的映射;单向表无法处理反向和传递替换。
  • 只初始化关系中出现的字符:baseStr 中未参与任何关系的字符也应自成一组并保持不变,因此 26 个字母必须全部初始化。

相似题目

题目 难度 考察点
990. 等式方程的可满足性 中等 同样是 26 个字母的等价类,但要先处理所有等式再用不等式做矛盾检验,顺序不能反
1202. 交换字符串中的元素 中等 下标之间连通,同一组内的字符可任意重排,需对每组排序后回填,而非取单个最小值
547. 省份数量 中等 只统计连通分量个数,根不携带业务语义,可以放心使用按秩合并
684. 冗余连接 中等 利用 union 返回「是否已同组」来定位第一条成环的边,考的是合并的返回值
721. 账户合并 中等 元素是邮箱字符串,要先做映射再合并,最后还需按组收集并排序输出
839. 相似字符串组 困难 边不是直接给出的,需要两两判断相似性才能建边,合并前的预处理是主要开销
128. 最长连续序列 中等 可用并查集维护「组的大小」作为根的业务语义,与本题「根存最小值」是同一套手法