LeetCode 97. 交错字符串:动态规划详解

在LeetCode中等难度题目中,「交错字符串」是一道经典的动态规划应用题。它的核心是判断一个字符串是否能由另外两个字符串"交错"组成,看似简单却容易陷入思维误区,今天我们就来一步步拆解这道题,从题目理解到代码实现,把每一个细节讲透。

一、题目核心理解

先明确题目要求:给定三个字符串s1、s2、s3,验证s3是否是由s1和s2交错组成的。这里的「交错」有严格定义,我们用通俗的话翻译一下:

  • 将s1分割成若干个非空子串(比如s1 = a + b + c),s2分割成若干个非空子串(比如s2 = x + y);

  • 分割后两个字符串的子串数量相差不超过1(|n - m| ≤ 1);

  • 把这两组子串交替拼接,要么是「s1子串在前、s2子串在后」(a+x + b+y + c),要么是「s2子串在前、s1子串在后」(x+a + y+b),最终拼接结果等于s3。

举个例子:s1 = "aabcc",s2 = "dbbca",s3 = "aadbbcbcac",就是一个合法的交错组合------s1分割为"aa"+"bc"+"c",s2分割为"dbbc"+"a",交替拼接后得到"aa"+"dbbc"+"bc"+"a"+"c",正好等于s3。

而如果s3的长度不等于s1+s2的长度,那直接可以判定为false,这是最基础的边界条件。

二、解题思路:为什么用动态规划?

拿到这道题,首先会想到暴力解法------枚举s1和s2的所有分割方式,再判断拼接后是否等于s3。但这种方法的时间复杂度极高,因为分割方式的数量是指数级的,对于稍长的字符串完全不适用。

这时候就需要动态规划(DP)来优化。动态规划的核心是「状态定义+状态转移」,我们可以通过定义一个DP数组,记录"用s1的前i个字符和s2的前j个字符,能否组成s3的前i+j个字符",这样就能把大问题拆解成小问题,逐步递推求解。

三、动态规划细节拆解

1. 状态定义

定义dp[i][j]:表示s1的前i个字符(s1[0..i-1])和s2的前j个字符(s2[0..j-1]),能否交错组成s3的前i+j个字符(s3[0..i+j-1])。

这里要注意下标细节:i和j从0开始,当i=0时,意味着不使用s1的任何字符,只使用s2的前j个字符;当j=0时,意味着不使用s2的任何字符,只使用s1的前i个字符。

2. 初始化

初始化的核心是处理"只使用s1"或"只使用s2"的情况:

  • dp[0][0] = true:s1的前0个字符(空字符串)和s2的前0个字符(空字符串),能组成s3的前0个字符(空字符串),这是基础条件。

  • 当i>0、j=0时:dp[i][0] = dp[i-1][0] && s1[i-1] === s3[i-1]。也就是说,只有前i-1个字符能组成s3的前i-1个字符,且s1的第i个字符(s1[i-1])等于s3的第i个字符(s3[i-1]),才能满足条件。

  • 当j>0、i=0时:类似上面,dp[0][j] = dp[0][j-1] && s2[j-1] === s3[j-1]。

3. 状态转移方程

对于任意i>0、j>0的情况,dp[i][j]的取值有两种可能,只要满足其中一种,就为true:

  1. 最后一个字符来自s1:此时需要满足「s1的前i-1个字符和s2的前j个字符能组成s3的前i+j-1个字符」(即dp[i-1][j]为true),并且s1的第i个字符(s1[i-1])等于s3的第i+j个字符(s3[i+j-1])。

  2. 最后一个字符来自s2:此时需要满足「s1的前i个字符和s2的前j-1个字符能组成s3的前i+j-1个字符」(即dp[i][j-1]为true),并且s2的第j个字符(s2[j-1])等于s3的第i+j个字符(s3[i+j-1])。

因此,状态转移方程可以写成:

dp[i][j] = (dp[i-1][j] && s1[i-1] === s3[i+j-1]) || (dp[i][j-1] && s2[j-1] === s3[i+j-1])

4. 最终结果

dp[s1.length][s2.length] 就是我们要的答案------表示s1的全部字符和s2的全部字符,能否交错组成s3的全部字符。

四、完整代码及逐行解读

