首页 简历|笔试面试

87.扰乱字符串

  • 25年9月4日 发布
  • 100.4KB 共8页
87.扰乱字符串87.扰乱字符串87.扰乱字符串87.扰乱字符串87.扰乱字符串

87. 扰乱字符串

题意

使用下面描述的算法可以扰乱字符串 s 得到字符串 t :

1. 如果字符串的长度为 1 ,算法停止

2. 如果字符串的长度 > 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

分析

看到题目给的字符串生成方法,我们很容易想到,把每个可能的扰动字符串生成出来,再

去比对,看答案是否就在其中。具体来说:

1. 对于字符串 s1,可以选择任何一个非空位置进行分割,得到两个子串。

2. 对于每一种分割,都可以选择交换这两个子串或不交换。

3. 递归地对每个子串重复上面的操作。

4. 如果在某一点,得到的扰乱序列与 s2 匹配,则返回 true。如果已经尝试了所有可能

的序列都没有匹配,则返回 false。

来看具体的题解:

public class Solution {

public boolean isScramble(String s1, String s2) {

// 如果两个字符串相等,直接返回 true

if (s1.equals(s2)) {

return true;

}

// 如果两个字符串的字符不一致,返回 false

int[] letters = new int[26];

for (int i = 0; i < s1.length(); i++) {

letters[s1.charAt(i) - 'a']++;

letters[s2.charAt(i) - 'a']--;

}

for (int i = 0; i < 26; i++) {

if (letters[i] != 0) {

return false;

}

}

// 尝试每一种分割和交换的组合

for (int i = 1; i < s1.length(); i++) {

// 不交换的情况

if (isScramble(s1.substring(0, i), s2.substring(0, i)) &&

isScramble(s1.substring(i), s2.substring(i))) {

return true;

}

// 交换的情况

if (isScramble(s1.substring(0, i), s2.substring(s2.length() - i)) &&

isScramble(s1.substring(i), s2.substring(0, s2.length() - i))) {

return true;

}

}

return false;

}

}

这种暴力解法很好理解,但时间复杂度是非常高的,对于每一个位置,都有两种可能的操

作(交换或不交换),因此时间复杂度约为 (O(2^n)),其中 (n) 是字符串的长度。

087.%E6%89%B0%E4%B9%B1%E5%AD%97%E7%AC%A6%E4%B8%B2-

20231008152955.png

我们来观察一下题目给出的信息吧。

首先一个显而易见的结论是——如果 s 为 t 的扰动字符串,那么 t 也是 s 的扰动字符串。

随机** 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺序不

变」。即,在执行这一步骤之后,**s** 可能是 **s = x + y** 或者 **s = y +

x** 。**

在 s 转化为 t 的过程中,如果是交换两个子字符串,t 转化为 s 也只需要交换两个子字符串

就可以,如果是保持两个子字符串的顺序不变,则 t 转化为 s 同样也只需要子字符串的顺

序不变即可。

因此我们可以得出第一个结论:

如果**s**为**t**的扰动字符串,那么**t**也是**s**的扰动字符串。

举个简单的例子来说明这一点:

假设有两个字符串 s = "great" 和 t = "rgeat",我们知道 t 是 s 的扰乱字符串,因为我们

可以将 s 分割为 gr | eat,然后交换这两个子串得到 t。

但是,我们也可以将 t 分割为 rg | eat,然后再次交换这两个子串,这样就可以得到 s。

因此,我们可以说 s 也是 t 的扰乱字符串。

第二个结论也好得出,只不过需要加上一点点小的思考:

如果**s**和**t**互为扰动字符串,那么它们的子串中肯定存在互为扰动字符串

的子串。

考虑字符串 s 和 t。如果 s 是 t 的扰乱字符串,那么在某个点,我们必须对 s 进行一次分

割得到 s1 和 s2,并对 t 进行一次分割得到 t1 和 t2,使得:

1. s1 是 t1 的扰乱字符串且 s2 是 t2 的扰乱字符串。

或者

2. s1 是 t2 的扰乱字符串且 s2 是 t1 的扰乱字符串。

这两种情况考虑了可能的交换操作。但无论是哪种情况,都意味着 s 和 t 的子串必须是扰

乱字符串。

针对本题,我们打算使用区间 DP,也就是区间动态规划。

区间动态规划 是动态规划的一个特定类别,主要用于解决需要处理字符串或数组中连续

