给定三个字符串 s1
、s2
、s3
,请你帮忙验证 s3
是否是由 s1
和 s2
__交错 组成的。
两个字符串 s
和 t
交错 的定义与过程如下,其中每个字符串都会被分割成若干 非空 子字符串:
s = s1 + s2 + ... + sn
t = 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
示例 2:
**输入:** s1 = "aabcc", s2 = "dbbca", s3 = "aadbbbaccc"
**输出:** false
示例 3:
**输入:** s1 = "", s2 = "", s3 = ""
**输出:** true
提示:
0 <= s1.length, s2.length <= 100
0 <= s3.length <= 200
s1
、s2
、和 s3
都由小写英文字母组成
进阶: 您能否仅使用 O(s2.length)
额外的内存空间来解决它?
方法一:动态规划 记 $|s_1| = n$,$|s_2| = m$。
思路与算法
双指针法错在哪里? 也许有同学看到这道题目的第一反应是使用双指针法解决这个问题,指针 $p_1$ 一开始指向 $s_1$ 的头部,指针 $p_2$ 一开始指向 $s_2$ 的头部,指针 $p_3$ 指向 $s_3$ 的头部,每次观察 $p_1$ 和 $p_2$ 指向的元素哪一个和 $p_3$ 指向的元素相等,相等则匹配并后移指针。样例就是一个很好的反例,用这种方法判断 $s_1 = {\rm aabcc}$,$s_2 = {\rm dbbca}$,$s_3 = {\rm aadbbcbcac}$ 时,得到的结果是 $\rm False$,实际应该是 $\rm True$。
解决这个问题的正确方法是动态规划。 首先如果 $|s_1| + |s_2| \neq |s_3|$,那 $s_3$ 必然不可能由 $s_1$ 和 $s_2$ 交错组成。在 $|s_1| + |s_2| = |s_3|$ 时,我们可以用动态规划来求解。我们定义 $f(i, j)$ 表示 $s_1$ 的前 $i$ 个元素和 $s_2$ 的前 $j$ 个元素是否能交错组成 $s_3$ 的前 $i + j$ 个元素。如果 $s_1$ 的第 $i$ 个元素和 $s_3$ 的第 $i + j$ 个元素相等,那么 $s_1$ 的前 $i$ 个元素和 $s_2$ 的前 $j$ 个元素是否能交错组成 $s_3$ 的前 $i + j$ 个元素取决于 $s_1$ 的前 $i - 1$ 个元素和 $s_2$ 的前 $j$ 个元素是否能交错组成 $s_3$ 的前 $i + j - 1$ 个元素,即此时 $f(i, j)$ 取决于 $f(i - 1, j)$,在此情况下如果 $f(i - 1, j)$ 为真,则 $f(i, j)$ 也为真。同样的,如果 $s_2$ 的第 $j$ 个元素和 $s_3$ 的第 $i + j$ 个元素相等并且 $f(i, j - 1)$ 为真,则 $f(i, j)$ 也为真。于是我们可以推导出这样的动态规划转移方程:
$$f(i, j) = [f(i - 1, j) , {\rm and} , s_1(i - 1) = s_3(p)] , {\rm or} , [f(i, j - 1) , {\rm and} , s_2(j - 1) = s_3(p)]$$
其中 $p = i + j - 1$。边界条件为 $f(0, 0) = {\rm True}$。至此,我们很容易可以给出这样一个实现:
[sol0-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : bool isInterleave (string s1, string s2, string s3) { auto f = vector < vector <int > > (s1.size () + 1 , vector <int > (s2.size () + 1 , false )); int n = s1.size (), m = s2.size (), t = s3.size (); if (n + m != t) { return false ; } f[0 ][0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[i][j] |= (f[i - 1 ][j] && s1[i - 1 ] == s3[p]); } if (j > 0 ) { f[i][j] |= (f[i][j - 1 ] && s2[j - 1 ] == s3[p]); } } } return f[n][m]; } };
[sol0-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public boolean isInterleave (String s1, String s2, String s3) { int n = s1.length(), m = s2.length(), t = s3.length(); if (n + m != t) { return false ; } boolean [][] f = new boolean [n + 1 ][m + 1 ]; f[0 ][0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[i][j] = f[i][j] || (f[i - 1 ][j] && s1.charAt(i - 1 ) == s3.charAt(p)); } if (j > 0 ) { f[i][j] = f[i][j] || (f[i][j - 1 ] && s2.charAt(j - 1 ) == s3.charAt(p)); } } } return f[n][m]; } }
[sol0-Golang] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 func isInterleave (s1 string , s2 string , s3 string ) bool { n, m, t := len (s1), len (s2), len (s3) if (n + m) != t { return false } f := make ([][]bool , n + 1 ) for i := 0 ; i <= n; i++ { f[i] = make ([]bool , m + 1 ) } f[0 ][0 ] = true for i := 0 ; i <= n; i++ { for j := 0 ; j <= m; j++ { p := i + j - 1 if i > 0 { f[i][j] = f[i][j] || (f[i-1 ][j] && s1[i-1 ] == s3[p]) } if j > 0 { f[i][j] = f[i][j] || (f[i][j-1 ] && s2[j-1 ] == s3[p]) } } } return f[n][m] }
[sol0-C] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool isInterleave (char * s1, char * s2, char * s3) { int n = strlen (s1), m = strlen (s2), t = strlen (s3); int f[n + 1 ][m + 1 ]; memset (f, 0 , sizeof (f)); if (n + m != t) { return false ; } f[0 ][0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[i][j] |= (f[i - 1 ][j] && s1[i - 1 ] == s3[p]); } if (j > 0 ) { f[i][j] |= (f[i][j - 1 ] && s2[j - 1 ] == s3[p]); } } } return f[n][m]; }
不难看出这个实现的时间复杂度和空间复杂度都是 $O(nm)$。
使用滚动数组优化空间复杂度。 因为这里数组 $f$ 的第 $i$ 行只和第 $i - 1$ 行相关,所以我们可以用滚动数组优化这个动态规划,这样空间复杂度可以变成 $O(m)$。敲黑板:我们又遇到「滚动数组」优化啦!不会的同学一定要学习哟。如果还没有做过这几个题建议大家做一下,都可以使用这个思想进行优化:
下面给出滚动数组优化的代码。
代码
[sol1-C++] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 class Solution {public : bool isInterleave (string s1, string s2, string s3) { auto f = vector <int > (s2.size () + 1 , false ); int n = s1.size (), m = s2.size (), t = s3.size (); if (n + m != t) { return false ; } f[0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[j] &= (s1[i - 1 ] == s3[p]); } if (j > 0 ) { f[j] |= (f[j - 1 ] && s2[j - 1 ] == s3[p]); } } } return f[m]; } };
[sol1-Java] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 class Solution { public boolean isInterleave (String s1, String s2, String s3) { int n = s1.length(), m = s2.length(), t = s3.length(); if (n + m != t) { return false ; } boolean [] f = new boolean [m + 1 ]; f[0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[j] = f[j] && s1.charAt(i - 1 ) == s3.charAt(p); } if (j > 0 ) { f[j] = f[j] || (f[j - 1 ] && s2.charAt(j - 1 ) == s3.charAt(p)); } } } return f[m]; } }
[sol1-Golang] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 func isInterleave (s1 string , s2 string , s3 string ) bool { n, m, t := len (s1), len (s2), len (s3) if (n + m) != t { return false } f := make ([]bool , m + 1 ) f[0 ] = true for i := 0 ; i <= n; i++ { for j := 0 ; j <= m; j++ { p := i + j - 1 if i > 0 { f[j] = f[j] && s1[i-1 ] == s3[p] } if j > 0 { f[j] = f[j] || f[j-1 ] && s2[j-1 ] == s3[p] } } } return f[m] }
[sol1-C] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 bool isInterleave (char * s1, char * s2, char * s3) { int n = strlen (s1), m = strlen (s2), t = strlen (s3); int f[m + 1 ]; memset (f, 0 , sizeof (f)); if (n + m != t) { return false ; } f[0 ] = true ; for (int i = 0 ; i <= n; ++i) { for (int j = 0 ; j <= m; ++j) { int p = i + j - 1 ; if (i > 0 ) { f[j] &= (s1[i - 1 ] == s3[p]); } if (j > 0 ) { f[j] |= (f[j - 1 ] && s2[j - 1 ] == s3[p]); } } } return f[m]; }
复杂度分析
时间复杂度:$O(nm)$,两重循环的时间代价为 $O(nm)$。
空间复杂度:$O(m)$,即 $s_2$ 的长度。