115. 不同的子序列
我的思路
-
字符串 + 出现个数,显然这极大可能是一个 动态规划 的题目。
-
1.确定数组f的含义和结构。显然,s的长度和t的长度均会对最终的结果造成影响。
- 设
f[i][j]
的含义是:字符串s[0:i]和字符串t[0:j],s[0:i]的子序列中t[0:j]出现的个数。 - 那么,最终的结果就是
f[s.size()-1][t.size()-1]
.
- 设
-
2.确定转移状态。我们应该如何求解
f[i][j]
?-
假设 s=“babgbag”,t=“bag”。
-
假设我们现在的状态是:
s => babgbag ↑ i t => bag ↑ j
- s[i] != t[j],那么,
f[i][j] = f[i-1][j]
。
- s[i] != t[j],那么,
-
假设我们现在的状态是:
s => babgbag ↑ i t => bag ↑ j
- s[i] = t[j],那么,
f[i][j] = f[i-1][j-1] + f[i-1][j]
。
- s[i] = t[j],那么,
-
那么我们得到了转移方程:
f[i][j] = { f[i-1][j] , s[i] = t[j]; f[i-1][j-1] + f[i-1][j], s[i] != t[j]; }
-
其实上述转移方程是不完全的。因为,在写上述转移方程的时候,我们有一个隐含的条件:i >= j. 因为根据题目含义,当 i < j时,f[i][j] = 0。
-
真正的转移方程:
f[i][j] = { f[i-1][j] , s[i] = t[j] && i >= j; f[i-1][j-1] + f[i-1][j] , s[i] != t[j] && i >= j; 0 , i < j; }
-
-
3.确定初始状态。
- 显然,上述转移方程中i和j的值均应该大于等于1.
- 显然,
f[0][0] = (s[0] == t[0] ? 1 : 0);
- 显然,
f[i][0] = 字符串s[0:i]中t[0]的个数。(i>0)
- 显然,
f[0][j] = 0。(j > 0)
由上述思路,可以写出AC代码:
class Solution {
public:
int numDistinct(string s, string t) {
int s_size = s.size();
int t_size = t.size();
if (s_size < t_size){ // 剪枝1
return 0;
}
if (s_size == t_size){ // 剪枝2
return s == t ? 1 : 0;
}
vector<vector<long long>> f(s_size, vector<long long>(t_size, 0));//注意1
f[0][0] = (s[0] == t[0] ? 1 : 0);
for (int i = 1; i < s_size; i ++){
if (s[i] == t[0]){
f[i][0] = f[i-1][0] + 1;
}else{
f[i][0] = f[i-1][0];
}
}
for (int j = 1; j < t_size; j ++){
f[0][j] = 0;
} // 这三行代码可以省略,因为f初始化为0
for (int i = 1; i < s_size; i ++){
for (int j = 1; j < t_size; j ++){
if (s[i] == t[j]){
f[i][j] = f[i-1][j] + f[i-1][j-1];
}else{
f[i][j] = f[i-1][j];
}
}
}
return f[s_size-1][t_size-1];
}
};
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:7.2 MB, 在所有 C++ 提交中击败了52.95%的用户
-
有一个注意点,f中数据的类型是long long,不能是int。
- 其实我刚开始也没注意到,提交了一次后发现报错:
Line 27: Char 41: runtime error: signed integer overflow: 891953512 + 1474397256 cannot be represented in type 'int' (solution.cpp) SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior prog_joined.cpp:36:41
- 也就是,超出int范围了。
- 但是题目保证:题目数据保证答案符合 32 位带符号整数范围。
- 先说结论:最后答案
f[s_size-1][t_size-1]
不一定是最大的数字。 - 考虑一种极端的样例:s=“aab”,t=‘ac’。显然最后应该返回0。但是f[1][0] = 2。
- 也就是说在某一次计算f[i][j]的时候,可能会超出int的范围。因此,f应该用long long。
- 其实我刚开始也没注意到,提交了一次后发现报错:
-
复杂度分析
- 时间复杂度:
O(s.size()*t.size())
,两层for循环。 - 空间复杂度:
O(s.size()*t.size())
,定义了f数组。
- 时间复杂度:
空间复杂度优化
转移方程:
f[i][j] = {
f[i-1][j] , s[i] = t[j] && i >= j;
f[i-1][j-1] + f[i-1][j] , s[i] != t[j] && i >= j;
0 , i < j;
}
- 可以看出,f的第一维可以优化掉。
j列
↓
i-1行 → ...|j-1(1)|j(2)|...
i行 → ...|j-1(3)|j(4)|...
根据转移方程,f[i][j]只与f[i-1][j-1]和f[i][j-1]有关[也就是(4)只与(1)和(2)有关]。
因此,可以使用一维的f,AC代码如下:
class Solution {
public:
int numDistinct(string s, string t) {
int s_size = s.size();
int t_size = t.size();
if (s_size < t_size){ // 剪枝1
return 0;
}
if (s_size == t_size){ // 剪枝2
return s == t ? 1 : 0;
}
vector<long long> f(t_size, 0);
f[0] = (s[0] == t[0] ? 1 : 0);
for (int i = 1; i < s_size; i ++){
for (int j = t_size-1; j >= 1; j --){ // 注意2
if (j > i){
f[j] = 0;
continue;
} // 注意3
if (s[i] == t[j]){
f[j] = f[j] + f[j-1];
}
}
if (s[i] == t[0]){
f[0] = f[0] + 1;
} // 注意4
}
return f[t_size-1];
}
};
- 注意2:j变量要倒序遍历!因为f[j]取决于f[j]和f[j-1],那么在确定f[j]时,f[j-1]必须是未更新的f[j-1],因此更新顺序应该是倒序。
- 注意3:当j>i时,显然,应该f[j] = 0.
- 注意4:因为是倒序,因此这三行代码应该放到for(j)的后面。
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6.2 MB, 在所有 C++ 提交中击败了97.15%的用户
- 复杂度分析
- 时间复杂度:
O(s.size()*t.size())
,两层for循环。 - 空间复杂度:
O(t.size())
,定义了一维的f数组。
- 时间复杂度:
2021.3.17