好的,我们开始一个新题目。
线性动态规划:交错字符串
题目描述
给定三个字符串 s1, s2 和 s3。你需要判断 s3 是否可以由 s1 和 s2 交错 组成。交错的定义是:保持 s1 和 s2 中每个字符的原有相对顺序,将它们混合在一起。
示例 1:
输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出: true
解释: 交错方式是:s1中的 “a” + s1中的 “a” + s2中的 “d” + s1中的 “b” + s2中的 “b” + s1中的 “c” + s2中的 “c” + s1中的 “c”。
即:aa d b b c c a c (用空格分隔只是为了显示s1和s2的来源,实际字符串是连续的)
示例 2:
输入: s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbbaccc”
输出: false
解题过程循序渐进讲解
这是一个经典的二维线性动态规划问题。我们的目标是判断能否用 s1 和 s2 “编织” 出 s3,关键在于保持每个原字符串内部的字符顺序。
第一步:定义状态
我们定义一个二维布尔数组 dp[i][j],其含义如下:
i表示从s1中已经使用了前i个字符(索引从 1 开始计数,i的范围是0到len(s1))。j表示从s2中已经使用了前j个字符。dp[i][j]表示:能否由s1的前i个字符和s2的前j个字符,交错组成s3的前(i+j)个字符。
dp[0][0] 表示两个空字符串能否交错组成空字符串,显然是 true。
第二步:状态转移方程
我们如何计算 dp[i][j] 呢?s3 的前 (i+j) 个字符的最后一个字符,要么来自 s1 的第 i 个字符,要么来自 s2 的第 j 个字符(如果 i 或 j 为 0,则只能来自其中一个)。
-
来自 s1:
- 前提条件:
i > 0(s1还有字符可用)。 - 逻辑判断:
s1[i-1](因为字符串索引从0开始)必须等于s3[i+j-1](s3的前(i+j)个字符的最后一个)。 - 如果这个字符来自s1,那么
dp[i][j]的真假就取决于:能否由s1的前(i-1)个字符和s2的前j个字符组成s3的前(i+j-1)个字符,也就是dp[i-1][j]。
- 前提条件:
-
来自 s2:
- 前提条件:
j > 0。 - 逻辑判断:
s2[j-1]必须等于s3[i+j-1]。 - 如果这个字符来自s2,那么
dp[i][j]的真假就取决于:能否由s1的前i个字符和s2的前(j-1)个字符组成s3的前(i+j-1)个字符,也就是dp[i][j-1]。
- 前提条件:
dp[i][j] 为真的条件是:来自s1或来自s2的两种情况,只要有一种成立即可。
因此,状态转移方程为:
如果 i > 0 且 s1[i-1] == s3[i+j-1]: 情况1 = dp[i-1][j]
如果 j > 0 且 s2[j-1] == s3[i+j-1]: 情况2 = dp[i][j-1]
dp[i][j] = 情况1 OR 情况2
(当 i 和 j 都为0时,是一种特殊情况,初始化为 true)
第三步:初始化
我们需要初始化第一行和第一列,这代表了只用其中一个字符串去匹配 s3 的前缀。
dp[0][j]:表示只用s2的前j个字符去匹配s3的前j个字符。只有当s2的前j个字符完全等于s3的前j个字符时,才为true。dp[0][0] = true(两个空字符串匹配空字符串)。- 对于
j >= 1:dp[0][j] = dp[0][j-1] && (s2[j-1] == s3[j-1])。意思是,前j-1个字符已经能匹配上,并且第j个字符也相等。
dp[i][0]:表示只用s1的前i个字符去匹配s3的前i个字符。逻辑类似:dp[i][0] = dp[i-1][0] && (s1[i-1] == s3[i-1])
第四步:计算顺序
我们的状态 dp[i][j] 依赖于它左边 dp[i][j-1] 和上方 dp[i-1][j] 的值。
因此,计算顺序应该是:从左到右,从上到下地遍历这个二维表格。外层循环遍历 i(0到len(s1)),内层循环遍历 j(0到len(s2))。
第五步:最终结果
我们最终想知道,能否用 s1 的全部(len(s1) 个字符)和 s2 的全部(len(s2) 个字符)交错组成 s3 的全部(len(s3) = len(s1)+len(s2) 个字符)。
所以,答案就是 dp[len(s1)][len(s2)]。
让我们通过一个简单的例子来走一遍流程
s1 = “a”, s2 = “b”, s3 = “ab”
长度:len1 = 1, len2 = 1, len3 = 2
-
初始化
dp表,大小为(len1+1) x (len2+1) = 2 x 2。dp[0][0] = true- 第一行
dp[0][1]:只用s2的前1个字符”b”去匹配s3的前1个字符”a”,’b’ != ‘a’,所以dp[0][1] = false。 - 第一列
dp[1][0]:只用s1的前1个字符”a”去匹配s3的前1个字符”a”,’a’ == ‘a’,且dp[0][0] = true,所以dp[1][0] = true。
初始
dp表:“” “b” “” T F “a” T ? -
计算
dp[1][1]:i=1, j=1。s3[i+j-1] = s3[1] = ‘b’- 情况1(来自s1):
i>0成立,s1[0] = ‘a’不等于’b’,此路不通。 - 情况2(来自s2):
j>0成立,s2[0] = ‘b’等于’b’,需要看dp[1][0],其值为true。所以此路通。 dp[1][1] = true OR false = true
最终
dp表:“” “b” “” T F “a” T T -
结果:
dp[1][1] = true,所以s3 = “ab”可以由s1 = “a”和s2 = “b”交错组成。
复杂度分析
- 时间复杂度:O(m * n),其中 m 和 n 分别是 s1 和 s2 的长度。我们需要填充一个 (m+1) x (n+1) 的 DP 表。
- 空间复杂度:O(m * n),即 DP 表所占用的空间。可以优化到 O(min(m, n)),即使用一维数组进行滚动更新。
核心思想总结
这个问题的核心在于设计 dp[i][j] 这个状态,它精准地描述了“前 i 个字符和前 j 个字符能交错组成前 (i+j) 个字符”这个子问题。状态转移则非常直观地模拟了交错的过程:最后一个字符要么来自 s1,要么来自 s2,只要有一种来源能匹配上 s3 的对应字符,并且剩下的部分也满足交错条件即可。通过从空串开始逐步增加字符,我们最终判断出整个字符串是否满足条件。