好的,根据你的要求,我从“线性动态规划”领域中随机挑选一个你尚未讲过的题目。我检查了你的历史列表,以下题目将是一个全新的讲解。
线性动态规划:交错字符串
题目描述
给你三个字符串 s1、s2 和 s3。请你编写一个算法来验证 s3 是否是由 s1 和 s2 交错 组成的。
交错 的定义是:保持 s1 和 s2 中每个字符的原始相对顺序不变,将它们混合在一起,形成一个新字符串。
示例 1:
- 输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
- 输出:
true - 解释:我们可以这样交错得到 s3:
- 从 s1 取 “aa”, 从 s2 取 “d”, 从 s1 取 “bbc”, 从 s2 取 “ca”, 最后从 s1 取 “c”。
- 组成的序列是:
a(s1)a(s1)d(s2)b(s1)b(s1)c(s1)b(s2)c(s2)a(s2)c(s1) - 可以看到,s1 的字符 (
a,a,b,b,c,c) 和 s2 的字符 (d,b,c,a) 在 s3 中都保持了原有的顺序。
示例 2:
- 输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
- 输出:
false - 解释:无法通过交错 s1 和 s2 得到 s3。因为在 s3 中,第二个 ‘b’ 和第三个 ‘b’ 是连续出现的,这破坏了 s2 (“dbbca”) 中 ‘b’ 和 ‘c’ 之间的顺序(s2中的
b后面必须是b或c,但s3中连续两个b后直接是a,会导致s2的字符顺序无法匹配)。
约束条件:
0 <= s1.length, s2.length <= 1000 <= s3.length <= 200s1、s2和s3都由小写英文字母组成。
解题过程(循序渐进)
这个问题本质上是一个双指针匹配问题,但简单的双指针会遇到歧义(比如两个字符串当前字符都和s3匹配时,该选哪一个?)。动态规划是解决这类“判断可行性”问题的标准方法。
第一步:问题分析与状态定义
我们的目标是判断 s3 能否由 s1 和 s2 交错形成。
核心约束是:必须保持 s1 和 s2 各自的字符顺序。
我们可以将这个过程想象为:从 s1 和 s2 的开头,按顺序“消耗”字符,来匹配 s3。
一个关键的直觉是:当我们决定匹配到 s3 的前 k 个字符时,我们从 s1 中取走了 i 个字符,从 s2 中取走了 j 个字符,并且 i + j = k。
这引导我们定义一个二维动态规划状态:
- 定义
dp[i][j]为一个布尔值(true/false)。 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分别代表已使用s1和s2的字符数量。 - 最终,我们希望知道
dp[len(s1)][len(s2)]是否为true,即整个s1和整个s2能否交错成整个s3。
小提示:我们让索引 i 和 j 表示“长度”,而不是“下标”,这样初始状态 dp[0][0] 表示用空字符串交错成空字符串,非常自然。
第二步:推导状态转移方程
我们需要思考 dp[i][j] 是如何从之前的状态转移过来的。
要判断 s1 的前 i 个字符和 s2 的前 j 个字符能否交错成 s3 的前 i+j 个字符,我们只需要看最后一个字符是从 s1 取来的,还是从 s2 取来的。
情况一:最后一个字符来自 s1。
- 这意味着
s3的第(i+j)个字符(下标为i+j-1)必须等于s1的第i个字符(下标为i-1)。 - 并且,在匹配这个字符之前的状态必须是可行的,即
s1的前i-1个字符和s2的前j个字符必须能交错成s3的前i+j-1个字符。 - 用状态表示就是:
dp[i-1][j]必须为true,并且s1[i-1] == s3[i+j-1]。
情况二:最后一个字符来自 s2。
- 同理,这要求
s3[i+j-1] == s2[j-1],并且dp[i][j-1]为true。
由于最终结果只要“能”交错即可,所以只要以上两种情况有任意一种成立,dp[i][j] 就应该是 true。
因此,状态转移方程为:
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])
第三步:确定初始状态(边界条件)
dp[0][0] 表示用两个空字符串交错成一个空字符串,这显然是可行的。所以:
dp[0][0] = true
边界情况1:s1 用完了,只用 s2。
- 即
i = 0的情况。 dp[0][j]表示只用s2的前j个字符去匹配s3的前j个字符。- 这只有在
s2的前j个字符完全等于s3的前j个字符时才成立。 - 转移方程简化为:
dp[0][j] = dp[0][j-1] && s2[j-1] == s3[j-1](因为i=0,所以i+j-1 = j-1)。
边界情况2:s2 用完了,只用 s1。
- 即
j = 0的情况。 - 同理,
dp[i][0] = dp[i-1][0] && s1[i-1] == s3[i-1]。
这些边界情况其实就是我们状态转移方程在 i=0 或 j=0 时的特例,在代码实现中需要单独处理。
第四步:计算顺序与最终答案
- 我们需要计算一个二维表格,行
i从0到len(s1),列j从0到len(s2)。 - 计算顺序可以是一行一行地计算,也可以是一列一列地计算。因为计算
dp[i][j]时,只需要它左边 (dp[i][j-1]) 和上边 (dp[i-1][j]) 的值。 - 最终答案就是
dp[len(s1)][len(s2)]。
一个重要剪枝:如果 len(s1) + len(s2) != len(s3),那么一定不可能交错成功,可以直接返回 false。
第五步:举例演算
以示例1为例:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”。
我们构建 dp 表,i 对应 s1 长度,j 对应 s2 长度。
- 初始化
dp[0][0] = true。 - 初始化第一行 (
i=0): 只用 s2 匹配 s3 的前缀。dp[0][1]: 匹配 s3[0] (‘a’) 和 s2[0] (‘d’),不相等,false。- 所以第一行后续都是
false。
- 初始化第一列 (
j=0): 只用 s1 匹配 s3 的前缀。dp[1][0]:dp[0][0] && s1[0](‘a’) == s3[0](‘a’)=>true。dp[2][0]:dp[1][0] && s1[1](‘a’) == s3[1](‘a’)=>true。dp[3][0]:dp[2][0] && s1[2](‘b’) == s3[2](‘d’)=>false。后续也都是false。
- 计算
dp[1][1]:s3 前2个字符是 “aa”。- 来自 s1:
dp[0][1] (false) && s1[0]==s3[1] (‘a’==’a’)=>false。 - 来自 s2:
dp[1][0] (true) && s2[0]==s3[1] (‘d’==’a’)=>false。 - 所以
dp[1][1] = false。
- 来自 s1:
- 以此类推计算下去。最终,
dp[5][4](i=5, j=4) 会根据dp[4][4]和dp[5][3]以及对应字符计算出来。- 你会发现,最终
dp[5][4]的结果是true,验证了交错的可能性。
- 你会发现,最终
第六步:复杂度分析与代码实现(伪代码)
- 时间复杂度:O(m * n),其中 m = len(s1), n = len(s2)。我们需要填充一个 (m+1) x (n+1) 的表格。
- 空间复杂度:我们可以优化到 O(n),只保留一行的状态,因为计算
dp[i][j]时只用到了上一行 (dp[i-1][j]) 和本行左边 (dp[i][j-1]) 的状态。
def isInterleave(s1: str, s2: str, s3: str) -> bool:
len1, len2, len3 = len(s1), len(s2), len(s3)
if len1 + len2 != len3:
return False
# 为了状态转移方便,我们创建长度为 (len2 + 1) 的数组
# dp[j] 代表:s1 的前 i 个字符和 s2 的前 j 个字符能否交错成 s3 的前 i+j 个字符
# 我们会在外层循环 i,内层更新 dp[j]
dp = [False] * (len2 + 1)
# 初始化 i=0 的情况
dp[0] = True # dp[0][0] = True
for j in range(1, len2 + 1):
# dp[0][j] = dp[0][j-1] and s2[j-1] == s3[j-1]
dp[j] = dp[j-1] and s2[j-1] == s3[j-1]
# 开始动态规划
for i in range(1, len1 + 1):
# 每一行开始时,先更新 j=0 的边界情况
# dp[i][0] = dp[i-1][0] and s1[i-1] == s3[i-1]
# 此时的 dp[0] 就代表上一行的 dp[i-1][0]
# 我们需要一个新的值 new_dp[0],但会覆盖旧值,所以先保存
# 或者我们可以直接先更新 dp[0]
dp[0] = dp[0] and s1[i-1] == s3[i-1]
for j in range(1, len2 + 1):
# 计算 dp[i][j]
# 情况1: 来自 s1。需要上一行 (i-1, j) 的状态为真,且字符匹配。
from_s1 = dp[j] and s1[i-1] == s3[i+j-1] # 注意这里的 dp[j] 还是上一行的值
# 情况2: 来自 s2。需要本行左边 (i, j-1) 的状态为真,且字符匹配。
from_s2 = dp[j-1] and s2[j-1] == s3[i+j-1] # 这里的 dp[j-1] 是本行刚更新过的值
# 更新当前 dp[j]
dp[j] = from_s1 or from_s2
return dp[len2] # 最终结果是 dp[len1][len2]
这个解法清晰地展示了如何将交错匹配问题转化为一个二维路径可行性问题,并用动态规划高效求解。