看了题解过后觉得好像有点熟悉……但是总之是想不起来怎么做的了,自己的做法也和题解不一样。

是一道很好的题啦,把两种做法都写一下作个总结。

题面

给定一个长度为 \(n\) (\(n\le 10^5\))的字符串 \(S\),字符集 \(\Sigma\) 仅包含前 \(10\) 种英文小写字母。再给定 \(Q\) 次(\(Q\le 10^6\))询问,每次给出区间 \([L, R]\),输出 \(S\) 的子段 \(S[L, R]\) 中有多少个本质不同的子序列,不含空串。

答案对 \(10^9+7\) 取模。


解析

解法1:序列自动机+猫树分治

最近对猫树分治比较上瘾,看啥区间询问都像猫树分治……

如果你觉得这是个自己没有见过的算法,说不定只是你没见过它的名字……就是把区间从中间划开,每次处理跨过区间中点的询问的算法。

首先考虑暴力怎么做。看到“本质不同的子序列”,就想到了序列自动机;就像看到“本质不同的子串”就想到后缀自动机一样。于是我们可以暴力建出子串的序列自动机,再拓扑做一遍 DP 即可。本质上是在求序列自动机这个 DAG 上的非空路径条数

序列自动机是非常特殊的 DAG,拓扑序就是从左到右。记位置 \(i\) 之前的第一个 \(S_i\) 的位置在 \(las_i\),那么拓扑 DP 的式子就可以写为:

\[dp_i=\sum_{j=las_i}^{i-1}dp_j+1
\]

然后考虑优化,注意到字符集大小只有 \(10\),说不定维护两个区间的某些信息后可以快速合并计算答案。于是可以想到猫树分治,那么需要解决的问题就是:询问 \([l,r]\) 跨过了分治中点 \(m\),维护 \([l,m]\) 的信息和 \([m+1,r]\) 的信息,支持快速合并计算答案。

先分析区间合并需要哪些信息:

左区间的 DP 值是确定的(不需要用到左区间之外的信息),但是右区间转移时可能会用到左区间的一些 DP 值,更确切地说,是左区间的一些后缀的 DP 值之和。记 \(p_c\) 为字符 \(c\) 在左区间最后一次出现的位置,若未出现则默认为左区间开头。记 \(Sdp_c\):

\[Sdp_c=\sum_{i=p_c}^m
\]

不难发现右区间的 DP 值可以完全用 \(Sdp_{c}\)(\(0\le c\le 9\))的一次多项式来表示,于是我们可以预处理右区间的DP 前缀和用 \(Sdp\) 表示的结果。

\[\sum_{i=m+1}^{r}dp_i=\sum_{c\in \Sigma}Sdp_{l,c}\times a_{r,c}
\]

于是左区间需要预处理的就是:

  • 后缀的 DP 值之和;
  • 后缀的 \(Sdp_c\) 的确切值。

之前提到这个 DP 本质上是在求 DAG 上的路径条数,那么求 \(Sdp_c\) 其实就是在求以 \([p_c,m]\) 结尾的路径的条数。

注意到 DP 不一定要顺着拓扑序,也可以倒着做——求从一个点出发的路径。由于左区间的预处理是每次向左扩展一个点,这样定义 DP 会更容易。具体地,定义

  • \(f_i\):左区间内从 \(i\) 出发的路径条数;
  • \(g_{i,c}\):左区间内从 \(i\) 出发,在 \([p_c,m]\) 结束的路径条数。

序列自动机还有一个特点:一个点的入度可以很大,但是出度是字符集大小 \(|\Sigma|\)。所以我们可以 \(\mathcal O(|\Sigma|)\) 计算出单个 \(f_i,g_{i,c}\)。这意味着预处理左区间要 \(\mathcal O(n|\Sigma|^2)\)?好像不够快的样子……再观察,注意到相邻两个点能够转移到的点集只有一个点不同,于是可以 \(\mathcal O(|\Sigma|)\) 在上一个点的 DP 值上修改。

最后预处理左右区间的复杂度都是 \(\mathcal {O(n|\Sigma|)}\),询问可以 \(\mathcal O(|\Sigma|)\) 合并左右区间,最终复杂度 \(\mathcal O\Big((Q + n\log n)|\Sigma|\Big)\),足以通过本题。

