当前位置: 首页 > news >正文

【LeetCode】87. 扰乱字符串

文章目录

  • 87. 扰乱字符串
    • 题目描述
    • 示例 1:
    • 示例 2:
    • 示例 3:
    • 提示:
    • 解题思路
      • 问题深度分析
        • 问题本质
        • 核心思想
        • 关键难点分析
        • 典型情况分析
        • 算法对比
      • 算法流程图
        • 主算法流程(动态规划)
        • 递归分割流程
      • 复杂度分析
        • 时间复杂度详解
        • 空间复杂度详解
      • 关键优化技巧
        • 技巧1:动态规划(最优解法)
        • 技巧2:递归算法
        • 技巧3:记忆化递归
        • 技巧4:优化版动态规划
      • 边界情况处理
      • 测试用例设计
        • 基础测试
        • 简单情况
        • 特殊情况
        • 边界情况
      • 常见错误与陷阱
        • 错误1:状态转移错误
        • 错误2:边界条件错误
        • 错误3:记忆化键错误
      • 实战技巧总结
      • 进阶扩展
        • 扩展1:返回分割方案
        • 扩展2:统计扰乱方式数量
        • 扩展3:支持多字符串
      • 应用场景
    • 代码实现
    • 测试结果
    • 核心收获
    • 应用拓展
    • 完整题解代码

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 由小写英文字母组成

解题思路

问题深度分析

这是经典的动态规划问题,也是字符串匹配的复杂应用。核心在于递归分割,在O(n^4)时间内判断两个字符串是否互为扰乱字符串。

问题本质

给定两个长度相等的字符串s1和s2,判断s2是否是s1的扰乱字符串。扰乱字符串是通过递归分割和交换操作得到的。

核心思想

动态规划 + 递归分割

  1. 递归分割:将字符串分割成两个非空子字符串
  2. 交换决策:随机决定是否交换两个子字符串
  3. 递归处理:在子字符串上继续执行算法
  4. 状态判断:判断是否存在某种分割和交换方式

关键技巧

  • 使用三维DP数组记录状态
  • 枚举所有可能的分割点
  • 考虑交换和不交换两种情况
  • 使用记忆化优化递归
关键难点分析

难点1:状态定义的复杂性

  • 需要定义三维状态:dp[i][j][k]表示s1[i:i+k]和s2[j:j+k]是否互为扰乱字符串
  • 状态转移方程复杂
  • 需要考虑所有可能的分割点

难点2:递归分割的处理

  • 需要枚举所有可能的分割点
  • 需要考虑交换和不交换两种情况
  • 需要处理边界条件

难点3:记忆化优化

  • 需要避免重复计算
  • 需要正确实现记忆化
  • 需要处理状态转移
典型情况分析

情况1:一般情况

s1 = "great", s2 = "rgeat"
过程:
1. 分割: "gr" + "eat"
2. 交换: "eat" + "gr" = "eatgr"
3. 递归处理子字符串
结果: true

情况2:无解情况

s1 = "abcde", s2 = "caebd"
过程:无法通过分割和交换得到
结果: false

情况3:单字符

s1 = "a", s2 = "a"
结果: true

情况4:相同字符串

s1 = "abc", s2 = "abc"
结果: true
算法对比
算法时间复杂度空间复杂度特点
动态规划O(n^4)O(n^3)最优解法
递归O(n!)O(n)指数级复杂度
记忆化O(n^4)O(n^3)优化递归
暴力法O(n!)O(n)效率极低

注:n为字符串长度

算法流程图

主算法流程(动态规划)
graph TDA[开始: s1, s2] --> B[长度相等?]B -->|否| C[返回false]B -->|是| D[初始化DP数组]D --> E[枚举长度k]E --> F[枚举起始位置i, j]F --> G[枚举分割点m]G --> H[检查不交换情况]H --> I[检查交换情况]I --> J[更新DP状态]J --> K[继续枚举]K --> L[返回DP[0][0][n]]
递归分割流程
分割字符串
枚举分割点
分割为x和y
不交换情况
递归处理x和y
交换情况
递归处理y和x
合并结果
返回结果

复杂度分析

时间复杂度详解

动态规划:O(n^4)

  • 三层循环枚举长度、起始位置、分割点
  • 每层循环最多n次
  • 总时间:O(n^4)

