字符串
liujiaxi123456 · · 个人记录
前言:
符号与标识:
*:表示有总结性的知识点。
\@:表示总体思路/算法。
!:表示重要的东西/切入点
#: 已复习的题目
知识点:
基本表示&定义:
-
-
子串:
S[i..j] i 到 j 的子串。 -
前缀:
Pre(S, i) = S[1..i] 。 -
后缀:
Suf(S, i) = S[i..|S|] 。 -
最长公共前缀/后缀
LCP(S, T)/LCF(S, T)
周期:
-
周期:对字符串 S 和
p\in(0, |S|] ,若s_i = s_{i+p} 对所有i\in[1, |S|-p] 成立,则称 p 是 S 的周期。-
循环节:
p||S| 的周期。 -
记
Per(S) 为 S 的最小周期,有时也表示 S 的周期集合。 -
整串周期所构成的集合包含于任意子串的周期的集合。
-
若 T 为 S 的前缀,S 有周期 a,T 有周期 b,且
b|a, |S| > a ,则 S 也有周期 b。
-
-
周期/ Border 与 LCP: 对于拥有周期 p 的串 S,
LCP(S[1..n-p], S[p..n]) = n-p -
弱周期引理 Weak Period Lemma: p 和 q 是串 S 的周期,若
p+q\le |S| ,则\gcd(p, q) 也是 S 周期。 -
强周期引理 Period Lemma: p 和 q 是串 S 的周期,若
p+q\le |S|+\gcd(p, q) ,则\gcd(p, q) 也是 S 周期。 -
循环节存在性的判定: 对于 S,存在循环节当且仅当其最小周期为循环节,即
Per(S)||S|
Border:
-
Border:对字符串 S 和
p\in[0, |S|) ,若 S 长度为 p 的前缀与长度为 p 的后缀相同,则称 S 长度为 p 的前缀是 S 的一个 border-
通常而言,S 的 border 不仅表示字符串,也可以表示 p 或 border 集合,视语境而定。
-
我自己一般通过大小写区分,大小是字符串,小写是长度。
-
-
周期与 Border 的对偶性: S 有长度为 p 的 border 当且仅当
|S|-p 是 S 的周期。 -
传递性: border 的 border 还是 border
-
推论 1:若一个 border 在串 S 中的 所有出现位置(不仅仅前后缀) 出现重叠,则重叠部分为 S 一个更小的 border。
-
推论 2:令 P 为 S 的最大 border,则
Border(S) = Border(P)\cup \left\{P\right\}
-
-
Border 与等差数列的关系:
-
由于 Border 可以有
|S| 减去周期得到,而周期往往成倍数,所有与等差数列有很大的关系。 -
字符串 S 中所有大于等于
\left \lceil \dfrac {|S|}2 \right \rceil 的 border 长度组成一个等差数列。 -
还有一些,懒得记了,到时候看 lyx 的笔记吧。
-
前缀函数 Partical Match Table
-
前缀函数:
\pi_i 表示前缀Pre(S, i) 的最长 Border 长度。-
\pi_i = \max_{k=1}^i {k|(S[1..k] = S[i-k+1..i])} -
示例代码:
-
for(int i=2; i<=M; i++) {
for(pi[i]=pi[i-1]; pi[i] and a[i]!=a[pi[i]+1]; pi[i]=pi[pi[i]]);
if(a[i] == a[pi[i]+1]) pi[i]++;
}
Border Tree/失配树
将
-
点 x 的子树大小 =
Pre(S, x) 的 border 个数 -
Manacher
用于预处理
原理在于维护当前
当
P.S. 为了方便,通常在每个字符中间插入特殊字符 '#' 从而使回文串的长度为奇数。
示例代码:
for(int i=1, l=0, r=-1; i<=N; i++) {
int k = (i>r ?1 :min(d[l+r-i], r-i+1));
for(; 1<=i-k and i+k<=N and s[i-k]==s[i+k]; k++);
d[i] = k;
if(i+k-1 > r) l = i-k+1, r = i+k-1;
}
-
本质不同回文子串数量:
O(|S|) -
在 Manacher 的 r++ 时用 hash 去重即可。
-
例题:BSOJ5485 -- 回文子串计数
-
-
最长回文子序列 = 正反串最长公共子序列
-
只是存在一个,不一定所有。
-
例题:*AGC021D -- Reversed LCS
-
Z 函数/扩展 KMP:
用于预处理
优化原理跟 Manacher 差不多,同样维护
当
再这个基础上继续用朴素方法扩展即可。
z[1] = N;
for(int i=2, l=0, r=0; i<=N; i++) {
if(i <= r and z[i-l+1]<r-i+1) {
z[i] = z[i-l+1];
} else {
z[i] = max(0, r-i+1);
for(; i+z[i]<=N and s[z[i]+1]==s[i+z[i]]; z[i]++);
}
if(i+z[i]-1 > r) l = i, r = i+z[i]-1;
// printf("z[%d]:%d, l:%d, r:%d\n", i, z[i], l, r);
}
后缀数组 SA:
Lyndon 分解:
-
Lyndon: 满足 S 的最小后缀是 S 本身的串。
-
引理:Lyndon 串不存在 border
-
引理:X,Y 均为 Lyndon 串,且 X<Y,则 XY 为 Lyndon 串。
-
引理:Lyndon 串 S 任意划分成两个非空串 S = S1S2,均满足 S1<S2
-
-
Lyndon 分解: 将 S 划分为字典序非增的 Lyndon 串 S1, S2, ... Sk
-
任意串 S 的 Lyndon 分解存在且唯一。
-
Duval 算法:
-
维护三元组 (i, j, k)。
-
AC 自动机:
以 Trie 的结构为基础,结合 KMP 的思想建立的自动机,接受且仅接受以某一个模式串作为后缀的字符串。
失配指针:
状态 u 的 fail 指针指向另一个状态 v,其中
构建:
-
建出 trie 树。
-
按照 BFS 的顺序遍历 trie。
-
对于每个点 u,令 p 表示父亲结点,且
trie(p, c) = u (表示 u 由 p 加上字符 c 的边得到)。-
若
trie(fail_p, c) 存在,则fail_u = trie(fail_p, c) 。 -
否则,往上爬 fail 链,
fail_u = trie(fail_{p 的某个 fail 祖先}, c) 。
-
AC 自动机:
令
求解方法显然可以通过跳 fail 链得到。
Query
对于文本串 s,状态
而显然,对于 u 的 fail 链上祖先,都是
所以答案是个类似树链加的东西。
Suffix Tree 后缀树:
Suffix Automata 后缀自动机:
基本概念&性质:
-
SAM 是一个接受且仅接受一个字符串的后缀的自动机。
-
SAM 通过路径表示一个后缀。
-
SAM 上的一个节点 u 代表一个
endpos 集合:-
即【所有从根到 u 的路径形成的字符串】在整串中的【出现末尾位置集合】相同。
-
这些【所有从根到 u 的路径形成的字符串】互为后缀。
-
另外,这些【所有从根到 u 的路径形成的字符串】的长度为连续区间。
-
记为:
minlen(u) 与maxlen(u) 。 -
-
-
SAM 上的一个节点 u 的 fail 表示【所有从根到 u 的路径形成的字符串】中【
endpos 集合不同于 u】的【最长真后缀】。- 满足:
len(fail_u)+1 = minlen(u) 。、
- 满足:
构建:
-
SAM 采用增量构建法。
-
对于一个当前添加的字符 c,记上一次的点为 last。
-
先新建点
u ,使len_u = len_{last}+1 ,并使p=last, last = u 。 -
从 p 向上跳 fail 链,保证 p 均没有
son_c ,让 p 向 u 连边。 -
若停下时
p = 0 ,则fail_u = 1 ,然后就没事了。 -
否则,令
q = son_{p, c} 。-
若
len_p +1 = len_q ,直接令fail_u = q 即可。 -
否则:
-
分裂 q,令
len_{neww} = len_p+1 。 -
将 q 的 son 复制给 neww。
-
修改 fail 链:
fail_{neww} = fail_q, fail_q = fail_u = neww 。 -
然后 p 一直往上跳,直到
son_{p, c}\not = q ,对于这些 p,son_{p, c} = neww 。
-
-
查询:
-
通常而言,在一开始新建 u 时,会在 u 上存一点信息。
-
然后建完了做 topo,fail 链收集信息。
- 可以用 len 做基数排序优化常数。
回文自动机 PAM:
基本概念:
-
只存储串中回文子串的自动机。
-
由于本质不同的回文子串只有 n 个,所以每个节点代表一个回文子串。
-
由于回文串的 border 还是回文串,所以 border 的性质 PAM 都有。
结构:
-
由于回文串有奇偶之分,所以 PAM 实际有奇树、偶树两棵。
-
奇树根的 len 为 -1,偶树根的 len 为 0。
-
节点每深度 +1,len + 2。
-
-
每个节点的
u 的 trans 边trans(u, c) 表示在 u 两边个加一个 c 字符的回文串。 -
fail 表示最长回文后缀对应的点。
构建:
-
偶跟的 fail 指向奇根。
-
增量构建。
-
以【上一个字符结尾】的【最长回文子串对应的节点】开始,不断沿着 fail 指针走,直到找到一个节点满足
s_{p}=s_{p-len-1} 。 -
即满足【【此节点所对应回文子串】的上一个字符】与待添加字符相同。
-
此时我们令 u 为
trans(p, c) 。- 若没有,需新建一个。
-
fail 同理,不断的跳 fail,直到。
应用:
-
求回文串出现次数:
-
借助 fail 构成的树形结构,出现次数即为子树 size。
-
直接倒叙枚举即可保证 topo 序。
-
例题:
Border
BSOJ4196 -- 【NOI2014】动物园/P2375 -- [NOI2014] 动物园
思路 1:
- border 树秒了
思路 2:
-
在预处理 PMT 的时候处理出
Pre(S, i) 的 border 数量。 -
求解时直接处理一下即可。
*BSOJ4616 -- 【POI2005】Sza-Template/P3426 -- [POI 2005] SZA-Template
思路 1:
-
枚举模板,快速验证可行性。
-
貌似在 border 树上可以推出性质(但我没有),参考此题解
思路 2:
-
直接 DP
-
设
f_i 表示填满前缀 i 所需的答案。 -
发现
f_i 只有两种答案:i, f_{fail_i} -
而显然,我们只需要判定什么时候能从
f_{fail_i} 转移即可。 -
引理 1:
-
对于
fail_i \ge i ,f_{fail_i} 一定能填满Pre(S, i) -
大概就是两个相交的 border 中间部分还是 border
-
-
推论 1:
S[i-fail_i+1, i] 一定能被填上 -
推论 2:充要条件为存在一个
j ,使得f_j=f_{fail_i}, i-fail_i \le j -
用桶实现即可。
Manacher
*AGC021D -- Reversed LCS
! * 如何想到区间 DP 的思路很重要
首先有一个结论:S 和 S 的反串的最长公共子序列 = S 的最长回文子序列。
-
一开始直接把这玩意儿当结论用了,后面才发现还需要证明。
-
证明也还好,就把
LCS\le LPS 和LPS\le LCS 个证一遍就得到相等了。 -
不想证的话感性理解一下也是很容易的。
于是考虑求解最长回文子序列:
-
可以改 k 个,大概率都需要记下来 DP 处理。
-
由于回文是两边扩展的,在线性递推里几乎没有任何性质,所以考虑区间 DP。
-
于是设
f_{l, r, k} 表示处理完 [l, r] 的区间,改了 k 个值时的最长回文子序列。
考虑转移:
-
就两种可能:
(l, r) 凑一对/内部继承。 -
然后分讨一下
[s_l=s_r] 即可。
*HDU5371 -- Hotaru's problem
定义了一个序列它分为三个部分,最左端和最右端是相同的,左端和中间的是回文。求这样的序列最长是多少。
首先,“最左端和最右端是相同的,左端和中间的是回文”,说明中间和右端又是回文。
回文的题,现在学过的唯一专门算法只有 Manacher,考虑先把 d 数组求出来。
此时有一个思路:
- 对于每个 i,二分回文半径,再 hash 判断左右是否相等。
这个思路本质上用了“左中、左右”的条件,考虑能否使用“左中、中右”条件。
-
即最大的
3x 满足:\exist i\in[1, n-x] 满足d_{i}\ge x \wedge d_{i+x}\ge x -
把 d 从大到小排序,然后处理一下即可。
BSOJ5485 -- 回文子串计数
板子。
SA:
*BSOJ4472 -- [NOI2015] 品酒大会/P2178 -- [NOI2015] 品酒大会
抽象出题目即为:
先考虑第一问求解:
-
先用 SA 求得 h 数组后,问题转化成一个区间的 h 的 min 大于等于 r。
-
具体的,可以通过笛卡尔树解决。
再考虑第二问:
-
发现在笛卡尔树上维护一个子树最大值即可。
-
由于可能负负得正,所以最小值也要维护。
P.S. 单调栈显然也是可以的。
注意细节,仔细分析。
*BSOJ1628 -- 识别子串
先写式子:
-
对于每个 i 求
\min\{ r-l+1 | i\in[l, r] \wedge S[l..r] 在 S 中仅出现一次 \} 。 -
S[l..r] 在 S 中仅出现一次 \Longleftrightarrow \max\{LCP(i\not=l, l) \}< r-l+1 -
然后把 LCP 用 h 表示出来,得到
\max\{\min\{h[i..rk[l]]\}|i\in[2, n] \}<r-l+1 。 -
显然 i 越接近 l 越优,所以等价于
\max(h[rk[l], rk[l]+1]) < r-l+1 。
考虑如何入手这个式子:
-
枚举 len 显然不好优化,于是优先考虑枚举 l。
-
可以得到
ans_i = \min\{\max(\max(h[rk[l]], h[rk[l]+1])+1, i-l+1)|l\in[1, i] \} -
注意这里当 min 里面那一坨(设为 len)
l+len-1>n 时,不能用来转移 ans。
考虑快速求这个东西:
-
为了方便,先令
a_i = \max(h[rk[l]], h[rk[l]+1])+1 。-
对于某些
l+len-1>n 的 l,由于l+(i-l+1)-1\le n ,所以只可能是l+a_l-1>n 。 -
因此,把这些
a_i 置为 n 即可。
-
-
由于 i 是递增的,所以一旦
a_l \le i-l+1 那么a_l 就一定不会用到了。 -
于是我们考虑用线段树维护前缀
min_a ,每次把不会用到的a_l 从线段树中踢出去单独处理即可。
BSOJ3410 -- 字符串识别
上面那道题的双倍经验。
*BSOJ3656 -- 【SCOI2012】喵星球上的点名/P2336 [SCOI2012] 喵星球上的点名
首先,有名和姓两个串比较麻烦,先考虑把他们用特殊字符链接得到
然后问题转化成:
-
对于每个
Q_j ,求S_i 是Q_j 子串的 i 数量。 -
对于每个
S_i ,求S_i 是Q_j 子串的 j 数量。
多串对多串,显然做不了,于是考虑把所有串拼在一起。
先考虑对每个
-
可以推出一个 sa 上的范围
[l, r] 使得范围内的 j 都要统计。 -
统计这些 j 的
id[sa[j]] 的种类数即可。
考虑统计
-
发现不能像上述那样预处理答案。
-
于是在上述过程中运用莫队即可。
*BSOJ3807 -- 【HAOI2016】找相同字符/P3181 [HAOI2016] 找相同字符
先写式子:
显然考虑用 h 展开后面的判别式:
容易发现可以进一步转化:
转化成 h 的 min 过后显然可以通过单调栈/笛卡尔树解决。
* 这里讲一下笛卡尔树的注意事项:
- 统计答案应该是:(左子树 + (最左的 l-1 的点)) * (右子树 + 自己)。
*BSOJ5809 -- 【NOI2016】优秀的拆分/P1117 [NOI2016] 优秀的拆分
\@ 需要用到 SA 维护关键点的技巧。
显然先写式子,可以得到:
显然这样后面 len2 不好做,稍微改一下,改为 i 枚举中间点。
所以需要维护
现在两种思路:
-
枚举 i 求 len。
-
枚举 len 更新 i。
其中第二种可做。
技巧: 对于这种涉及到枚举 len 的,考虑往调和级数方面想,于是我们考虑每 len 维护一个关键点。
-
对于当前的 len,显然一个合法的区间至少包住两个关键点。
-
考虑统计相邻两个关键点的贡献。
-
*! 我们现在只枚举了相邻两个关键点,肯定只能从这两个关键点的信息入手。
- 显然性质最好的就是这两个关键点的 LCP 与 LCS。
-
把这两点的 LCP 和 LCS 求出后,可以发现能更新的 f/g 是一段区间内的值。
-
直接差分即可。
自动机
*CF1090J -- Two Prefixes
做题思路 1:
先考虑大体思路:
-
定一求一
-
枚举 s/t 后,统计【当前枚举的前缀】能形成的【新的/重复】的串数量。
-
把上述条件的等价条件推出,然后想办法快速计数。
定一求一显然有两种方向,先考虑枚举 s 的前缀 i:
-
大概率看着统计新形成的重复串数量就要好做一点。
-
假设当前枚举到
s[1..i] ,t 的前缀为t[1..j] ,造成重复的前缀 s' 为s[1..k] ,t' 自然为t[1..i+j-k] 。 -
- $s[k+1..i] = t[1..i-k] -
t[1..j] = t[i-k+1..i+j-k]
-
-
考虑如何计数:
-
发现两个条件都类似与 border 的形式,考虑往 kmp,fail 树方面靠。
-
发现本质上就是三元点对计数,所以有多种(包括
i+j-k 等)可以考虑先枚举。 -
优先处理第二个条件,因为它只跟 t 本身有关。
-
于是,我们现在的思路转变为枚举
i+j-k 。
-
考虑枚举 t 的前缀
- 但似乎并不好处理第一个条件。
我们发现若先处理第二个条件的思路是不应该的,应该考虑先处理难的条件,再快速处理简单的。
-
两个枚举思路:
-
枚举
s[1..i] 。 -
枚举
t[1..i-k] 。
-
-
考虑枚举 s:
- 想了想似乎没有思路。
-
考虑枚举 t:
-
令
l = i-k 。 -
发现合法的 j 就是 LCP,第二个条件就处理完了。
-
至于第一个条件,就是用 t 的这段前缀去匹配 s。
-
即
LCP \ge l ,把 s,t 拼接后用 SA 统计即可。
-
发现不对,最上面的
CF1186C -- Vus the Cossack and Strings
P.S. 这是一道我打错题号的题,可能跟自动机无关。
做题思路 1:
先考虑枚举 a 的为 b 长度的子串,看能否快速 check:
- 首先,发现由于是 0/1 序列,
*CF1168C -- And Reachability
做题思路 1:
抽象一下题意就是判是否存在 q 个区间里的子序列(头尾必选),满足相邻两项的“与”不为 0。
显然按位考虑,每个点对每一位向最近的点连边。
然后就是判两点连通性。
预处理每个点到第 i 位为 1 的最近的点即可。
查询时枚举 r 的每一位即可。
AC 自动机:
BSOJ5672 -- GRE Words Revenge/UVA1676 背单词 GRE Words Revenge
二进制分组 + AC-AM 即可。
但是我调了 3h。。。
自动机上 DP:
*CF1303E -- Erase Subsequences
显然对 s 建出自动机。
维护保证不相交,显然在枚举 s1/s2 长度后,同时进行匹配。
然后 DP 一下即可,并不困难。
*CF1383E -- Strange Operation
先考虑构造出一个仅接受合法的 01 序列的自动机:
-
为方便,令
(u, i) 表示一个时刻的匹配情况,其中 u 表示匹配到 u 节点,i 表示匹配到第 i 位。 -
s 表示原串,t 表示一个待检测的 01 序列。
-
分讨一下,假设
s_u = 1 :-
若
t_i = 0 :转移到下一个 0 即可。 -
若
t_i = 1 :转移到下一个 1 即可。
-
-
若
s_u = 0 :-
若
t_i = 0 : -
若
s_{u+1} = 0 :转移到 u+1 即可。 -
若
s_{u+1} = 1 :-
发现 1 无法删掉。
-
只能通过放弃
s_u 前的一段 0,转移到后面有一段足够长的连续 0 的地方。
-
-
若
t_i = 1 :转移到下一个 1 即可。
-
-
发现关键在于
s_u = 0, t_i = 0, s_{u+1}= 1 时不好建边:-
因为我们没有记录 0 的长度。
-
所以考虑转化一下模型,让长度有关。
-
具体的,把 01 序列改成记录两个 1 之间的 0 的数量,这个显然一一对应。
-
考虑在这个自动机上 DP:
-
直接设
f_u 表示匹配到自动机上 u 节点的 01 序列数量。 -
转移时枚举 0 的长度,并匹配到相应位置
v, f_{u} 贡献给f_{v} ,但这样显然会超时。 -
正难则反,考虑
f_v 收集f_u 的贡献。-
令
p_u 表示节点 u 与上一个 1 之间的 0 的个数。 -
显然一个 u 加上 k 个 0 能转移到 v,当且仅当:
\max_{i=u+1}^{v-1}p_i< k 。 -
所以
f_v = \sum_{u=1}^{v-1} f_u\cdot \max(0, p_v-\max_{i=u+1}^{v-1}p_i) 。
-
-
考虑如何处理这个式子:
-
显然往单调栈方向思考。
-
发现暴力遍历有势能,于是再加个前缀和即可。
-
P.S. 前后的 0 似乎要单独处理一下?
*CF1575H -- Holiday Wall Ornaments
做题思路 1:
没啥思路,先考虑个二分:
-
现在相等于要判断
-
然后贪心/ DP 似乎都不行?
做题思路 2:经过题解点拨后考虑先建出 KMP 自动机。
直接考虑 DP:
-
记录
f_{u, k, i} 表示匹配到节点 u,出现次数为 k,匹配到s_i ,时最小的变换次数。- 相等于此时
Pre(t, u) 是Pre(s, i) 的后缀。
- 相等于此时
-
决策是否动用变换次数改即可。
SP10502 VIDEO - Video game combos
做题思路 1:
多模式串匹配次数最值问题,优先考虑建出 AC 自动机:
-
显然考虑在自动机上 DP。
-
似乎直接记录
f_{u, i} 表示匹配到 u 节点,匹配到s_i 的最大得分即可? -
转移枚举下一位是什么,然后求 fail 链贡献即可。
P4052 [JSOI2007] 文本生成器/1162 -- 文本生成器
跟上一题似乎很像,只用多记录个 0/1 即可。
为了方便,可以转化成总数减去不合法的。
OJ 和 SPOJ 由于数据范围不同,需要写矩阵。
2428 -- 【HNOI2008】GT考试/P3193 [HNOI2008] GT考试
显然先建出 KMP 自动机。
然后可以列出较为显然的 DP 方程。
显然使用矩阵优化。
BSOJ2241 -- 【BZOJ3864】英雄遇见魔鬼Hero meet devil
做题思路 1:
先概括一下题意:
-
给你一个字符集大小为 4 的字符串
s(s\le15) 。 -
求长度为
m(m\le 1000) 的串 t,LCS(s, t) = i 的 t 的数量。
SAM
*CF235C -- Cyclical Quest
处理循环问题,显然考虑倍长后做类似双指针的东西。
相当于在 SAM 上做匹配,假设现在匹配到了:
-
若
len > |t| 则尝试 fail,相当于把左端点删掉。 -
否则统计答案,记得打标记以去重。
-
若 son 为空,则一直跳 fail 直到有。
P.S. 由于跳一次 fail,必然让 len--,所以有势能。
*P4112 [HEOI2015] 最短不公共子串
四种情况,分开求解:
-
子串-子串:
-
显然具有单调性,考虑先二分一个 len。
-
check hash 判一下即可。
-
-
子串-子序列:
-
仍然有单调性。
-
二分后仍然可以快速判断。
-
-
子序列-子串:
-
-
答案即为:
\min f_{u, 0/1} (按写法而定)。
-
-
子序列-子序列:
-
-
答案即为:
\min f_{u, m+1} 。
-
P.S. 不需要实际 DP,可以直接 bfs。
P.S. 上面两种方法也可以用自动机解决。
PAM:
BSOJ4339 -- 【ural2041】Palindromes and Super Abilities 2
跟板子差不多,只要有新建节点就是 1,否则是 0.
BSOJ4200 -- 【APIO2014】回文串/P3649 [APIO2014] 回文串
统计回文子串出现次数板子。
BSOJ2118 -- 【HDU6599】我喜欢回文串I Love Palindrome String
有一个限制条件,稍微处理一下即可。
BSOJ4342 -- 【BZOJ2160】拉拉队排练/P1659 [国家集训队] 拉拉队排练
直接按 bfs 序倒叙排序即可。
*BSOJ2124 -- /CF17E -- Palisection
做题思路 1:
发现直接在 PAM 上做,由于缺乏下标上的信息,比较麻烦。
于是考虑用 PAM 求出一些信息,最后还是放到序列上来做。
注意到 PAM 能求出的,跟下标有关的,大概都是形如:以
发现直接求相交很困难,考虑正难则反,转成求不相交的对数。
很显然,【前
但是如果直接这么统计,显然会重。
于是考虑用【前
显然不重不漏。
P.S. CF 卡空间,要写链表 PAM。。。
*BSOJ4351 -- 【BZOJ4044】病毒的合成Virus synthesis/P4762 [CERC2014] Virus synthesis
做题思路 1:
首先,我们发现只要有回文串,就用 2 操作一定更优。
所以我们可以发现,答案一定是:一个回文串 +
于是我们令
-
得到这个回文串,显然应通过尽可能的使用 2 操作。
-
于是应该考虑在 fail 链上找到 len 最大的 v,满足
2len_v\le len_u 。 -
使得
f_u = f_v+(len_u/2-len_v)+1 。 -
f 的初值应为:
f_{fa}+1 。-
注意这里的 fa 不是 fail。
-
表示在上次翻折之前再加上一个字符。
-
注意这样需令
f_0 = 1 。
-
然后就只需要维护 len 一半的 v 了:
-
可以直接 dfs 后双指针。
-
也可以在线做。
注意,只应在偶回文串上转移。
/*
所以我们可以列出 DP 方程:
-
设
f_i 表示生成前 i 的答案。 -
则
f_i 有两种转移:-
f_i = f_{i-1} + 1 -
-
-
然后通过回文 border 的 log 划分性质维护即可。
*/
BSOJ2309 -- 【BZOJ2565】最长双回文串
显然考虑枚举中间点。
然后就很显然了。
*CF932G -- Palindrome Partition
做题思路 1:
首先显然考虑 dp,由于是个类似回文的形式,所以应该考虑从中间点开始向两边拓展。
令
设
显然有转移:
-
f_i = \sum_{j=0}^{i-1} f_j\cdot [s[mid-i+1..mid-j] == s[mid+j+1..mid+i]]
考虑优化:
-
我们考虑能否用数据结构预处理出所有合法的 j 的答案。
-
先推一下合法的 j 满足的性质:
-
发现这些 j 和 i 构成的串一定互为 border。
-
但是我们似乎连 i 的最大 border 都求不出来。
-
-
我们注意到问题的关键在于前后两个串的关联太薄弱了,需要更好的连接方式以获得更好的性质。
-
考虑如何把这两个串拼成一个串:
-
由于我们对于每个 i 都要维护信息,所以这两个串拼在一起后应该最好同增同减。
-
于是我们考虑将前串反转后每个 i 一一对应的插在一起。
-
即形如:
t = s_{mid}s_{mid+1}s_{mid-1}s_{mid+2}...s_{1}s_n 。
-
-
研究一下此时的判断条件变成了什么:
-
f_i = \sum_{j=0}^{i-1} f_j\cdot [t[2j+1..2i] 为回文串]
-
-
于是算法很自然的就出来了:
- 用 PAM 维护出
2i 前的最长回文子串,收集fail 链答案。
- 用 PAM 维护出
-
考虑如何快速收集答案:
-
显然再没有性质的情况下无法更进一步优化,肯定需要挖掘性质:
-
要么 f 的值具有性质,可以提出通项公式之类的。
-
要么利用 PAM 回文串形成 log 个等差数列的性质,尝试对每个等差数列快速计算贡献。
-
稍微分析一下就能发现 f 似乎没啥性质,考虑等差数列。
-
通过画图可以发现现在的 i 和之前的只多一个
f[i-len[up[u]]-delta[u]] 。- P.S. 可见 这篇博客。
-
于是就能直接维护了。
-
*BSOJ6216 -- 【5.22模拟 BZOJ5384】有趣的字符串题
求区间本质不同回文子串个数。
做题思路 1:
基本思路:
-
将询问离线排序,在 PAM 一个个加入字符时处理答案。
-
通过变化量求解。
考虑 PAM 上每个节点的有效区间:
-
对于一个节点 u,只有当 l 小于等于 u 的 fail 子树内的最大时间戳时,u 才是有用的。
-
因此,查询直接查所有点的最大时间戳即可。
-
修改使用重剖 + ODT 即可。
错误的,应维护左端点,而非右端点。
做题思路 2:
回文划分板子。
*CF906E -- Reverses
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.
To be continued.