字符串的交错组成

给定三个字符串str1、str2和aim。如果aim包含且仅包含来自str1和str2的所有字符,而且在aim中属于str1的字符之间保持原来在str1中的顺序,属于str2的字符之间保持原来在str2中的顺序,那么称aim是str1和str2的交错组成。实现一个函数,判断aim是否是str1和str2交错组成。
str1=“AB”,str2=“12”。那么“AB12”、“A1B2”、“A12B”、“1A2B”和“1AB2”等等都是str1和str2交错组成。

思路 && 代码

经典动态规划

如果str1的长度为M, str2的长度为N,经典动态规划的方法可以达到时间复杂度O(M*N),额外空间复杂度O(M*N)。

首先aim如果是str1和str2的交错组成, aim的长度一定是M+N,否则直接返回false。
然后生成大小为(M+1)*(N+1)布尔类型的矩阵dp。
dp[i][j]的值代表aim[0..i+j-1]能否被str1[0..i-1]和str2[0..j-1]交错组成。

计算dp矩阵的时候,是从左到右再从上到下的计算的, dp[M][N]就是dp矩阵中最右下角的值,
就表示aim整体能否被str1整体和str2整体交错组成,也就是最终结果。具体说明dp矩阵每个位置的值是如何计算。

1, dp[0][0]=true。 aim为空串时,当然可以被str1为空串和str2为空串交错组成。
2,矩阵dp第一列即dp[0..M-1][0]。 dp[i][0]表示aim[0..i-1]能否只被str1[0..i-1]交错组成。
如果aim[0..i-1]等于str1[0..i-1],则令dp[i][0]=true,否则令dp[i][0]=false。
3,矩阵dp第一行即dp[0][0..N-1]。 dp[0][j]表示aim[0..j-1]能否只被str2[0..j-1]交错组成。
如果aim[0..j-1]等于str1[0..j-1],则令dp[i][0]=true,否则令dp[i][0]=false。
4,对于其他位置(i,j), dp[i][j]的值由下面的情况决定。
1) dp[i-1][j]代表aim[0..i+j-2]能否被str1[0..i-2]和str2[0..j-1]交错组成,如果可以,
那么如果再有str1[i-1]等于aim[i+j-1],说明str1[i-1]可以作为交错组成aim[0..i+j-1]的最
后一个字符。令dp[i][j]=true。
2) dp[i][j-1]代表aim[0..i+j-2]能否被str1[0..i-1]和str2[0..j-2]交错组成,如果可以,
那么如果再有str2[j-1]等于aim[i+j-1],说明str1[j-1]可以作为交错组成aim[0..i+j-1]的最
后一个字符。令dp[i][j]=true。
3)如果情况1)和情况2)都不满足,令dp[i][j]=false。
具体过程请参看如下代码中的isCross1方法。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public static boolean isCross1(String str1, String str2, String aim) {
if (str1 == null || str2 == null || aim == null) {
return false;
}
char[] char1 = str1.toCharArray();
char[] char2 = str2.toCharArray();
char[] caim = aim.toCharArray();
if (char1.length + char2.length != caim.length) {
return false;
}
// 多增加一行和一列,为空值,方便计算
int row = char1.length + 1;
int col = char2.length + 1;
boolean[][] dp = new boolean[row][col];
dp[0][0] = true;
for (int i = 1; i < row; i++) {
// str1[0,i-1]交错aim[0,i-1]
if(char1[i-1] != caim[i-1]){
//遇到不等就退出循环,初始化时是false
break;
}
dp[i][0] = true;
}
for (int i = 1; i < col; i++) {
// str2[0,i-1]交错aim[0,i-1]
if(char2[i-1] != caim[i-1]){
break;
}
dp[0][i] = true;
}
for (int i = 1; i < row; i++) {
for (int j = 1; j < col; j++) {
if (caim[i + j - 1] == char1[i - 1] && dp[i - 1][j]
|| caim[i + j - 1] == char2[j - 1] && dp[i][j - 1]) {
dp[i][j] = true;
}
}
}
return dp[row - 1][col - 1];
}

经典动态规划方法结合空间压缩的方法。

如果结合空间压缩的技巧可以把额外空间复杂度减至O(min{M,N})。
实际进行空间压缩的时候,比较str1和str2哪个长度较小,长度较小的那个作为列对应的字符串,
然后生成和较短字符串长度一样的一维数组dp,滚动更新即可。
具体请参看如下代码中的isCross2方法。

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
28
29
30
31
32
33
34
35
36
37
38
39
40
//压缩空间,只用一行表示一个数组,然后滚动更新
public static boolean isCross2(String str1, String str2, String aim) {
if (str1 == null || str2 == null || aim == null) {
return false;
}
char[] ch1 = str1.toCharArray();
char[] ch2 = str2.toCharArray();
char[] chaim = aim.toCharArray();
if (chaim.length != ch1.length + ch2.length) {
return false;
}
//选出长短,长的在外循环,短的在循环内,因为使得不断更新短的数值
char[] longs = ch1.length >= ch2.length ? ch1 : ch2;
char[] shorts = ch1.length < ch2.length ? ch1 : ch2;
boolean[] dp = new boolean[shorts.length + 1];
dp[0] = true;
//初始化该行
for (int i = 1; i <= shorts.length; i++) {
if (shorts[i - 1] != chaim[i - 1]) {
break;
}
dp[i] = true;
}
//滚动更新的过程
for (int i = 1; i <= longs.length; i++) {
//更新该行的第一列
dp[0] = dp[0] && longs[i - 1] == chaim[i - 1];
for (int j = 1; j <= shorts.length; j++) {
//此时只更新到j-1,而j还没有更新,表示的是上一行的j值,即要更新位置的上面
//j-1已经更新,表示的是要更新位置的左边
if ((longs[i - 1] == chaim[i + j - 1] && dp[j])
|| (shorts[j - 1] == chaim[i + j - 1] && dp[j - 1])) {
dp[j] = true;
} else {
dp[j] = false;
}
}
}
return dp[shorts.length];
}