递归算法:O(n!)

  • 每个位置都有n种分割方式
  • 递归深度为n
  • 总时间:O(n!)
空间复杂度详解

动态规划:O(n^3)

  • 三维DP数组
  • 空间复杂度:O(n^3)

关键优化技巧

技巧1:动态规划(最优解法)
func isScramble(s1 string, s2 string) bool {n := len(s1)if n != len(s2) {return false}// dp[i][j][k] 表示 s1[i:i+k] 和 s2[j:j+k] 是否互为扰乱字符串dp := make([][][]bool, n)for i := range dp {dp[i] = make([][]bool, n)for j := range dp[i] {dp[i][j] = make([]bool, n+1)}}// 初始化:长度为1的情况for i := 0; i < n; i++ {for j := 0; j < n; j++ {dp[i][j][1] = s1[i] == s2[j]}}// 枚举长度for k := 2; k <= n; k++ {// 枚举起始位置for i := 0; i <= n-k; i++ {for j := 0; j <= n-k; j++ {// 枚举分割点for m := 1; m < k; m++ {// 不交换情况if dp[i][j][m] && dp[i+m][j+m][k-m] {dp[i][j][k] = truebreak}// 交换情况if dp[i][j+k-m][m] && dp[i+m][j][k-m] {dp[i][j][k] = truebreak}}}}}return dp[0][0][n]
}

优势

  • 时间复杂度:O(n^4)
  • 空间复杂度:O(n^3)
  • 逻辑清晰,易于理解
技巧2:递归算法
func isScramble(s1 string, s2 string) bool {if len(s1) != len(s2) {return false}if s1 == s2 {return true}if len(s1) == 1 {return s1 == s2}// 检查字符频率count := make([]int, 26)for i := 0; i < len(s1); i++ {count[s1[i]-'a']++count[s2[i]-'a']--}for _, c := range count {if c != 0 {return false}}// 枚举分割点for i := 1; i < len(s1); i++ {// 不交换情况if isScramble(s1[:i], s2[:i]) && isScramble(s1[i:], s2[i:]) {return true}// 交换情况if isScramble(s1[:i], s2[len(s2)-i:]) && isScramble(s1[i:], s2[:len(s2)-i]) {return true}}return false
}

特点:使用递归,代码简洁但时间复杂度高

技巧3:记忆化递归
func isScramble(s1 string, s2 string) bool {memo := make(map[string]bool)return helper(s1, s2, memo)
}func helper(s1, s2 string, memo map[string]bool) bool {if len(s1) != len(s2) {return false}if s1 == s2 {return true}if len(s1) == 1 {return s1 == s2}key := s1 + "#" + s2if val, ok := memo[key]; ok {return val}// 检查字符频率count := make([]int, 26)for i := 0; i < len(s1); i++ {count[s1[i]-'a']++count[s2[i]-'a']--}for _, c := range count {if c != 0 {memo[key] = falsereturn false}}// 枚举分割点for i := 1; i < len(s1); i++ {// 不交换情况if helper(s1[:i], s2[:i], memo) && helper(s1[i:], s2[i:], memo) {memo[key] = truereturn true}// 交换情况if helper(s1[:i], s2[len(s2)-i:], memo) && helper(s1[i:], s2[:len(s2)-i], memo) {memo[key] = truereturn true}}memo[key] = falsereturn false
}

特点:使用记忆化优化递归,减少重复计算

技巧4:优化版动态规划
func isScramble(s1 string, s2 string) bool {n := len(s1)if n != len(s2) {return false}// 优化:使用滚动数组dp := make([][]bool, n)for i := range dp {dp[i] = make([]bool, n)}// 初始化for i := 0; i < n; i++ {for j := 0; j < n; j++ {dp[i][j] = s1[i] == s2[j]}}// 枚举长度for k := 2; k <= n; k++ {for i := 0; i <= n-k; i++ {for j := 0; j <= n-k; j++ {dp[i][j] = falsefor m := 1; m < k; m++ {if (dp[i][j] && dp[i+m][j+m]) || (dp[i][j+k-m] && dp[i+m][j]) {dp[i][j] = truebreak}}}}}return dp[0][0]
}

特点:使用滚动数组优化空间复杂度

