LeetCode 115 Distinct Subsequences 不同字串
题目
Given a string S and a string T, count the number of distinct subsequences of S which equals T.
A subsequence of a string is a new string which is formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. (ie, “ACE” is a subsequence of “ABCDE” while “AEC” is not).
It’s guaranteed the answer fits on a 32-bit signed integer.
题目给定两个字符串S, 和T,要求返回S有多少不同子串等于T。这里由于定义子串是从原始字符串中删除任意(包括0个)字符的结果,所以当S中有重复字符时,删掉不同位置的相同字符算作不同的子串。
比如:
Input: S = “rabbbit”, T = “rabbit”
Output: 3
Explanation:
As shown below, there are 3 ways you can generate “rabbit” from S.
(The caret symbol ^ means the chosen letters)
rabbbit
^^^^ ^^
rabbbit
^^ ^^^^
rabbbit
^^^ ^^^
S中的3个‘b’删掉任意一个都等于T,所以结果为3.
解析
本题涉及到两个字符串相关的关系,一般会思考是不是能用二维动态规划解决。比如编辑距离等题目,就会设计二维数组dp,其中
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]代表S从0到i, T从0到j两个子串得到的结果。本题仍然可以采用这一策略进行:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]代表S从0到i的子串有多少不同子串等于 T从0到j这部分。
那么
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]怎么从前面推断出来呢?
首先如果
S
[
i
]
=
=
T
[
j
]
S[i]==T[j]
S[i]==T[j],也就是两个字符串最后一个字符是一样的,那就有两种情况:
第一 用S[i]对位T[j],二者一样,那就保留S[i]不删掉用它来表示T[j],用S从0到i-1这部分表示出T从0到j-1这部分,双方后面都加上一个相同字符,数量自然不变,此时符合题意的子串数量就是
d
p
[
i
−
1
]
[
j
−
1
]
dp[i-1][j-1]
dp[i−1][j−1];
第二,不采用S[i],直接删掉他,那么此时就要用S传从0到i-1这部分来表示T从0到j这部分,则符合题意的数量就是
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]。
所以
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
−
1
]
+
d
p
[
i
−
1
]
[
j
]
,
S
[
i
]
=
=
T
[
j
]
dp[i][j] = dp[i-1][j-1]+dp[i-1][j],S[i]==T[j]
dp[i][j]=dp[i−1][j−1]+dp[i−1][j],S[i]==T[j]
而当最后一个字符不相等时,显然只能删掉S[i],符合题意的数量就是
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]。
也即
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
,
S
[
i
]
!
=
T
[
j
]
dp[i][j] = dp[i-1][j],S[i]!=T[j]
dp[i][j]=dp[i−1][j],S[i]!=T[j]
另外上述讨论一定满足
j
≤
i
j \le i
j≤i,如果
j
>
i
j>i
j>i,要得到的字符串比原始字符串还要长,显然不可能,数量只能为0.
基于上述的状态表示与转移策略,C++代码如下:
int solution::numDistinct(string s, string t)
{
if (t.empty()) return 1;
else if (s.empty()) return 0;
int m = s.size(), n = t.size();
vector<vector<uint64_t>>dp(m + 1, vector<uint64_t>(n + 1, 0));
for (int i = 0; i <= m; ++i) dp[i][0] = 1;
for (int j = 1; j <= n; ++j)dp[0][j] = 0;
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (j > i)dp[i][j] = 0;
else {
if (s[i-1] == t[j-1])dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
else dp[i][j] = dp[i - 1][j];
}
}
}
return dp[m][n];
}
这里我们为dp数组行列各加一,用来存放空串时的情况。显然S为空串时,除非T也为空子串数量是1,否则都是0;而当T是空串时,无论S是什么,数量都是1.
这里
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]就表示输入S[0,i-1] T[0,j-1]的结果。
实现上跟前面解析一致,对于
j
>
i
j>i
j>i直接取0;否则分别讨论s[i-1]和t[j-1]是否相等的情况。
优化
观察可以发现,无论何种情况,
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]都只和
d
p
[
i
−
1
]
dp[i-1]
dp[i−1]那部分有关,因此只需要1行存放数据就够了。由于用到的是左边和左上角的信息,我们采取两个变量left和cur分别记录前一个值和当前值,cur计算完成,其左上角点就不再需要了,更新为left即可,然后left更新为cur,再去计算下一个点。节省了存储空间也一定程度上提高了速度。
代码如下:
int solution::numDistinct(string s, string t)
{
if (t.empty()) return 1;
else if (s.empty()) return 0;
int m = s.size(), n = t.size();
uint64_t left = 0, cur = 0,last=0;
vector<uint64_t>dp(m + 1, 1);
for (int j = 1; j <= n; ++j) {
for (int i = j; i <= m; ++i) {
if (s[i - 1] == t[j - 1]) cur = dp[i - 1] + left;
else cur = left;
dp[i - 1] = left;
left = cur;
}
dp[m] = left;
left = 0;
}
return dp[m];
}
这里dp就是记录当前j值一定,每个i对应的情况,每次i从i=j开始讨论即可。left初始化为0,通过dp[i-1](相当于二维动规中的dp[i-1][j-1])和left(相当于二维动规中的dp[i-1][j])计算cur值。cur计算完成,dp[i-1]就不需要了,将它赋值为left,相当于此时dp[i-1] 更新成了二维动规中的dp[i-1][j],cur变成了此时的left,继续计算下一个i的情况。