下面是完整的TypeScript代码,结合上面的思路,逐行解读每一步的作用:

typescript 复制代码
function isInterleave(s1: string, s2: string, s3: string): boolean {
  // 1. 边界条件:s3长度不等于s1+s2长度,直接返回false
  const l1: number = s1.length;
  const l2: number = s2.length;
  const l3: number = s3.length;
  if (l1 + l2 != l3) {
    return false;
  }

  // 2. 初始化DP数组:dp[i][j]表示s1前i个、s2前j个能否组成s3前i+j个
  const dp: boolean[][] = Array.from({ length: l1 + 1 }, () => new Array(l2 + 1).fill(false))
  dp[0][0] = true; // 空字符组成空字符,基础条件

  // 3. 填充DP数组:双重循环遍历所有i和j的组合
  for (let i = 0; i <= l1; i++) {
    for (let j = 0; j <= l2; j++) {
      const p = i + j - 1; // s3当前对应的下标(前i+j个字符的最后一个下标)
      // 情况1:最后一个字符来自s1(i>0才有可能)
      if (i > 0) {
        dp[i][j] = (dp[i - 1][j] && s1[i - 1] === s3[p]) || dp[i][j];
      }
      // 情况2:最后一个字符来自s2(j>0才有可能)
      if (j > 0) {
        dp[i][j] = (dp[i][j - 1] && s2[j - 1] === s3[p]) || dp[i][j];
      }
    }
  }

  // 4. 返回最终结果:s1全部和s2全部能否组成s3全部
  return dp[l1][l2];
};

五、关键注意点&优化方向

1. 下标细节(最容易踩坑)

一定要注意:s1的第i个字符对应的下标是i-1,s2的第j个字符对应的下标是j-1,s3的前i+j个字符的最后一个下标是i+j-1(即变量p)。很多人会在这里混淆下标,导致代码出错。

2. 空间优化

上面的代码使用了二维DP数组,空间复杂度是O(l1*l2)。但观察状态转移方程可以发现,dp[i][j]只依赖于dp[i-1][j](上一行)和dp[i][j-1](同一行前一列),因此可以优化为一维DP数组,将空间复杂度降低到O(min(l1, l2))。

优化思路:用一个一维数组dp[j],每次遍历i时,更新dp[j]的值,具体可以自行尝试(提示:遍历i时,dp[j]的更新需要注意顺序,避免覆盖未使用的值)。

3. 特殊测试用例

提交代码前,建议测试以下几个特殊用例,避免边界漏洞:

  • s1、s2均为空:s3也为空 → 返回true;s3非空 → 返回false。

  • 其中一个字符串为空:比如s1为空,判断s2是否等于s3;反之亦然。

  • 字符重复场景:比如s1="aabcc",s2="dbbca",s3="aadbbbaccc" → 返回false(最后一个c的来源不匹配)。

六、总结

「交错字符串」的核心是用动态规划将"分割拼接"的复杂问题,转化为"逐步判断字符匹配"的子问题。关键在于正确定义DP状态,理清状态转移的两种情况,同时注意下标细节。

这道题的DP思路具有通用性,类似"两个字符串拼接成第三个字符串"的问题,都可以尝试用类似的状态定义来解决。掌握了这道题,也能加深对动态规划"递推思想"的理解。

相关推荐
木斯佳1 小时前
前端八股文面经大全:字节暑期前端一面(2026-04-24)·面经深度解析
前端
凯瑟琳.奥古斯特1 小时前
Redis是什么及核心特性
前端·css·redis·缓存
爱学习的张大1 小时前
具身智能论文问答(三):Open VLA
人工智能·算法
架构源启1 小时前
OpenClaw 只能手动写脚本?我用 Chrome 插件实现了“录制即生成“
前端·人工智能·chrome·自动化
yingyima1 小时前
正则表达式实战:如何高效清洗脏数据
前端
兔子零10241 小时前
Ofox AI值得用吗?
前端·javascript·后端
wearegogog1231 小时前
基于Q-learning的栅格地图路径规划MATLAB仿真程序
开发语言·算法·matlab
旖-旎2 小时前
深搜练习(组合总和)(7)
c++·算法·深度优先·力扣
小O的算法实验室2 小时前
2026年ASOC,基于人工势场的差分进化算法改进框架,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进