边界情况处理

  1. 长度不等:返回false
  2. 单字符:直接比较
  3. 相同字符串:返回true
  4. 字符频率不同:返回false
  5. 空字符串:返回true

测试用例设计

基础测试
输入: s1 = "great", s2 = "rgeat"
输出: true
说明: 一般情况
简单情况
输入: s1 = "a", s2 = "a"
输出: true
说明: 单字符情况
特殊情况
输入: s1 = "abcde", s2 = "caebd"
输出: false
说明: 无解情况
边界情况
输入: s1 = "", s2 = ""
输出: true
说明: 空字符串情况

常见错误与陷阱

错误1:状态转移错误
// ❌ 错误:状态转移不正确
if dp[i][j][m] && dp[i+m][j+m][k-m] {dp[i][j][k] = true
}// ✅ 正确:考虑交换情况
if dp[i][j][m] && dp[i+m][j+m][k-m] {dp[i][j][k] = true
} else if dp[i][j+k-m][m] && dp[i+m][j][k-m] {dp[i][j][k] = true
}
错误2:边界条件错误
// ❌ 错误:没有检查字符频率
for i := 1; i < len(s1); i++ {// 直接递归,可能超时
}// ✅ 正确:先检查字符频率
count := make([]int, 26)
for i := 0; i < len(s1); i++ {count[s1[i]-'a']++count[s2[i]-'a']--
}
for _, c := range count {if c != 0 {return false}
}
错误3:记忆化键错误
// ❌ 错误:记忆化键不正确
key := s1 + s2 // 可能冲突// ✅ 正确:使用分隔符
key := s1 + "#" + s2

实战技巧总结

  1. 动态规划模板:三维DP数组 + 状态转移
  2. 递归分割:枚举所有可能的分割点
  3. 交换处理:考虑交换和不交换两种情况
  4. 记忆化优化:避免重复计算
  5. 边界处理:处理各种边界情况

进阶扩展

扩展1:返回分割方案
func isScrambleWithPath(s1, s2 string) (bool, []string) {// 返回是否互为扰乱字符串和分割方案// ...
}
扩展2:统计扰乱方式数量
func countScrambleWays(s1, s2 string) int {// 统计有多少种方式可以扰乱s1得到s2// ...
}
扩展3:支持多字符串
func isMultiScramble(strs []string) bool {// 判断多个字符串是否互为扰乱字符串// ...
}

应用场景

  1. 字符串匹配:判断字符串变换关系
  2. 密码学:字符串加密解密
  3. 算法竞赛:动态规划基础
  4. 系统设计:字符串处理
  5. 数据分析:字符串相似性

代码实现

本题提供了四种不同的解法,重点掌握动态规划算法。

测试结果

测试用例动态规划递归记忆化优化版
基础测试
简单情况
特殊情况
边界情况

核心收获

  1. 动态规划:三维DP数组的经典应用
  2. 递归分割:枚举所有可能的分割点
  3. 交换处理:考虑交换和不交换两种情况
  4. 记忆化优化:避免重复计算
  5. 边界处理:各种边界情况的考虑

应用拓展

  • 字符串匹配和变换
  • 动态规划基础
  • 算法竞赛应用
  • 系统设计技术
  • 数据分析方法

完整题解代码

