87. 扰乱字符串
使用下面描述的算法可以扰乱字符串 s
得到字符串 t
:
- 如果字符串的长度为 1 ,算法停止
- 如果字符串的长度 > 1 ,执行下述步骤:
- 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串
s
,则可以将其分成两个子字符串x
和y
,且满足s = x + y
。 - 随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不变」。即,在执行这一步骤之后,
s
可能是s = x + y
或者s = y + x
。 - 在
x
和y
这两个子字符串上继续从步骤 1 开始递归执行此算法。
- 在一个随机下标处将字符串分割成两个非空的子字符串。即,如果已知字符串
给你两个 长度相等 的字符串 s1
和 s2
,判断 s2
是否是 s1
的扰乱字符串。如果是,返回 true
;否则,返回 false
。
示例 1:
输入:s1 = "great", s2 = "rgeat"
输出:true
解释:s1 上可能发生的一种情形是:
"great" --> "gr/eat" // 在一个随机下标处分割得到两个子字符串
"gr/eat" --> "gr/eat" // 随机决定:「保持这两个子字符串的顺序不变」
"gr/eat" --> "g/r / e/at" // 在子字符串上递归执行此算法。两个子字符串分别在随机下标处进行一轮分割
"g/r / e/at" --> "r/g / e/at" // 随机决定:第一组「交换两个子字符串」,第二组「保持这两个子字符串的顺序不变」
"r/g / e/at" --> "r/g / e/ a/t" // 继续递归执行此算法,将 "at" 分割得到 "a/t"
"r/g / e/ a/t" --> "r/g / e/ a/t" // 随机决定:「保持这两个子字符串的顺序不变」
算法终止,结果字符串和 s2 相同,都是 "rgeat"
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true
示例 2:
输入:s1 = "abcde", s2 = "caebd"
输出:false
示例 3:
输入:s1 = "a", s2 = "a"
输出:true
提示:
s1.length == s2.length
1 <= s1.length <= 30
s1
和s2
由小写英文字母组成
方法一:暴力递归
一个暴力的做法,直接根据题目的「扰乱规则」来匹配字符串。
假设字符串长度为 n
,当前分割的长度为 i
,有 不交换 与 交换 两种匹配方式:
- 不交换 相等条件:
s1[0, i) == s2[0, i)
且s1[i, n) == s2[i, n)
- 交换 相等条件:
s1[0, i) == s2[n - i, n)
且s1[i, n) == s2[0, n - i)
递归执行函数,直到字符串长度 n = 1
即可,代码如下:
public boolean isScramble(String s1, String s2) {
int n = s1.length();
for (int i = 1; i < n; i++) {
String x1 = s1.substring(0, i), y1 = s1.substring(i);
String x2 = s2.substring(0, i), y2 = s2.substring(i);
// 不交换
if (isScramble(x1, x2) && isScramble(y1, y2)) {
return true;
}
String x3 = s2.substring(n - i, n), y3 = s2.substring(0, n - i);
// 交换
if (isScramble(x1, x3) && isScramble(y1, y3)) {
return true;
}
}
return s1.equals(s2);
}
结果必然超时,卡在 195 个测试用例,如下图所示:
上面的方法没有进行任何 剪枝,能够想到的 剪枝 方法有:
- 在方法入口先判断两个字符串是否相等,相等直接返回
true
- 判断两个字符串每个字母的出现频率是否相等,不相等直接返回
false
剪枝 的代码如下:
public boolean isScramble(String s1, String s2) {
// 剪枝1:先判断字符串是否相等
if (s1.equals(s2)) {
return true;
}
// 剪枝2:判断单词出现频率是否相同
if (!checkFreq(s1, s2)) {
return false;
}
int n = s1.length();
for (int i = 1; i < n; i++) {
String x1 = s1.substring(0, i), y1 = s1.substring(i);
String x2 = s2.substring(0, i), y2 = s2.substring(i);
if (isScramble(x1, x2) && isScramble(y1, y2)) {
return true;
}
String x3 = s2.substring(n - i, n), y3 = s2.substring(0, n - i);
if (isScramble(x1, x3) && isScramble(y1, y3)) {
return true;
}
}
return false;
}
public boolean checkFreq(String s1, String s2) {
int[] count1 = new int[26];
int[] count2 = new int[26];
for (int i = 0; i < s1.length(); i++) {
count1[s1.charAt(i) - 'a']++;
count2[s2.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (count1[i] != count2[i]) {
return false;
}
}
return true;
}
虽然 剪枝 优化了一些匹配路径,但并不会使时间复杂度下降一个数量级。
结果依然超时,卡在 286 个测试用例,如下图所示:
方法二:记忆化递归
暴力递归 超时的原因很简单,即我们做了很多重复的判断,我们可以使用一个记忆数组 memory
保存之前的处理结果。
memory[i][j][len]
表示 s1 从 i 开始长度为 len 的子串
和 s2 从 j 开始长度为 len 的字串
,是否已经处理过和对应的处理结果,这里可以使用包装类 Boolean
:
memory[i][j][len]
==null
,表示没处理过memory[i][j][len]
==true
,表示处理过 且 两个子串处理后可以相等memory[i][j][len]
==false
,表示处理过 且 两个子串处理后也无法相等
参考代码
String str1, str2;
Boolean[][][] memory;
public boolean isScramble(String s1, String s2) {
int n = s1.length();
str1 = s1; str2 = s2;
memory = new Boolean[n][n][n + 1];
return dfs(0, 0, n);
}
public boolean dfs(int i, int j, int len) {
if (memory[i][j][len] != null) {
return memory[i][j][len];
}
String x = str1.substring(i, i + len), y = str2.substring(j, j + len);
if (x.equals(y)) {
return true;
}
if (checkFreq(x, y)) {
for (int k = 1; k < len; k++) {
if (dfs(i, j, k) && dfs(i + k, j + k, len - k) ||
dfs(i, len + j - k, k) && dfs(i + k, j, len - k)) {
return memory[i][j][len] = true;
}
}
}
return memory[i][j][len] = false;
}
public boolean checkFreq(String s1, String s2) {
int[] count1 = new int[26];
int[] count2 = new int[26];
for (int i = 0; i < s1.length(); i++) {
count1[s1.charAt(i) - 'a']++;
count2[s2.charAt(i) - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (count1[i] != count2[i]) {
return false;
}
}
return true;
}
执行结果