一、题目要求
- 使用下面描述的算法可以扰乱字符串 s 得到字符串 t:
-
-
-
-
- 在一个随机下标处将字符串分割成两个非空的子字符串。即如果已知字符串 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"
"r/g / e/ a/t" --> "r/g / e/ a/t"
算法终止,结果字符串和 s2 相同,都是 "rgeat"
这是一种能够扰乱 s1 得到 s2 的情形,可以认为 s2 是 s1 的扰乱字符串,返回 true
输入:s1 = "abcde", s2 = "caebd"
输出:false
输入:s1 = "a", s2 = "a"
输出:true
二、求解算法
① 动态规划
- 给定两个字符串 T 和 S,假设 T 是由 S 变换而来:
-
-
- 如果长度一样,顶层字符串 S 能够划分为 S1 和 S2,同样字符串 T 也能够划分为 T1 和 T2;
-
-
- 情况一:没交换,S1 ==> T1,S2 ==> T2;
-
-
- 情况二:交换了,S1 ==> T2,S2 ==> T1;
- 子问题就是分别讨论两种情况,T1 是否由 S1 变来,T2 是否由 S2 变来,或 T1 是否由 S2 变来,T2 是否由 S1 变来。

- dp[i][j][k][h] 表示 T[k…h] 是否由 S[i…j] 变来。由于变换必须长度是一样的,因此这边有个关系 j−i=h−k ,可以把四维数组降成三维。dp[i][j][len] 表示从字符串 S 中 i 开始长度为 len 的字符串是否能变换为从字符串T 中 j 开始长度为 len 的字符串。
- 转移方程:

- 枚举 S1 长度 w(从 1~k−1,因为要划分),f[i][j][w] 表示 S1 能变成 T1 ,f[i+w][j+w][k−w] 表示 S2 能变成 T2 ,或者是 S1 能变成 T2 ,S2 能变成 T1。
- 初始条件:对于长度是 1 的子串,只有相等才能变过去,相等为 true,不相等为 false。
- Java 示例:
class Solution {
public boolean isScramble(String s1, String s2) {
char[] chs1 = s1.toCharArray();
char[] chs2 = s2.toCharArray();
int n = s1.length();
int m = s2.length();
if (n != m) {
return false;
}
boolean[][][] dp = new boolean[n][n][n + 1];
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
dp[i][j][1] = chs1[i] == chs2[j];
}
}
for (int len = 2; len <= n; len++) {
for (int i = 0; i <= n - len; i++) {
for (int j = 0; j <= n - len; j++) {
for (int k = 1; k <= len - 1; k++) {
if (dp[i][j][k] && dp[i + k][j + k][len - k]) {
dp[i][j][len] = true;
break;
}
if (dp[i][j + len - k][k] && dp[i + k][j][len - k]) {
dp[i][j][len] = true;
break;
}
}
}
}
}
return dp[0][0][n];
}
}
class Solution {
public boolean isScramble(String s1, String s2) {
if (s1.length() != s2.length()) {
return false;
}
if (s1.equals(s2)) {
return true;
}
int n = s1.length();
HashMap<Character, Integer> map = new HashMap<>();
for (int i = 0; i < n; i++) {
char c1 = s1.charAt(i);
char c2 = s2.charAt(i);
map.put(c1, map.getOrDefault(c1, 0) + 1);
map.put(c2, map.getOrDefault(c2, 0) - 1);
}
for (Character key : map.keySet()) {
if (map.get(key) != 0) {
return false;
}
}
for (int i = 1; i < n; i++) {
boolean flag =
(isScramble(s1.substring(0, i), s2.substring(0, i)) && isScramble(s1.substring(i), s2.substring(i))) ||
(isScramble(s1.substring(0, i), s2.substring(n - i)) && isScramble(s1.substring(i), s2.substring(0, s2.length() - i)));
if (flag) {
return true;
}
}
return false;
}
}
② 朴素解法(TLE)
- 一个朴素的做法根据「扰乱字符串」的生成规则进行判断,由于题目说了整个生成「扰乱字符串」的过程是通过「递归」来进行。要实现 isScramble 函数的作用是判断 s1 是否可以生成出 s2。
- 这样判断的过程,同样我们可以使用「递归」来做:假设 s1 的长度为 n, 的第一次分割的分割点为 i,那么 s1 会被分成 [0,i) 和 [i,n) 两部分。同时由于生成「扰乱字符串」时,可以选交换也可以选不交换,因此我们的 s2 会有两种可能性:

- 因为对于某个确定的分割点,s1 固定分为两部分,分别为 [0,i) & [i,n)。而 s2 可能会有两种分割方式,分别 [0,i) & [i,n) 和 [0,n−i) & [n−i,n)。
- 只需要递归调用 isScramble 检查 s1 的 [0,i) & [i,n) 部分能否与 「s2 的 [0,i) & [i,n)」 或者 「s2 的 [0,n−i) & [n−i,n)」 匹配即可。
- 同时,将「s1 和 s2 相等」和「s1 和 s2 词频不同」作为「递归」出口。
- Java 示例:
class Solution {
public boolean isScramble(String s1, String s2) {
if (s1.equals(s2)) return true;
if (!check(s1, s2)) return false;
int n = s1.length();
for (int i = 1; i < n; i++) {
String a = s1.substring(0, i), b = s1.substring(i);
String c = s2.substring(0, i), d = s2.substring(i);
if (isScramble(a, c) && isScramble(b, d)) return true;
String e = s2.substring(0, n - i), f = s2.substring(n - i);
if (isScramble(a, f) && isScramble(b, e)) return true;
}
return false;
}
boolean check(String s1, String s2) {
if (s1.length() != s2.length()) return false;
int n = s1.length();
int[] cnt1 = new int[26], cnt2 = new int[26];
char[] cs1 = s1.toCharArray(), cs2 = s2.toCharArray();
for (int i = 0; i < n; i++) {
cnt1[cs1[i] - 'a']++;
cnt2[cs2[i] - 'a']++;
}
for (int i = 0; i < 26; i++) {
if (cnt1[i] != cnt2[i]) return false;
}
return true;
}
}