解法2:矩阵求逆

换一种 DP 的定义,不从序列自动机考虑:\(dp_{i,c}\) 表示已经决策到第 \(i\) 个字符,当前子序列以 \(c\) 结尾的方案数。这种定义天然保证了本质不同。

以下用 \(R\) 代称 \(|\Sigma|\)。

转移只需要决策当前字符选不选,如果选,则上一个字符是什么。由于 \(|\Sigma|\) 很小,可以直接写出矩阵的形式。显然转移矩阵只与 \(i\) 处的字符 \(c\) 有关(第 \(R\) 行用来表示空串)。

\[D_i=\begin{bmatrix}dp_{i,0}\\dp_{i,1}\\\vdots\\dp_{i,R}\end{bmatrix},V_{c,i,j}=\begin{cases}1&(i=j)\\1&(i=c)\\0&otherwise\end{cases}
\]

转移即为

\[D_i=V_{S_i}D_{i-1}
\]

所以答案就是一个区间的矩阵之积。套路地,我们发现我们并不需要整个矩阵,而是一个位置的值。具体地,我们需要的是:

\[\begin{pmatrix}1&1&\cdots&1\end{pmatrix}\times V_{S_l}\times V_{S_{l+1}}\times\cdots\times V_{S_r}\times\begin{pmatrix}0\\0\\\vdots\\1\end{pmatrix}
\]

由于矩阵长得特殊,我们可以手推矩阵的逆(虽然看题解之前我也忘了该怎么手推)。于是可以转换成矩阵的前缀积以及矩阵的逆的前缀积的形式:

\[\begin{aligned}
&M_i=V_{S_1}\times V_{S_2}\times\cdots\times V_{S_i}\\
&N_i=V_{S_i}^{-1}\times V_{S_{i-1}}^{-1}\times\cdots\times V_{S_1}^{-1}\\
&A=\begin{pmatrix}1&1&\cdots&1\end{pmatrix},B=\begin{pmatrix}0\\0\\\vdots\\1\end{pmatrix}\\
&ans=A\times N_{l-1}\times M_r\times B
\end{aligned}
\]

于是可以预处理行列向量:

\[\begin{aligned}
&I_i=A\times N_{i},J_i=M_i\times B
\end{aligned}
\]

具体预处理的方法以 \(J_i\) 为例,\(I_i\) 类似。

首先会发现 \(J_i=M_{i-1}\times V_{S_i}\times B\),这导致我们没法直接用 \(J_{i-1}\) 乘上一个矩阵得到 \(J_i\)。于是不可避免地要矩阵乘法。但是一定要用 \(\mathcal O(R^3)\) 的矩阵乘法吗?注意到 \(V_{S_i}\) 非常特殊——

\[M_{i,p,q}=\begin{cases}M_{i-1,p,q}&q=S_i\\M_{i-1,p,q}+M_{i-1,p,S_i}&q\neq S_i\end{cases}
\]

除了第 \(S_i\) 列,其他位置都加上了相同行的第 \(S_i\) 列的值。我们可以打一个“全局加 \(S_i\) 列”的 tag,但是 \(S_i\) 列本身并没有加,那我们就反过来,打上全局加 tag 后把这一列减去第 \(S_i\) 列,即清零。

这样就可以快速维护矩阵。对于 \(J_i\),实际上 \(J_{i,p}=M_{i,p,R}\),只需要矩阵最后一列的确切值;对于 \(I_i\),\(I_{i,p}=\sum_q N_{i,q,p}\),还需要同时维护矩形每一列的和。

最后复杂度可以做到 \(\mathcal O(n|\Sigma|^2+Q|\Sigma|)\)


源代码