子段(或称为区间)的问题。在这类问题中,我们经常需要枚举所有可能的区间,并尝试

通过某种方式组合这些区间来得到答案。

区间 DP 的特点通常包括以下几点:

1. 状态表示:通常使用两个索引 i 和 j 来表示区间 [i, j]。例如,dp[i][j] 可以表示从 i 到

j 的某个值。

2. 状态转移:为了计算 dp[i][j],我们可能需要知道在该区间内的某个位置 k 的分割情

况,然后根据 dp[i][k] 和 dp[k+1][j] 来计算。这通常涉及一个内部循环来枚举所有

可能的 k。

3. 初始化:较小的区间(如长度为 1 的区间)通常可以直接计算或由题目给出。这为我

们提供了动态规划的基本情况。

4. 计算顺序:由于某些区间的值可能依赖于较小的区间,所以计算的顺序是很重要的。

通常,我们可能会先计算较小的区间,然后扩展到较大的区间。

扰乱字符串就属于经典的区间 DP 问题,我们来看看这道题的状态表示、状态转移、初始

化和计算顺序。

首先我们可以创建一个 f[i][j][k][l]的四维数组,来表示从 i 到 j 的字符串 s 的字串,判断

其是否跟从 k 到 l 的字符串 t 的字串互为扰动字符串,

因为 s 和 t 互为扰动字符串,长度必然相等,所以我们可以节省一维,只用 f[i][j][len] 表

示从 i 开始的长度为 len 的 s 的子串跟从 j 开始的长度为 len 的 t 的子串互为扰动字符串。

做动态规划题,在明确了 状态(表示每个阶段开始所处的自然状况或客观条件) 之后,

接下来要做的就是确定初始状态。

我们从最小的情况来考虑,就是子串长度为 1 的情况,如果二者互为扰动字符串,没有别

的选择,必须是 s[i] == t[j]。

所以初始状态就是:

for (int i = 0;i < s1.length();i++)

for (int j = 0;j < s2.length();j++)

if (s1.charAt(i) == s2.charAt(j))

f[i][j][1] = true;

搞定了初始状态,我们就要考虑接下来的动态转移方程了。

观察题目的条件 “随机 决定是要「交换两个子字符串」还是要「保持这两个子字符串的顺

序不变」。即,在执行这一步骤之后,_s_ 可能是 _s = x + y_ 或者 _s = y + x_ 。”

假设 s = x1 + y1,t = x2 + y2,要判断 s 和 t 互为扰动字符串,需要判断以下两种情

况:

• x1 与 x2 互为扰动字符串,且 y1 与 y2 互为扰动字符串

• x1 与 y2 互为扰动字符串,且 y1 与 x2 互为扰动字符串

上述两种情况分别对应了「保持这两个子字符串的顺序不变」和 「交换两个子字符串」

的情况。

我们可以不断找出 s 和 t 的所有拆分方法,然后依次判断这其中是否存在能使得上述两种

情况成立的一个可行办法,只要有,s 和 t 就互为扰动字符串。

所以动态转移方程就是:

$

$

详细解释一下这个动态转移方程:(以下讲解中把从 i 开始,长度为 len 的 s1 的子串记为

s,从 j 开始,长度为 len 的 s2 的子串记为 t)

• $ [f(i,j,k) & f(i+k,j+k,len-k)] $ 表示我们把长度为 len 的 s 和 t 分成两部分,一部分长度

为 k,另外一部分长度则为 len-k,这时,k 是它们的分隔点,且没有经过交换,即_

「保持这两个子字符串的顺序不变」_的情况

• $ f(i,j+len-k,k) & f(i+k,j,len-k) $ 表示 k 还是他们的分割点,但是这次是_「交换两个子

字符串」_的情况。

最终的答案,存储在 f[0][0][len]中,因为 f[0][0][s.length]的含义就是从 s 的 0 位置开

始,长度为 s.length 的字符串与 t 的从 0 位置开始,长度为 s.length 的字符串互为扰动字

符串。

来看最终的题解。

