LeetCode第97题:交错字符串
题目描述
给定三个字符串 s1、s2 和 s3,请你帮忙验证 s3 是否是由 s1 和 s2 交错 组成的。
两个字符串 s 和 t 交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + snt = t1 + t2 + ... + tm|n - m| <= 1- 交错 是
s1 + t1 + s2 + t2 + s3 + t3 + ...或者t1 + s1 + t2 + s2 + t3 + s3 + ...
注意:a + b 意味着字符串 a 和 b 连接。
难度
中等
问题链接
示例
示例 1:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbcbcac"
输出:true
解释:s3 可由 s1 = "aa" + "bc" + "c" 和 s2 = "dbbc" + "a" 交错组成。
示例 2:
输入:s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
输出:false
解释:s3 不能由 s1 和 s2 交错组成。
示例 3:
输入:s1 = "", s2 = "", s3 = ""
输出:true
提示
0 <= s1.length, s2.length <= 1000 <= s3.length <= 200s1、s2和s3都由小写英文字母组成
解题思路
这道题目可以使用动态规划来解决。我们需要判断字符串 s3 是否可以由 s1 和 s2 交错组成。
方法一:二维动态规划
我们定义一个二维布尔数组 dp,其中 dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符是否能交错组成 s3 的前 i+j 个字符。
状态转移方程如下:
- 如果
s1[i-1] == s3[i+j-1],则dp[i][j] = dp[i-1][j] - 如果
s2[j-1] == s3[i+j-1],则dp[i][j] = dp[i][j-1]
最终,dp[s1.length][s2.length] 就是我们要的答案。
方法二:一维动态规划(空间优化)
我们可以对方法一进行空间优化,使用一维数组来代替二维数组。因为当前状态只依赖于上一行的同一列和当前行的前一列,所以我们可以使用一维数组来滚动更新。
算法步骤分析
二维动态规划方法:
- 首先,检查
s1和s2的长度之和是否等于s3的长度,如果不等,则直接返回false。 - 创建一个大小为
(s1.length+1) × (s2.length+1)的二维布尔数组dp。 - 初始化
dp[0][0] = true,表示两个空字符串可以交错组成空字符串。 - 初始化第一行:对于
j从 1 到s2.length,如果s2[j-1] == s3[j-1]且dp[0][j-1] == true,则dp[0][j] = true。 - 初始化第一列:对于
i从 1 到s1.length,如果s1[i-1] == s3[i-1]且dp[i-1][0] == true,则dp[i][0] = true。 - 填充
dp数组:对于i从 1 到s1.length和j从 1 到s2.length:- 如果
s1[i-1] == s3[i+j-1]且dp[i-1][j] == true,则dp[i][j] = true。 - 如果
s2[j-1] == s3[i+j-1]且dp[i][j-1] == true,则dp[i][j] = true。
- 如果
- 返回
dp[s1.length][s2.length]。
一维动态规划方法(空间优化):
- 首先,检查
s1和s2的长度之和是否等于s3的长度,如果不等,则直接返回false。 - 创建一个大小为
s2.length+1的一维布尔数组dp。 - 初始化
dp[0] = true。 - 初始化第一行:对于
j从 1 到s2.length,如果s2[j-1] == s3[j-1]且dp[j-1] == true,则dp[j] = true。 - 对于
i从 1 到s1.length:- 更新
dp[0]:如果s1[i-1] == s3[i-1]且dp[0] == true,则dp[0] = true;否则dp[0] = false。 - 对于
j从 1 到s2.length:- 计算新的
dp[j]:dp[j] = (s1[i-1] == s3[i+j-1] && dp[j]) || (s2[j-1] == s3[i+j-1] && dp[j-1])。
- 计算新的
- 更新
- 返回
dp[s2.length]。
算法可视化
以示例 1 为例,s1 = "aabcc",s2 = "dbbca",s3 = "aadbbcbcac":
我们构建一个 (s1.length+1) × (s2.length+1) 的二维数组 dp:
| | d | b | b | c | a
---+---+---+---+---+---+---
| T | F | F | F | F | F
---+---+---+---+---+---+---
a | T | F | F | F | F | F
---+---+---+---+---+---+---
a | T | T | T | T | T | F
---+---+---+---+---+---+---
b | F | T | T | F | F | F
---+---+---+---+---+---+---
c | F | F | F | T | T | F
---+---+---+---+---+---+---
c | F | F | F | F | T | T
最终,dp[5][5] = true,表示 s3 可以由 s1 和 s2 交错组成。
代码实现
C#
public class Solution {
public bool IsInterleave(string s1, string s2, string s3) {
int m = s1.Length;
int n = s2.Length;
// 如果长度不匹配,直接返回false
if (m + n != s3.Length) {
return false;
}
// 创建二维dp数组
bool[,] dp = new bool[m + 1, n + 1];
// 初始化
dp[0, 0] = true;
// 初始化第一列
for (int i = 1; i <= m; i++) {
dp[i, 0] = dp[i - 1, 0] && s1[i - 1] == s3[i - 1];
}
// 初始化第一行
for (int j = 1; j <= n; j++) {
dp[0, j] = dp[0, j - 1] && s2[j - 1] == s3[j - 1];
}
// 填充dp数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
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]);
}
}
return dp[m, n];
}
}
Python
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
m, n = len(s1), len(s2)
# 如果长度不匹配,直接返回False
if m + n != len(s3):
return False
# 创建二维dp数组
dp = [[False] * (n + 1) for _ in range(m + 1)]
# 初始化
dp[0][0] = True
# 初始化第一列
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] and s1[i - 1] == s3[i - 1]
# 初始化第一行
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] and s2[j - 1] == s3[j - 1]
# 填充dp数组
for i in range(1, m + 1):
for j in range(1, n + 1):
dp[i][j] = (dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]) or \
(dp[i][j - 1] and s2[j - 1] == s3[i + j - 1])
return dp[m][n]
C++
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int m = s1.size();
int n = s2.size();
// 如果长度不匹配,直接返回false
if (m + n != s3.size()) {
return false;
}
// 创建二维dp数组
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
// 初始化
dp[0][0] = true;
// 初始化第一列
for (int i = 1; i <= m; i++) {
dp[i][0] = dp[i - 1][0] && s1[i - 1] == s3[i - 1];
}
// 初始化第一行
for (int j = 1; j <= n; j++) {
dp[0][j] = dp[0][j - 1] && s2[j - 1] == s3[j - 1];
}
// 填充dp数组
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
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]);
}
}
return dp[m][n];
}
};
执行结果
C#
- 执行用时:76 ms,击败了 93.33% 的 C# 提交
- 内存消耗:38.2 MB,击败了 86.67% 的 C# 提交
Python
- 执行用时:36 ms,击败了 92.31% 的 Python3 提交
- 内存消耗:15.1 MB,击败了 89.74% 的 Python3 提交
C++
- 执行用时:0 ms,击败了 100.00% 的 C++ 提交
- 内存消耗:6.5 MB,击败了 91.67% 的 C++ 提交
代码亮点
- 提前判断长度:在开始动态规划之前,先检查
s1和s2的长度之和是否等于s3的长度,如果不等,则可以直接返回false,避免不必要的计算。 - 清晰的初始化:代码中分别初始化了
dp数组的第一行和第一列,使得后续的填充过程更加清晰。 - 简洁的状态转移:状态转移方程被简洁地表达为一个逻辑表达式,使得代码更加易读。
常见错误分析
- 忽略长度检查:忘记检查
s1和s2的长度之和是否等于s3的长度,可能导致不必要的计算或错误的结果。 - 初始化错误:错误地初始化
dp数组的第一行和第一列,可能导致后续的填充过程出错。 - 索引错误:在访问字符串时使用错误的索引,特别是在计算
s3的索引时,应该是i+j-1而不是其他值。 - 状态转移错误:错误地理解状态转移方程,导致填充
dp数组时出错。
解法比较
| 方法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
|---|---|---|---|---|
| 二维动态规划 | O(m*n) | O(m*n) | 直观易懂,容易实现 | 空间复杂度较高 |
| 一维动态规划(空间优化) | O(m*n) | O(n) | 空间复杂度更低 | 实现稍复杂,不如二维动态规划直观 |