点击展开/折叠 源代码(猫树分治)
/* Lucky_Glass */
#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm> const int N = 5e5 + 10, MOD = 1e9 + 7; inline int add(int a, const int &b) { return (a += b) >= MOD ? a - MOD : a; }
inline int sub(int a, const int &b) { return (a -= b) < 0 ? a + MOD : a; }
inline int mul(const int &a, const int &b) { return int(1ll * a * b % MOD); } #define ITERON(a, b, fun) a = fun(a, b) char str[N];
int las_pos[N], qry[N][2], ans[N];
int n; int doBF(const int &le, const int &ri) {
static int bf_sumf[N];
bf_sumf[le - 1] = 0;
for (int i = le; i <= ri; ++i) {
int fi;
if (las_pos[i] < le) fi = add(1, bf_sumf[i - 1]);
else fi = sub(bf_sumf[i - 1], bf_sumf[las_pos[i] - 1]);
bf_sumf[i] = add(bf_sumf[i - 1], fi);
}
return bf_sumf[ri];
} const int BOND = 200; int qry_id[N], tmp_qry_id[N];
int f[N][11], sum_f[N][11]; void solveDac(const int &le, const int &ri, const int &ql, const int &qr) {
if (ql > qr) return;
if (le + BOND >= ri) return; int las_ind[13] = {}, tmp_sum[13] = {}; int mi = (le + ri) >> 1;
for (int i = mi; i >= le; --i) {
for (int c = 0; c <= 10; ++c)
f[i][c] = add(tmp_sum[c], !las_ind[c]);
if (las_ind[str[i] - 'a'])
for (int c = 0; c <= 10; ++c)
ITERON(tmp_sum[c], f[las_ind[str[i] - 'a']][c], sub);
las_ind[str[i] - 'a'] = i;
for (int c = 0; c <= 10; ++c)
ITERON(tmp_sum[c], f[i][c], add);
for (int c = 0; c <= 10; ++c)
sum_f[i][c] = add(tmp_sum[c], !las_ind[c] && c < 10);
}
for (int i = mi + 1; i <= ri; ++i)
for (int c = 0; c < 10; ++c) {
if (las_pos[i] - 1 <= mi) {
if (i == mi + 1) f[i][c] = 0;
else f[i][c] = sum_f[i - 1][c];
} else {
f[i][c] = sub(sum_f[i - 1][c], sum_f[las_pos[i] - 1][c]);
}
if (las_pos[i] <= mi && str[i] - 'a' == c) ITERON(f[i][c], 1, add);
if (i == mi + 1) sum_f[i][c] = f[i][c];
else sum_f[i][c] = add(sum_f[i - 1][c], f[i][c]);
} int nql = ql - 1, nqr = qr + 1;
for (int i = ql; i <= qr; ++i)
if (qry[qry_id[i]][1] < mi) {
tmp_qry_id[++nql] = qry_id[i];
} else if (qry[qry_id[i]][0] > mi) {
tmp_qry_id[--nqr] = qry_id[i];
} else {
int qle = qry[qry_id[i]][0], qri = qry[qry_id[i]][1];
int &res = ans[qry_id[i]] = sum_f[qle][10];
if (qri == mi) continue;
for (int c = 0; c < 10; ++c)
ITERON(res, mul(sum_f[qle][c], sum_f[qri][c]), add);
}
for (int i = ql; i <= nql; ++i) qry_id[i] = tmp_qry_id[i];
for (int i = nqr; i <= qr; ++i) qry_id[i] = tmp_qry_id[i]; solveDac(le, mi, ql, nql);
solveDac(mi + 1, ri, nqr, qr);
} int rin(int &r) {
int c = getchar(); r = 0;
while (c < '0' || '9' < c) c = getchar();
while ('0' <= c && c <= '9') r = r * 10 + (c ^ '0'), c = getchar();
return r;
}
int main() { scanf("%s", str + 1);
n = (int)strlen(str + 1); int tmp_las_pos[20] = {};
for (int i = 1; i <= n; ++i) {
las_pos[i] = tmp_las_pos[str[i] - 'a'];
tmp_las_pos[str[i] - 'a'] = i;
} int qry_cnt = 0, ncas = rin(ncas);
for (int i = 1; i <= ncas; ++i) {
rin(qry[i][0]), rin(qry[i][1]);
if (qry[i][0] + BOND >= qry[i][1]) ans[i] = doBF(qry[i][0], qry[i][1]);
else qry_id[++qry_cnt] = i;
}
solveDac(1, n, 1, qry_cnt); for (int i = 1; i <= ncas; ++i)
printf("%d\n", ans[i]);
return 0;
}
点击展开/折叠 源代码(矩阵求逆)
/*Lucky_Glass*/
#include <cstdio>
#include <cstring>
#include <algorithm> const int N = 5e5 + 10, MOD = 1e9 + 7;
#define con(typ) const typ & inline int add(int a, con(int) b) {return (a += b) >= MOD ? a - MOD : a;}
inline int sub(int a, con(int) b) {return (a -= b) < 0 ? a + MOD : a;}
inline int mul(con(int) a, con(int) b) {return int(1ll * a * b % MOD);} inline int rin(int &r) {
int c = getchar(); r = 0;
while ( c < '0' || '9' < c ) c = getchar();
while ( '0' <= c && c <= '9' ) r = (r * 10) + (c ^ '0'), c = getchar();
return r;
}
int n, totad, m;
char str[N];
int ad[11], neg[N][11], psi[N][11], cur2[11][11], cur[11][11], totcur[11]; int main() {
scanf("%s", str + 1);
n = int(strlen(str + 1));
for (int i = 0; i < 11; ++i) {
for (int j = 0; j < 11; ++j)
cur2[i][j] = cur[i][j] = i == j;
totcur[i] = 1;
}
for (int i = 1; i <= n; ++i) {
int c = str[i] - 'a';
for (int j = 0; j < 11; ++j) {
int tmp2 = ad[j];
ad[j] = add(ad[j], add(cur[c][j], ad[j]));
cur[c][j] = sub(0, tmp2);
totcur[j] = sub(totcur[j], cur2[j][c]);
cur2[j][c] = sub(cur2[j][c], totcur[j]);
totcur[j] = add(totcur[j], cur2[j][c]);
}
for (int j = 0; j < 11; ++j) {
psi[i][j] = add(cur[10][j], ad[j]);
neg[i][j] = sub(totcur[j], cur2[j][10]);
}
}
for (int j = 0; j < 10; ++j)
neg[0][j] = 1;
rin(m);
while ( m-- ) {
int le, ri; rin(le), rin(ri);
--le;
int ans = 0;
for (int i = 0; i < 11; ++i)
ans = add(ans, mul(psi[ri][i], neg[le][i]));
printf("%d\n", ans);
}
return 0;
}