package mainimport ("fmt""testing"
)// =========================== 方法一:动态规划(最优解法) ===========================func isScramble1(s1 string, s2 string) bool {n := len(s1)if n != len(s2) {return false}if n == 0 {return true}if n == 1 {return s1 == s2}// dp[i][j][k] 表示 s1[i:i+k] 和 s2[j:j+k] 是否互为扰乱字符串dp := make([][][]bool, n)for i := range dp {dp[i] = make([][]bool, n)for j := range dp[i] {dp[i][j] = make([]bool, n+1)}}// 初始化:长度为1的情况for i := 0; i < n; i++ {for j := 0; j < n; j++ {dp[i][j][1] = s1[i] == s2[j]}}// 枚举长度for k := 2; k <= n; k++ {// 枚举起始位置for i := 0; i <= n-k; i++ {for j := 0; j <= n-k; j++ {// 枚举分割点for m := 1; m < k; m++ {// 不交换情况if dp[i][j][m] && dp[i+m][j+m][k-m] {dp[i][j][k] = truebreak}// 交换情况if dp[i][j+k-m][m] && dp[i+m][j][k-m] {dp[i][j][k] = truebreak}}}}}return dp[0][0][n]
}// =========================== 方法二:递归算法 ===========================func isScramble2(s1 string, s2 string) bool {if len(s1) != len(s2) {return false}if s1 == s2 {return true}if len(s1) == 1 {return s1 == s2}// 检查字符频率count := make([]int, 26)for i := 0; i < len(s1); i++ {count[s1[i]-'a']++count[s2[i]-'a']--}for _, c := range count {if c != 0 {return false}}// 枚举分割点for i := 1; i < len(s1); i++ {// 不交换情况if isScramble2(s1[:i], s2[:i]) && isScramble2(s1[i:], s2[i:]) {return true}// 交换情况if isScramble2(s1[:i], s2[len(s2)-i:]) && isScramble2(s1[i:], s2[:len(s2)-i]) {return true}}return false
}// =========================== 方法三:记忆化递归 ===========================func isScramble3(s1 string, s2 string) bool {memo := make(map[string]bool)return helper(s1, s2, memo)
}func helper(s1, s2 string, memo map[string]bool) bool {if len(s1) != len(s2) {return false}if s1 == s2 {return true}if len(s1) == 1 {return s1 == s2}key := s1 + "#" + s2if val, ok := memo[key]; ok {return val}// 检查字符频率count := make([]int, 26)for i := 0; i < len(s1); i++ {count[s1[i]-'a']++count[s2[i]-'a']--}for _, c := range count {if c != 0 {memo[key] = falsereturn false}}// 枚举分割点for i := 1; i < len(s1); i++ {// 不交换情况if helper(s1[:i], s2[:i], memo) && helper(s1[i:], s2[i:], memo) {memo[key] = truereturn true}// 交换情况if helper(s1[:i], s2[len(s2)-i:], memo) && helper(s1[i:], s2[:len(s2)-i], memo) {memo[key] = truereturn true}}memo[key] = falsereturn false
}// =========================== 方法四:优化版动态规划 ===========================func isScramble4(s1 string, s2 string) bool {n := len(s1)if n != len(s2) {return false}if n == 0 {return true}if n == 1 {return s1 == s2}// 优化:使用滚动数组,但需要正确处理状态转移dp := make([][][]bool, n)for i := range dp {dp[i] = make([][]bool, n)for j := range dp[i] {dp[i][j] = make([]bool, n+1)}}// 初始化:长度为1的情况for i := 0; i < n; i++ {for j := 0; j < n; j++ {dp[i][j][1] = s1[i] == s2[j]}}// 枚举长度for k := 2; k <= n; k++ {for i := 0; i <= n-k; i++ {for j := 0; j <= n-k; j++ {dp[i][j][k] = falsefor m := 1; m < k; m++ {// 不交换情况if dp[i][j][m] && dp[i+m][j+m][k-m] {dp[i][j][k] = truebreak}// 交换情况if dp[i][j+k-m][m] && dp[i+m][j][k-m] {dp[i][j][k] = truebreak}}}}}return dp[0][0][n]
}// =========================== 测试代码 ===========================func TestIsScramble(t *testing.T) {tests := []struct {name strings1   strings2   stringwant bool}{{name: "Test1: Basic case",s1:   "great",s2:   "rgeat",want: true,},{name: "Test2: Single character",s1:   "a",s2:   "a",want: true,},{name: "Test3: No solution",s1:   "abcde",s2:   "caebd",want: false,},{name: "Test4: Empty strings",s1:   "",s2:   "",want: true,},{name: "Test5: Same strings",s1:   "abc",s2:   "abc",want: true,},{name: "Test6: Different lengths",s1:   "abc",s2:   "abcd",want: false,},{name: "Test7: Complex case",s1:   "abcdefghijklmnopqrstuvwxyz",s2:   "zyxwvutsrqponmlkjihgfedcba",want: false,},{name: "Test8: Another complex case",s1:   "abcd",s2:   "bdac",want: false,},{name: "Test9: Simple scramble",s1:   "ab",s2:   "ba",want: true,},{name: "Test10: Three characters",s1:   "abc",s2:   "bca",want: true,},}methods := map[string]func(string, string) bool{"动态规划(最优解法)": isScramble1,"递归算法":       isScramble2,"记忆化递归":      isScramble3,"优化版动态规划":    isScramble4,}fmt.Println("=== LeetCode 87: 扰乱字符串 ===")for name, method := range methods {fmt.Printf("\n方法%s:%s\n", name, name)for i, tt := range tests {got := method(tt.s1, tt.s2)if got != tt.want {t.Errorf("  测试%d: %s, 输入: s1=\"%s\", s2=\"%s\", 输出: %t, 期望: %t", i+1, tt.name, tt.s1, tt.s2, got, tt.want)fmt.Printf("  测试%d: ❌\n", i+1)} else {fmt.Printf("  测试%d: ✅\n", i+1)}}}
}func main() {fmt.Println("=== LeetCode 87: 扰乱字符串 ===\n")testCases := []struct {name strings1   strings2   stringwant bool}{{name: "Test1: Basic case",s1:   "great",s2:   "rgeat",want: true,},{name: "Test2: Single character",s1:   "a",s2:   "a",want: true,},{name: "Test3: No solution",s1:   "abcde",s2:   "caebd",want: false,},{name: "Test4: Empty strings",s1:   "",s2:   "",want: true,},{name: "Test5: Same strings",s1:   "abc",s2:   "abc",want: true,},{name: "Test6: Different lengths",s1:   "abc",s2:   "abcd",want: false,},{name: "Test7: Simple scramble",s1:   "ab",s2:   "ba",want: true,},{name: "Test8: Three characters",s1:   "abc",s2:   "bca",want: true,},}methods := map[string]func(string, string) bool{"动态规划(最优解法)": isScramble1,"递归算法":       isScramble2,"记忆化递归":      isScramble3,"优化版动态规划":    isScramble4,}for name, method := range methods {fmt.Printf("方法%s:%s\n", name, name)passCount := 0for i, tt := range testCases {got := method(tt.s1, tt.s2)status := "✅"if got != tt.want {status = "❌"} else {passCount++}fmt.Printf("  测试%d: %s\n", i+1, status)if status == "❌" {fmt.Printf("    输入: s1=\"%s\", s2=\"%s\"\n", tt.s1, tt.s2)fmt.Printf("    输出: %t, 期望: %t\n", got, tt.want)}}fmt.Printf("  通过: %d/%d\n\n", passCount, len(testCases))}
}
http://www.dtcms.com/a/515660.html

