给定一个字符串 s,计算 s 的不同非空子序列的个数。因为结果可能很大,所以返回答案需要对 109 + 7 取余 。
字符串的子序列是经由原字符串删除一些(也可能不删除)字符但不改变剩余字符相对位置的一个新字符串。
例如,“ace” 是 “abcde” 的一个子序列,但 “aec” 不是。
示例 1:
输入:s = “abc”
输出:7
解释:7 个不同的子序列分别是 “a”, “b”, “c”, “ab”, “ac”, “bc”, 以及 “abc”。
示例 2:
输入:s = “aba”
输出:6
解释:6 个不同的子序列分别是 “a”, “b”, “ab”, “ba”, “aa” 以及 “aba”。
示例 3:
输入:s = “aaa”
输出:3
解释:3 个不同的子序列分别是 “a”, “aa” 以及 “aaa”。
提示:
1 <= s.length <= 2000
s 仅由小写英文字母组成
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/distinct-subsequences-ii
(1)回溯算法
如果使用回溯算法,那么本题就与LeetCode_回溯_中等_78.子集这题十分相似,只不过多了一个去重的操作,这里可以选择用 hashSet 来记录不同的非空子序列。但是由于本题 s 的长度最高可达 2000,所以在 LeetCode 中提交时会出现“超出时间限制”的提示!
(2)动态规划
思路参考本题官方题解。
① 定义 dp 数组,dp[i] 表示 s 中以某个小写字母结尾的不同非空子序列个数。
/*
dp[0] 表示 s 中以小写字母 a 结尾的不同非空子序列个数
dp[1] 表示 s 中以小写字母 b 结尾的不同非空子序列个数
...
dp[25] 表示 s 中以小写字母 z 结尾的不同非空子序列个数
它们的初始值均为 0
*/
long[] dp = new long[26];
② 定义变量 res,用于记录不同非空子序列的个数,其初始值为 0。
③ 遍历字符串 s,记当前的小写字母为 letter = s.charAt(i),然后再做如下处理:
1)记 letterCnt 表示 s[0…i - 1] 中以 letter 结尾的不同非空子序列个数,如果 i = 0,那么 letterCnt 为默认值 0;
long letterCnt = dp[letter - 'a'];
2)算上当前的 letter,那么 s[0…i] 中以 letter 结尾的子序列个数 dp[letter - ‘a’] = res + 1;
/*
(1) 更新 dp[letter - 'a'],让其表示 s[0...i] 中以 letter 结尾的子序列个数
(2) 此处的 res 表示的还是 s[0...i - 1] 中不同非空子序列的个数
(3) dp[letter - 'a'] = res + 1 的说明:
如果我们已知 s[0...i - 1] 中不同非空子序列的个数为 res,那么要求 s[0...i] 中以 s[i] = letter 结尾的子序列时
可以直接将 res 个子序列的末尾都拼接上字母 letter,并且同时还考虑以 letter 自身一个字母所表示的子序列,所以就可
以推出 dp[letter - 'a'] = res + 1
*/
dp[letter - 'a'] = res + 1;
3)更新 res;
/*
(1) 在更新 res 之前,res 表示 s[0...i - 1] 中不同非空子序列的个数;
(2) 此时要求出 s[0...i] 中不同非空子序列的个数,并且已知
- 第 i 个小写字母 s[i] = letter
- s[0...i - 1] 中以 letter 结尾的不同非空子序列个数 letterCnt
(3) res += (res + 1 - letterCnt) % MOD_NUM 的说明如下:
- 首先,如果不排除重复的子序列,那么 res = res + res + 1,此处的推导与上面的 dp[letter - 'a'] = res + 1 类似,只不过
这里的更新后的 res 是包含更新前的 res 的,所以需要进行累加。
- 然后,我们再考虑去重的问题,此时我们需要思考,从 res 变为 res + res + 1 的过程中,有哪些子序列是重复的?
例如,s[0...2] = "bac",s[0...3] = "bacc"
s[0...2] 中不同非空子序列 = {b, a, c, ba, bc, ac, bac},res -> res + res + 1 后
s[0...3] 中非空子序列 = {b, a, c, ba, bc, ac, bac, bc, ac, cc, bac, bcc, acc, bacc, c}
显然我们可以发现重复的子序列 = {c, bc, ac, bac},而它们正好是 s[0...2] 中以 letter(即小写字母 c) 结尾的不同非空子序列!
- 其实,这一点也比较容易理解,假设 s[0...i - 1] 中的某个以 letter 结尾的子序列 bac,那么去掉 c 后的 ba 一定也是 s[0...i - 1]
的子序列,经过 res -> res + res + 1 后,一定会生成子序列 ba + c = bac,这样便与之前存在的 bac 重复了,所以 res + res + 1
还需要减去 letterCnt。
*/
res += (res + 1 - letterCnt) % MOD_NUM;
//思路1————回溯算法
class Solution {
int MOD_NUM = 1000000007;
// res 记录不同非空子序列的个数,其初始值为 0
int res = 0;
// hashSet 记录所有的非空子序列
Set<String> hashSet = new HashSet<>();
// builder 记录当前的非空子序列
StringBuilder builder = new StringBuilder();
public int distinctSubseqII(String s) {
int length = s.length();
char[] chs = s.toCharArray();
backtrace(chs, 0);
return res;
}
public void backtrace(char[] chs, int start) {
String curSubString = builder.toString();
if (!curSubString.equals("") && !hashSet.contains(curSubString)) {
res = (res + 1) % MOD_NUM;
hashSet.add(curSubString);
}
//回溯算法框架
for (int i = start; i < chs.length; i++) {
//做选择
builder.append(chs[i]);
backtrace(chs, i + 1);
//撤销选择
builder.deleteCharAt(builder.length() - 1);
}
}
}
//思路2————动态规划
class Solution {
public int distinctSubseqII(String s) {
long MOD_NUM = 1000000007;
int length = s.length();
//dp[i] 表示 s 中以某个小写字母结尾的不同非空子序列个数
long[] dp = new long[26];
// res 记录不同非空子序列的个数,初始值为 0
long res = 0;
//遍历字符串 s
for (int i = 0; i < length; i++) {
//当前遍历到的小写字母记为 letter
char letter = s.charAt(i);
//letterCnt 表示 s[0...i - 1] 中以 letter 结尾的子序列个数
long letterCnt = dp[letter - 'a'];
/*
(1) 算上当前的 letter,s[0...i] 中以 letter 结尾的子序列个数 dp[letter - 'a'] = res + 1
(2) 此处的 res 表示 s[0...i - 1] 中不同非空子序列的个数
*/
dp[letter - 'a'] = res + 1;
//更新 res
res += (res + 1 - letterCnt) % MOD_NUM;
}
return (int)(res % MOD_NUM);
}
}