今天依然是子序列问题的延续,要比之前的内容难。这两道题都涉及到主串和模式串情境下“删除主串末尾元素”的思想。第1题重在问题的转化,将“是或不是子串的判断问题”转化为了“计算两个数组的最大相同子序列长度”问题;第2题则要仔细思考出现次数的来源,以及初始化方面的物理含义。
第1题(LeetCode 392. 判断子序列)需要首先将问题转化为“求s和t的最大相同子序列长度”,用dp[i][j]表示“s[0 : i - 1]和t[0 : j - 1]的最大相同子序列长度”(使用i - 1、j - 1而不用i、j的原因与之前一样,还是为了后面的初始化方便)。这里要注意,s是较短的字符串, t是较长的字符串。
与之前的题目一样,当s[i - 1]与t[j - 1]相等时,dp[i][j]需要在dp[i - 1][j - 1]的基础上加1;而两者不相等时,因为s较短,t较长,题目要求s包含于t内,所以要删除t[j - 1],取dp[i][j - 1]。这里不应该考虑删除s[i - 1],取dp[i - 1][j],因为s包含于t内,删除s的末尾得到的dp数值是没有意义的。
这样一来,dp每个位置的数值都依赖于其左上方和左方,所以需要初始化第0行和第0列。其分别对应s为空和t为空的情况,所以都初始化为0即可。也正因为这个依赖关系,所以内外层循环的遍历方向都为正向。最终如果dp右下角的值与s的长度相等,则代表s是t的子序列;否则就不是。
- class Solution {
- public:
- bool isSubsequence(string s, string t) {
- vector
int>> dp(s.size() + 1, vector<int>(t.size() + 1, 0)); - for (int i = 1; i <= s.size(); ++i) {
- for (int j = 1; j <= t.size(); ++j) {
- if (s[i - 1] == t[j - 1]) { // 不是s[i] == t[j]
- dp[i][j] = dp[i - 1][j - 1] + 1;
- }
- else {
- dp[i][j] = dp[i][j - 1];
- }
- }
- }
- return dp[s.size()][t.size()] == s.size();
- }
- };
这道题还有时间复杂度更优的双指针解法,两个指针分别位于s和t中。不断的右移t指针,使t指针所指的值匹配s指针所指的值。匹配成功后s也右移,否则就只有t右移。如果s指针最终能到达s的末尾,说明s是t的子序列;否则就不是。
- class Solution {
- public:
- bool isSubsequence(string s, string t) {
- int indS = 0, indT = 0;
- while (indS < s.size() && indT < t.size()) {
- if (s[indS] == t[indT]) {
- ++indS, ++indT;
- }
- else {
- ++indT;
- }
- }
- return indS == s.size();
- }
- };
第2题(LeetCode 115. 不同的子序列)相比上一题就难很多,自己没有做出来。题解将dp[i][j]定义为s[0 : - 1]中出现t[0 : j - 1]的次数(使用i - 1、j - 1而不用i、j的原因与之前一样,还是为了后面的初始化方便)。注意这道题中s较长,t较短,与上一题刚好相反。
这道题同样分s[i - 1]与t[j - 1]相等和不相等两种情况。相等时,出现次数一部分来源于s[0 : i - 2]中t[0 : j - 2]的出现次数,对应dp[i - 1][j - 1]。另一部分则来源于s[0 : i - 2]中t[0 : j - 1]的出现次数(即删除掉s[i - 1]),对应dp[i - 1][j]。前者比较容易理解,后者是类似“s[0 : i - 1]为dogg,t[0 : j - 1]为dog”的情况,不仅应该计算dog中do的出现次数,还应该加上dog中dog的出现次数。而不相等时,出现次数则只能来源于s[0 : i - 2]中t[0 : j - 1]的出现次数(即删除掉s[i - 1]),对应dp[i - 1][j]。
所以当前位置的dp值依赖于其左上方和上方位置,就需要初始化第0行和第0列。其分别对应s为空和t为空的情况。第0列,对应t为空,dp[i][0]对应s[0 : i - 1]“删除任意字符后出现空字符”的个数,只有删除全部元素时才能出现空,所以只有一种删除方式,dp[i][0]都应该初始化为1;第0行,对应s为空,dp[0][j](j > 0)对应空字符“删除任意字符后出现t[0 : i - 1]”的个数,空字符无论怎么删除都还是空字符,所以有0中删除方式,dp[0][j](j > 0)都应该初始化为0。而特殊的dp[0][0],则代表空字符“删除任意字符后出现空字符”的个数,也只有“不做任何删除”这一种方法,所以应该初始化为1。
也正因为上一段开头提到的依赖关系,所以双层循环的遍历方向都应该是正向。最终返回dp右下角的值作为结果。
- class Solution {
- public:
- int numDistinct(string s, string t) {
- vector
unsigned long long>> dp(s.size() + 1, vector<unsigned long long>(t.size() + 1, 0)); - for (int i = 0; i <= s.size(); ++i) {
- dp[i][0] = 1;
- }
- for (int i = 1; i <= s.size(); ++i) {
- for (int j = 1; j <= t.size(); ++j) {
- if (s[i - 1] == t[j - 1]) { // 不是s[i] == t[j]
- dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
- }
- else {
- dp[i][j] = dp[i - 1][j];
- }
- }
- }
- return dp[s.size()][t.size()];
- }
- };
这道题的测试用例中会包含超出long long范围的数字,所以要将dp类型设置为unsigned long long,也就是uint64_t(第一次遇到,具体可见这里)。