class Solution {

public boolean isScramble(String s1, String s2) {

// 如果两个字符串相等,直接返回 true

if (s1.equals(s2)){

return true;

}

// 如果两个字符串的长度不相同,那么它们不能是扰乱字符串,返回 false

if (s1.length() != s2.length()){

return false;

}

// f[i][j][len] 表示从 s1 的第 i 位开始、从 s2 的第 j 位开始、长度为 len 的子串,是否为

扰乱字符串

boolean[][][] f = new boolean[s1.length()][s2.length()][s1.length() + 1];

// 初始化:如果 s1 和 s2 的相应位置字符相同,则长度为 1 的子串是扰乱字符串

for (int i = 0;i < s1.length();i++)

for (int j = 0;j < s2.length();j++)

if (s1.charAt(i) == s2.charAt(j))

f[i][j][1] = true;

// 外层循环:遍历所有可能的子串长度

for (int len = 2;len <= s1.length();len++)

// i 和 j 是两个子串的起始位置

for (int i = 0;i <= s1.length() - len;i++)

for (int j = 0;j <= s2.length() - len;j++) {

// k 是区间 [i, i+len) 和 [j, j+len) 的划分点

for (int k = 1;k < len;k++) {

// 检查是否存在一种划分方式,使得两个子串是扰乱字符串

if ((f[i][j][k] && f[i + k][j + k][len - k])

|| (f[i][j + len - k][k] && f[i + k][j][len - k])) {

f[i][j][len] = true;

break;

}

}

}

// 返回整个 s1 和 s2 是否为扰乱字符串

return f[0][0][s1.length()];

}

}

解释:

• 我们使用一个三维数组 f 来保存状态,其中 f[i][j][len] 表示从 s1 的第 i 位开始、从

s2 的第 j 位开始、长度为 len 的子串是否为扰乱字符串。

• 我们首先初始化长度为 1 的子串。如果 s1 和 s2 的相应位置上的字符相同,那么这两

个子串显然是扰乱字符串。

• 之后,我们从长度为 2 的子串开始,逐渐增加子串的长度。对于每一种长度,我们枚

举所有可能的起始位置 i 和 j,然后尝试所有可能的分割方式(通过变量 k)。

• 对于每一种分割方式,我们检查是否存在一种情况,使得两个子串是扰乱字符串。这

可以是 f[i][j][k] 和 f[i + k][j + k][len - k] 都为 true,或者是 f[i][j + len - k][k] 和 f[i

+ k][j][len - k] 都为 true。这两种情况分别对应于不交换和交换两个子串。

• 最后,我们返回 f[0][0][s1.length()],这表示整个 s1 和 s2 是否为扰乱字符串。

考虑两个字符串 s1 = "great" 和 s2 = "rgeat"。我们来验证 s2 是否是 s1 的扰乱字符

串。

1. 初始化:

我们首先查看每个字符位置,如果 s1 和 s2 在该位置上的字符相同,则对应长度为 1 的子

串标记为扰乱字符串。

s1 = g r e a t

s2 = r g e a t

f = F F T T T (对于长度为 1 的子串)

2. 扩展到长度为 2 的子串:

我们看 s1 的前两个字符 “gr” 和 s2 的前两个字符 “rg”。由于 “g” 和 “r” 可以交换位置,所

以 “gr” 和 “rg” 互为扰乱字符串。

s1 = g r e a t

| |

s2 = r g e a t

| |

f = T (对于长度为 2 的子串,从位置 0 开始)

3. 扩展到更长的子串:

接下来,我们可以考虑长度为 3 的子串 “gre” 和 “rge”。由于 “gr” 和 “rg” 互为扰乱字符

串,并且它们后面的字符 “e” 是相同的,所以 “gre” 和 “rge” 也互为扰乱字符串。

s1 = g r e a t

| | |

s2 = r g e a t

| | |

f = T (对于长度为 3 的子串,从位置 0 开始)

通过上面的步骤,我们可以继续考虑更长的子串,并尝试所有可能的分割和交换,直到得

出整个字符串是否为扰乱字符串。

来看题解的效率,还行。

087.%E6%89%B0%E4%B9%B1%E5%AD%97%E7%AC%A6%E4%B8%B2-

20231008161127.png

总结

动态规划的难点主要集中在如何找到 状态 和 确定动态转移方程 上,状态需要我们去认真

观察,并且有时候需要去总结一些结论才能找出合适的状态,而动态转移方程,题目往往

会以很规整的格式给出,我们需要学会细心总结。

力扣链接:https://leetcode.cn/problems/scramble-string/

开通会员 本次下载免费

所有资料全部免费下载! 推荐用户付费下载获取返佣积分! 积分可以兑换商品!
一键复制 下载文档 联系客服