困难题,第一个难点就是“交错”这个词的意思,想象两把梳子面向对方插过去,比较难解释:
假设 s3 中来自 s1 的字符集合为 A ,来自 s2 的字符集合为 B ,那么A 和 B中元素的相对顺序与 s1或者 s2中是一致的。
看到一个图(类似路径问题,找准状态方程快速求解 ),解释这个题目再形象不过了:
首先符合要求的 s3 显然有:
- s3.size() = s1.size() + s2.size()
- 如果s1/s2为空,s3 = s1 or s2;
回溯
一开始是想到回溯法的思路,不过枚举可能复杂度太高了。
假设 n = s3.size(),我们每次从s1 or s2中取1个字符,一共需要取n次,当碰到岔路口的时候,比如s1中的下一个待取元素与s2中的下一个待取元素相同,那就有两种选择。
三指针思路:i, j, k分别指向s3, s1, s2的下一个待考虑元素。
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
if(s1.empty()) return s2 == s3;
if(s2.empty()) return s1 == s3;
int j = 0, k = 0; //用于记录s1,s2中的下一个待选元素下标
for(int i = 0; i < s3.size(); ++i){
if(s1[j] != s2[k]){//只有一种选择
//选择s1
if(s3[i] == s1[j]) ++j;
//选择s2
else if(s3[i] == s2[k]) ++k;
// 选哪个都不行
else return false;
}else{//有两种选择
//如果两种选择都不满足要求
if(s3[i] != s1[j]) return false;
//两种选法进入不同路口
bool flag1 = isInterleave(s1.substr(j+1),s2.substr(k), s3.substr(i+1) ); //选s1
if(flag1) return true;
bool flag2 = isInterleave(s1.substr(j),s2.substr(k+1), s3.substr(i+1)); //选s2
return flag2;
}
// 此时j == s1.size() 或者 k == s2.size(),有一个字符串全部被选完
if(j == s1.size()) return s2.substr(k) == s3.substr(i+1);
if(k == s2.size()) return s1.substr(j) == s3.substr(i+1);
}
return true;
}
};
上述代码逻辑挺简单的,但是第99个用例过不去,超出时间限制,应该是递归的问题,而且存在大量的重复计算,也就是有些路径走了多次。
那尝试给递归加上记忆化吧,走过的路径直接记录它的状态,下次再走的时候直接查询状态即可。上面的代码主要涉及 i, j, k三个参数,但是其实总会有i + j = k,严格来说我们只需要一个二维(i,j)数组就能存储所有的状态了。
代码还是由上面修改而来,顺利通过,效率也很高:
class Solution {
public:
// p1, p2, p3分别是s1, s2, s3中下一个待选元素下标
bool interleave(string &s1, int p1, string &s2, int p2, string &s3, int p3, vector<vector<int>> &vec){
if(vec[p1][p2] != -1) return vec[p1][p2];//已经存在记录,直接返回相应值
// 如果p1 == s1.size() 或者 p2 == s2.size(),则有一个字符串被选完,只需比较另一个字符串和s3的剩余部分是否相等即可
if(p1 == s1.size()){
vec[p1][p2] = s2.substr(p2) == s3.substr(p3) ? 1 : 0;
return s2.substr(p2) == s3.substr(p3);
}
if(p2 == s2.size()){
vec[p1][p2] = s1.substr(p1) == s3.substr(p3) ? 1 : 0;
return s1.substr(p1) == s3.substr(p3);
}
//for循环其实不是必须的,可以每一个元素都选或者不选,然后进入不同的分支
for(int i = p3; i < s3.size(); ++i){
if(s1[p1] != s2[p2]){//只有一种选择
//选择s1
if(s3[i] == s1[p1]) ++p1;
//选择s2
else if(s3[i] == s2[p2]) ++p2;
// 选哪个都不行
else return false;
}else{//有两种选择
//如果两种选择都不满足要求
if(s3[i] != s1[p1]) return false;
//两种选法进入不同路口
bool flag1 = interleave(s1, p1+1, s2, p2, s3, i+1, vec); //选s1
if(flag1){
vec[p1+1][p2] = 1; //选s1可行
return true;
}
bool flag2 = interleave(s1, p1, s2, p2+1, s3, i+1, vec); //选s2
if(flag2){
vec[p1][p2+1] = 1; //选s1可行
return true;
}
vec[p1][p2] = 0; //均不可行
return false;
}
}
return true;
}
bool isInterleave(string s1, string s2, string s3) {
if(s1.size() + s2.size() != s3.size()) return false;
vector<vector<int>> vec(s1.size()+1, vector<int>(s2.size()+1, -1)); //初始化为-1
return interleave(s1, 0, s2, 0, s3, 0, vec);
}
};
上面的代码for循环可以去掉,思路还是大同小异,效率也差不多:
class Solution {
public:
// p1, p2, p3分别是s1, s2, s3中下一个待选元素下标
bool interleave(string &s1, int p1, string &s2, int p2, string &s3, int p3, vector<vector<int>> &vec){
if(vec[p1][p2] != -1) return vec[p1][p2];//已经存在记录,直接返回相应值
// 如果p1 == s1.size() 或者 p2 == s2.size(),则有一个字符串被选完,只需比较另一个字符串和s3的剩余部分是否相等即可
if(p1 == s1.size()){
vec[p1][p2] = s2.substr(p2) == s3.substr(p3) ? 1 : 0;
return s2.substr(p2) == s3.substr(p3);
}
if(p2 == s2.size()){
vec[p1][p2] = s1.substr(p1) == s3.substr(p3) ? 1 : 0;
return s1.substr(p1) == s3.substr(p3);
}
if(s3[p3] == s1[p1]){
if(interleave(s1, p1+1, s2, p2, s3, p3+1, vec)){
vec[p1+1][p2] = 1;
return true;
}
}
if(s3[p3] == s2[p2]){
if(interleave(s1, p1, s2, p2+1, s3, p3+1, vec)){
vec[p1][p2+1] = 1;
return true;
}
}
vec[p1][p2] = 0; //均不可行
return false;
}
bool isInterleave(string s1, string s2, string s3) {
if(s1.size() + s2.size() != s3.size()) return false;
vector<vector<int>> vec(s1.size()+1, vector<int>(s2.size()+1, -1)); //初始化为-1
return interleave(s1, 0, s2, 0, s3, 0, vec);
}
};
动态规划
其实这个题更好的解法是动态规划,用动态规划很好理解这个题,具体就不分析了,可以看交错字符串 -官方题解。
public:
bool isInterleave(string s1, string s2, string s3) {
vector < vector <int> > f(s1.size() + 1, vector <int> (s2.size() + 1, false));
int n = s1.size(), m = s2.size();
if (n + m != s3.size()) return false;
f[0][0] = true;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
if (i > 0) f[i][j] = (f[i - 1][j] && s1[i - 1] == s3[ i + j - 1]);
if (j > 0) f[i][j] = f[i][j] || (f[i][j - 1] && s2[j - 1] == s3[ i + j - 1]); //如果上一步为true,这一步也为true,所以使用或运算
//if (j > 0) f[i][j] |= (f[i][j - 1] && s2[j - 1] == s3[ i + j - 1]);
}
}
return f[n][m];
}
};
优化,使用滚动数组优化空间复杂度,可以看到上述代码中其实并不需要把每个f的值都存储起来,因为f 的第 i 行只和第i−1 行有关,具体说:
f为二维数组,i对应行,j对应列
f[i][j] = (f[i - 1][j] && s1[i - 1] == s3[ i + j - 1]):这儿用到了上一行的元素f[i - 1][j]
f[i][j] = f[i][j] || (f[i][j - 1] && s2[j - 1] == s3[ i + j - 1]);:这儿用到了这一行的元素f[i][j - 1]
所以可以使用滚动数组优化:
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
vector <int> f(s2.size() + 1, false);
int n = s1.size(), m = s2.size();
if (n + m != s3.size()) return false;
f[0] = true;
for (int i = 0; i <= n; ++i) {
for (int j = 0; j <= m; ++j) {
//这么写的原因是上一行的值正是保存在f[j],这一边的目的是更新值
if (i > 0) f[j] = f[j] && (s1[i - 1] == s3[ i + j - 1]);
if (j > 0) f[j] |= (f[j - 1] && s2[j - 1] == s3[ i + j - 1]);
}
}
return f[m];
}
};