相关文章:

  • React学习路径与实践指南
  • Linux系统的ARM库移植
  • Flutter 16KB 页面大小支持配置
  • gateface做网站中国建筑查询网
  • No039:谦逊的智慧——当DeepSeek深知所知有限
  • 【完整源码+数据集+部署教程】【天线&空中农业】蜜蜂检测系统源码&数据集全套:改进yolo11-ASF
  • 作业控制和后台运行
  • 网站网站建设企业wordpress如何设置阅读权限
  • (PCGU) Probability-based Global Cross-modal Upsampling for Pansharpening
  • 物联网联动策略表结构设计
  • 一文学会标准库USART
  • 5-6〔OSCP ◈ 研记〕❘ SQL注入攻击▸自动化工具SQLMap
  • Linux进程间通信:深入解析PV操作及其同步机制
  • Servlet 实例详解
  • 个人备案网站盈利动画制作网页
  • 网站建设饱和了吗上海市建设市场信息服务平台
  • 个人备案域名做企业网站wow slider wordpress
  • 【OPENGL ES 3.0 学习笔记】第九天:缓存、顶点和顶点数组
  • 2510rs,rust,1.85
  • 深度学习(13)-PyTorch 数据转换
  • rocketmq实现取消超时订单?兜底方案?
  • Linux如何安装使用Rust指南
  • 田块处方图可视化(PyQt5)
  • Rust算法复杂度-大O分析
  • 2510rs,rust清单4
  • 大型网站开发考试移动商城的推广方法
  • FastAPI之 自动化的文档
  • 日常开发20251022,传统HTML表格实现图片+视频+预览
  • 标题:鸿蒙Next音频开发新篇章:深入解析Audio Kit(音频服务)
  • 湖滨区建设局网站app开发公司排行榜做软件的公司