THE END

Thanks for reading!

告别土壤,白桦再难生长。
植根星海,又能否重获青苍?
每夜放逐信仰,四光年之外去流浪——
纵躯壳埋葬,灵魂自由释放。

——《岁月成碑》By 乐正绫 / Days

> Link 岁月成碑 - 网易云

「SOL」序列计数sequence (模拟赛)的更多相关文章

  1. AC日记——「SDOI2017」序列计数 LibreOJ 2002

    「SDOI2017」序列计数 思路: 矩阵快速幂: 代码: #include <bits/stdc++.h> using namespace std; #define mod 201704 ...

  2. loj#2002. 「SDOI2017」序列计数(dp 矩阵乘法)

    题意 题目链接 Sol 质数的限制并没有什么卵用,直接容斥一下:答案 = 忽略质数总的方案 - 没有质数的方案 那么直接dp,设\(f[i][j]\)表示到第i个位置,当前和为j的方案数 \(f[i ...

  3. loj2002 「SDOI2017」序列计数

    水题 #include <iostream> #include <cstring> #include <cstdio> using namespace std; t ...

  4. 「题解」:07.18NOIP模拟赛T1:星际旅行

    问题 A: 星际旅行 时间限制: 1 Sec  内存限制: 256 MB 题面 题面谢绝公开. 考试心路历程 拿到这道题感觉很懵逼,所以先搞的T2和T3,最后码了个暴力,结果还不如直接输出‘0’得分高 ...

  5. Loj #3059. 「HNOI2019」序列

    Loj #3059. 「HNOI2019」序列 给定一个长度为 \(n\) 的序列 \(A_1, \ldots , A_n\),以及 \(m\) 个操作,每个操作将一个 \(A_i\) 修改为 \(k ...

  6. 「HNOI2016」序列 解题报告

    「HNOI2016」序列 有一些高妙的做法,懒得看 考虑莫队,考虑莫队咋移动区间 然后你在区间内部找一个最小值的位置,假设现在从右边加 最小值左边区间显然可以\(O(1)\),最小值右边的区间是断掉的 ...

  7. loj #2051. 「HNOI2016」序列

    #2051. 「HNOI2016」序列 题目描述 给定长度为 n nn 的序列:a1,a2,⋯,an a_1, a_2, \cdots , a_na​1​​,a​2​​,⋯,a​n​​,记为 a[1: ...

  8. 「JSOI2014」序列维护

    「JSOI2014」序列维护 传送门 其实这题就是luogu的模板线段树2,之所以要发题解就是因为学到了一种比较NB的 \(\text{update}\) 的方式.(参见这题) 我们可以把修改操作统一 ...

  9. 「BZOJ2839」集合计数

    「BZOJ2839」集合计数 题目大意: 一个包含 \(n\) 个数的集合有 \(2^n\) 个子集,从这些子集中取出若干个集合(至少一个),使他们的交集的元素个数恰好为 \(k\),求方案数,答案对 ...

  10. 「Vijos 1284」「OIBH杯NOIP2006第二次模拟赛」佳佳的魔法阵

    佳佳的魔法阵 背景 也许是为了捕捉猎物(捕捉MM?),也许是因为其它原因,总之,佳佳准备设计一个魔法阵.而设计魔法阵涉及到的最关键问题,似乎就是那些带有魔力的宝石的摆放-- 描述 魔法阵是一个\(n ...

随机推荐

  1. vulnhub靶场渗透实战15-matrix-breakout-2-morpheus

    vulnhub靶场渗透实战15-matrix-breakout-2-morpheus 靶机搭建:vulnhub上是说vbox里更合适.可能有vbox版本兼容问题,我用的vmware导入. 靶场下载地址 ...

  2. 12月19日内容总结——Q查询进阶、ORM查询优化、ORM事务、ORM常用字段类型和字段参数、Ajax介绍、数据编码格式、Ajax携带文件数据

    目录 一.Q查询进阶操作 二.ORM查询优化 三.ORM事务操作 四.ORM常用字段类型 五.ORM常用字段参数 六.Ajax AJAX简介 应用场景 AJAX的优点 语法实现 七.数据编码格式(Co ...

  3. Python内置对象(一)

    Python内置对象(一) 分多次讲解 这部分相对比较简单,偶尔一些特殊的做法会强调下(前面加★) 总览 builtins = [_ for _ in dir(__builtins__) if not ...

  4. fiddler的简单使用

    一.fiddler接口测试介绍 二.fiddler过滤器的使用 fiddler可以指定只抓哪些包,通过filters实现 如果需要抓取多个网站,各个需要抓取的网站之间用分号隔开 三.fiddler抓取 ...

  5. 编译报错,提示:This dependency was not found:* vue-editor-bridge

    前端代码引入了: 1 import func from 'vue-editor-bridge'; 工具自动填充,导致引入上述JS去掉重新编译,问题解决

  6. 学习Java Day7

    今天学习了if语句和C/C++基本无差异,也再次练了一下Java的输入

  7. go语言面试

    go面试 基础 = 和 := 的区别? **Go语言中,= 操作符用于赋值,而 := 操作符可以用于声明及赋值. Go 语言支持短变量声明(针对局部变量),以 := 为标志,这里要注意的是,Go 语言 ...

  8. C语言小程序-天天酷跑类游戏 26号完成

    1.创建游戏内背景 2.实现人物 奔跑 跳跃 优化帧 3.障碍物 出现 https://kxd.lanzoul.com/iKXU20elmtah 开源地址 半成品

  9. 益赛普等TNFi持续治疗强直性脊柱炎的长期疗效观察(≥3年)

    北大深圳医院风湿免疫科在2021年发表了益赛普等TNFi持续治疗强直性脊柱炎的长期(≥3年)疗效观察[1]. 入排条件严苛,坚持随访不容易 观察对象是2009-2019年间就诊于该科室的AS患者,需有 ...

  10. 基于Python的OpenGL 05 之坐标系统

    1. 引言 本文基于Python语言,描述OpenGL的坐标系统 前置知识可参考: 基于Python的OpenGL 04 之变换 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com) 笔者 ...