LC 854. 相似度为 K 的字符串

题目描述

这是 LeetCode 上的 854. 相似度为 K 的字符串 ,难度为 困难

对于某些非负整数 k ,如果交换 $s_1$ 中两个字母的位置恰好 k 次,能够使结果字符串等于 $s_2$ ,则认为字符串 $s_1$ 和 $s_2$ 的 相似度为 k

给你两个字母异位词 $s_1$ 和 $s_2$ ,返回 $s_1$ 和 $s_2$ 的相似度 k 的最小值。

示例 1:

1
2
3
输入:s1 = "ab", s2 = "ba"

输出:1

示例 2:
1
2
3
输入:s1 = "abc", s2 = "bca"

输出:2

提示:

  • $1 <= s1.length <= 20$
  • $s2.length = s1.length$
  • s1s2 只包含集合 {'a', 'b', 'c', 'd', 'e', 'f'} 中的小写字母
  • s2s1 的一个字母异位词

AStar 算法

由于题目确保了 s1s2 互为字母异位词(必然有解),因此最好的求解方式是使用 AStar 算法。

可直接根据本题规则来设计 AStar 的启发式函数: 对于两个状态 ab 直接计算出「理论最小转换次数」: 不同字符串的转换成本之和,由于每一次交换最多可减少两个不同的字符,我们可计算 ab 的不同字符数量 $ans$,对应的理论最小转换次数为 $\left \lfloor \frac{ans + 1}{2} \right \rfloor$。

需要注意的是:由于我们衡量某个字符 str 的估值是以目标字符串 target 为基准,因此我们只能确保 target 出队时为「距离最短」,而不能确保中间节点出队时「距离最短」,因此我们不能单纯根据某个节点是否「曾经入队」而决定是否入队,还要结合当前节点的「最小距离」是否被更新而决定是否入队。

一些细节:在使用当前状态(字符串)poll 拓展新状态(字符串)nstr 时,只拓展能够减少不同字符数量的方案,从而收窄搜索空间。

Java 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Solution {
int n;
String t;
int f(String s) {
int ans = 0;
for (int i = 0; i < n; i++) ans += s.charAt(i) != t.charAt(i) ? 1 : 0;
return ans + 1 >> 1;
}
public int kSimilarity(String s1, String s2) {
if (s1.equals(s2)) return 0;
t = s2;
n = s1.length();
Map<String, Integer> map = new HashMap<>();
PriorityQueue<String> pq = new PriorityQueue<>((a,b)->{
int v1 = f(a), v2 = f(b), d1 = map.get(a), d2 = map.get(b);
return (v1 + d1) - (v2 + d2);
});
map.put(s1, 0);
pq.add(s1);
while (!pq.isEmpty()) {
String poll = pq.poll();
int step = map.get(poll);
char[] cs = poll.toCharArray();
int idx = 0;
while (idx < n && cs[idx] == t.charAt(idx)) idx++;
for (int i = idx + 1; i < n; i++) {
if (cs[i] != t.charAt(idx) || cs[i] == t.charAt(i)) continue;
swap(cs, idx, i);
String nstr = String.valueOf(cs);
swap(cs, idx, i);
if (map.containsKey(nstr) && map.get(nstr) <= step + 1) continue;
if (nstr.equals(t)) return step + 1;
map.put(nstr, step + 1);
pq.add(nstr);
}
}
return -1; // never
}
void swap(char[] cs, int i, int j) {
char c = cs[i];
cs[i] = cs[j];
cs[j] = c;
}
}

  • 时间复杂度:启发式搜索不分析时空复杂度
  • 空间复杂度:启发式搜索不分析时空复杂度

最后

这是我们「刷穿 LeetCode」系列文章的第 No.854 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。

在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。

为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode

在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。