马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
子序列自动机
根本概念
子序列自动机 是一种高效处理惩罚 子序列匹配 题目的数据结构。
焦点头脑: 预处理惩罚出从每个位置 \(i\) 开始(包罗 \(i\) ),每个字符 \(c\) 下一次出现的位置。
界说:
- n: 字符串 \(s\) 的长度。
- \(\sum\) : 字符集巨细。
- nxt[c]: 体现从位置 \(i ~ (0 \le i \le n)\) 开始,字符 \(c\) 第一次出现的位置。若不存在,则设为 \(n\) (大概 \(n + 1\) )。
构建方法
二维数组法(小字符集)
- string s; cin >> s;
- int n = s.size();
- vector<vector<int> > nxt(n + 1, vector<int> (26, n));
- for(int i = n - 1; i >= 0; i --){
- nxt[i] = nxt[i + 1];
- nxt[i][s[i] - 'a'] = i;
- }
复制代码 时间复杂度: \(O(n \cdot \sum)\)
二分查找法(大字符集)
- vector<int> pos[N]; // N: 字符集大小
- for(int i = 0; i < n; i ++){
- pos[arr[i]].emplace_back(i);
- }
- // 查询字符 c 在位置 cur 之后的下一个位置
- auto it = upper_bound(pos[c].begin(), pos[c].end(), cur);
- if(it == pos[c].end()){
- // 不存在
- }else{
- // 下一个位置 = *it;
- }
复制代码 时间复杂度: 构建 \(O(n)\),单次查询 \(O(log~n)\)
重要应用
判定是否为子序列
给出模式串 \(T\),判定是否为 \(S\) 的子序列。
这个给出针对 小字符集 的判定方法。- int cur = 0;
- for(char ch : T){
- cur = nxt[cur][ch - 'a'];
- if(cur == n) return false;
- }
- return true;
复制代码 盘算差异的子序列
差异的子序列 II : 给定一个字符串 \(s\),盘算 \(s\) 的 差异非空子序列 的个数(答案对 1e9 + 7 取余)。
子序列自动机+DP
界说:
- dp: 从下标 \(i\) 开始到字符串末端,可以大概构成的 全部差异子序列的数目 (包罗空串)。
状态转移方程:
\[dp = 1 + \sum_{c = 0}^{25} dp\left[nxt[c] + 1\right]\]
- 1: 体现当前位置留空。
- \(\sum_{c = 0}^{25} dp\left[nxt[c] + 1\right]\): 罗列当前位置须要放置的每个字符 \(c\) 。
- nxt[c]: 体现从 \(i\) 位置开始下一个字符 \(c\) 的位置。
- nxt[c] + 1: 确定字符 \(c\) 后,字符 \(c\) 的下一个位置。
- dp[nxt[c] + 1]: 字符 \(c\) 背面可以跟的差异子序列的数目。
代码:- class Solution {
- public:
- int distinctSubseqII(string s) {
- int n = s.size();
- int mod = 1e9 + 7;
- // 初始化 子序列自动机
- vector<vector<int> > nxt(n + 1, vector<int> (26, n));
- for(int i = n - 1; i >= 0; i --){
- nxt[i] = nxt[i + 1];
- nxt[i][s[i] - 'a'] = i;
- }
- // dp[i] := 从下标 i 开始到字符串末尾,能够组成的 所有不同子序列 的数量(包括空串)
- vector<int> dp(n + 1, 0);
- dp[n] = 1; // 空串的不同子序列数量也为 1
- for(int i = n - 1; i >= 0; i --){
- dp[i] ++; // 当前位置字符为空
- for(int c = 0; c < 26; c ++){
- if(nxt[i][c] != n){ // 避免 i 位置及以后不存在字符 c
- dp[i] = ((long long)dp[i] + dp[nxt[i][c] + 1]) % mod;
- }
- }
- }
- return (dp[0] - 1 + mod) % mod; // 排除 dp[0] - 1 取负
- }
- };
复制代码 时间复杂度: \(O(n \cdot \sum)\)
线性DP
这里更加保举利用 线性DP 来办理这题。
界说:
- last[c]: 字符 c 末了出现的位置。
- dp: \(s[0, i]\) 子串中差异子序列的数目。
状态转移方程:
\[dp = (dp[i - 1] \times 2 - dp[last[c] - 1] + MOD) ~ \% ~ MOD\]
- dp[i - 1] * 2: \(i\) 位置有 选 和 不选 两种状态。
- - dp[last[c] - 1]: 之前出现过字符 \(c\) ,会重复思量 \(s\left[0,~ last[c] - 1\right]\) 部门的 差异子序列个数。
代码:- class Solution {
- public:
- int distinctSubseqII(string s) {
- int n = s.size();
- int mod = 1e9 + 7;
- s = " " + s; // 统一起始索引从 1 开始
- unordered_map<char, int> last; // last[c]:= 字符 c 最后出现的位置
- // dp[i] := s[0, i]子串中不同子序列的数量
- vector<int> dp(n + 1, 0);
- dp[0] = 1; // 空串的不同子序列数量为 1
- for(int i = 1; i <= n; i ++){
- dp[i] = dp[i - 1] * 2 % mod;
- if(last.count(s[i])){ // 减去重复部分
- dp[i] = (dp[i] - dp[last[s[i]] - 1] + mod) % mod;
- }
- last[s[i]] = i; // 更新字符最后出现的位置
- }
- return (dp[n] - 1 + mod) % mod;
- }
- };
复制代码 时间复杂度: \(O(n \cdot \sum)\)
|