题目描述(困难难度)
在两个字符串 s1 和 s2 中依次取字母,问是否可以组成 S3。什么意思呢?比如 s1 = abc , s2 = de,s3 = abdce。
s1 取 1 个 字母得到 a,s1 再取个字母得到 ab,s2 取个字母得到 abd, s1 取 1 个 字母得到 abdc, s2 取 1 个 字母得到 abdce,然后就得到了 s3,所以返回 true。
解法一 回溯法
如果我们简化下问题,如果 s1 和 s2 中不含有重复的字母,比如 s1 = abc,s2 = de,s3 = abdce。
这样是不是就简单多了。我们只需要三个指针,依次遍历字符串。
i 和 k 的指的字母相等,所以 i 后移,k 后移
a b c
^
i
d e
^
j
a b d c e
^
k
i 和 k 的指的字母相等,所以 i 后移,k 后移
a b c
^
i
d e
^
j
a b d c e
^
k
j 和 k 的指的字母相等,所以 j 后移,k 后移
a b c
^
i
d e
^
j
a b d c e
^
k
就这样比较下去,如果 i,j,k 都成功移动到了末尾即成功。
但是这道题 s1 和 s2 中会有重复的字符出现,比如下边的情况
a d c
^
i
d e
^
j
a d c e
^
k
此时 i 和 j 指向的字母都和 k 相等,此时该怎么办呢?
回溯法!是的,我们先尝试 i 和 k 后移,然后看能不能成功。不行的话我们再回溯回来,把 j 和 k 后移。
public class Interleaving_String {
public static boolean isInterleave(String s1,String s2,String s3) {
return getAns(s1,0,s2,0,s3,0);
}
private static boolean getAns(String s1,int i,String s2,int j,String s3,int k) {
//长度不匹配直接返回false
if(s1.length()+s2.length()!=s3.length()) return false;
if(i==s1.length() && j==s2.length() && k==s3.length()) return true;
//i到达了末尾,直接移动j和k不停比较
if(i==s1.length()) {
while(j<s2.length()) {
if(s2.charAt(j)!=s3.charAt(k)) {
return false;
}
j++;
k++;
}
return true;
}
//j到达了末尾,直接移动i和k不停比较
if(j==s2.length()) {
while(i<s1.length()) {
if(s1.charAt(i)!=s3.charAt(k)) {
return false;
}
i++;
k++;
}
return true;
}
//判断i和k指向的字符串是否相等
if(s1.charAt(i)==s3.charAt(k)) {
if(getAns(s1,i+1,s2,j,s3,k+1)) {
return true;
}
}
//移动i和k失败,尝试移动j和k
if(s2.charAt(j)==s3.charAt(k)) {
if(getAns(s1,i,s2,j+1,s3,k+1)) {
return true;
}
}
return false;
}
public static void main(String args[]) {
String s1="aabcc";
String s2="dbbca";
String s3="aadbbcbcac";
boolean ans=isInterleave(s1,s2,s3);
System.out.println(ans);
}
}
解法二 动态规划
在现在这种方法中,我们用另一种思路来考虑同样的问题。这里我们考虑用 s 1 s1 s1和 s 2 s2 s2的某个前缀是否能形成 s 3 s3 s3 的一个前缀。
这个方法的前提建立于:判断一个 s 3 s3 s3的前缀(用下标 k k k 表示),能否用 s 1 s1 s1 和 s 2 s2 s2的前缀(下标分别为 i i i 和 j j j),仅仅依赖于 s 1 s1 s1 前 i i i 个字符和 s 2 s2 s2 前 j j j 个字符,而与后面的字符无关。
为了实现这个算法, 我们将使用一个 2D 的布尔数组 d p dp dp 。 d p [ i ] [ j ] dp[i][j] dp[i][j]表示用 s 1 s1 s1的前 ( i + 1 ) (i+1) (i+1)和 s 2 s2 s2 的前 ( j + 1 ) (j+1) (j+1) 个字符,总共$ (i+j+2)$个字符,是否交错构成 s 3 s3 s3 的前缀。为了求出 d p [ i ] [ j ] dp[i][j] dp[i][j],我们需要考虑 2 种情况:
-
s 1 s1 s1的第 i i i 个字符和 s 2 s2 s2 的第 j j j 个字符都不能匹配 s 3 s3 s3的第 k k k 个字符,其中 k = i + j + 1 k=i+j+1 k=i+j+1 。这种情况下, s 1 s1 s1和 s 2 s2 s2的前缀无法交错形成 s 3 s3 s3 长度为 k + 1 k+1 k+1的前缀。因此,我们让 d p [ i ] [ j ] dp[i][j] dp[i][j]为 False。
-
s 1 s1 s1的第 i i i个字符或者 s 2 s2 s2 的第 j j j个字符可以匹配 s 3 s3 s3的第 k k k字符,其中 k = i + j + 1 k=i+j+1 k=i+j+1 。假设匹配的字符是 x x x 且与 s 1 s1 s1的第 i i i个字符匹配,我们就需要把 x x x 放在已经形成的交错字符串的最后一个位置。此时,为了我们必须确保 s 1 s1 s1的前 ( i − 1 ) (i-1) (i−1)个字符和 s 2 s2 s2的前 j j j个字符能形成 s 3 s3 s3的一个前缀。类似的,如果我们将 s 2 s2 s2的第 j j j个字符与 s 3 s3 s3的第 k k k个字符匹配,我们需要确保 s 1 s1 s1的前 i i i个字符和 s 2 s2 s2的前 ( j − 1 ) (j-1) (j−1)个字符能形成 s 3 s3 s3的一个前缀,我们就让 d p [ i ] [ j ] dp[i][j] dp[i][j]为 True 。
最终结果如下图所示。
代码如下:
public class Interleaving_String2 {
public static boolean isInterleave(String s1, String s2, String s3) {
if(s3.length()!=s1.length()+s2.length()) return false;
boolean[][]dp=new boolean[s1.length()+1][s2.length()+1];
for(int i=0;i<=s1.length();i++) {
for(int j=0;j<=s2.length();j++) {
if(j==0 && j==0) {
dp[i][j]=true;
}else if(i==0) {
dp[i][j]=dp[i][j-1] && s2.charAt(j-1)==s3.charAt(i+j-1);
}else if(j==0) {
dp[i][j]=dp[i-1][j] && s1.charAt(i-1)==s3.charAt(i+j-1);
}else {
dp[i][j]=(dp[i][j-1] && s2.charAt(j-1)==s3.charAt(i+j-1))||dp[i-1][j] && s1.charAt(i-1)==s3.charAt(i+j-1);
}
}
}
return dp[s1.length()][s2.length()];
}
public static void main(String args[]) {
String s1="aabcc";
String s2="dbbca";
String s3="aadbbcbcac";
boolean ans=isInterleave(s1,s2,s3);
System.out.println(ans);
}
}
参考文献
1.https://zhuanlan.zhihu.com/p/71855844
2.使用二维动态规划
3.https://www.youtube.com/watch?v=HmAF9xeS_2I