字符串总结
manacher
充分利用了之前计算过的数组,翻转回文串还是回文串的性质。
能拓展的东西不多,最多搞个二维的(paper-cutting),或者换一下匹配的模式(antisymmetry)。一般的题都把它作为中间步骤。
例题:Palindrome Construction
看似是反向 manacher,但只需像此题一样,用并查集st表维护绑定关系即可。
如果是求满足另外某些条件的回文串,可以哈希加 manacher,比如此题。
KMP
跳 fail(或者叫 border),然后扩展。
本体没什么好说的。但问题是它求出的 border 有一堆性质。
- border 构成一棵树,所以可以做许多树上操作,比如 k 级祖先,最近公共祖先,还可以 DP。如果字符串有动态权值,甚至可以树链剖分。
 例题:动物园 失配树 Prefixes and Suffixes
- \(circ=n-border[n]\)
 例题:无线传输 Periods of Words
- 一个字符串的所有 border 可分成 \(\mathcal O(\log n)\) 个等差数列。
- 周期定理:若 \(p\) 和 \(q\) 均是 \(s\) 的周期,且 \(p+q-\gcd(p,q)\le |s|\),则 \(\gcd(p,q)\) 也是 \(s\) 的周期。
Z 函数
感觉 Z 函数和 manacher 的基本思想很类似,都是找了一个有最多信息的区间来转移。
对于例题,既可以写两个函数,也可以将两个字符串拼在一起,然后一个函数解决。
所以它为什么也叫扩展 KMP 呢?
B函数
之前的对于自动机的总结见此处。之前写过的这里就写少一点。
回文自动机
是一个比较新的算法。基本思想是构建两棵半回文树,同时记录节点到根的距离。
跳 fail:看能否扩展,如果不能就继续跳。
建立 fail 指针时从上一个节点跳到的节点再跳一次即可。
int jump(int now){
	while(c[ri-p[now].len-1]!=c[ri])
		now=p[now].fail;
	return now;
}
后缀排序
倍增加基数排序

oiwiki 上的好图片。
基本思路是分别对长度为 \(1,2,4\) 等等的字串进行排序。
哦,应该直接写后缀自动机的。(no)
sa-is 线性求法
论文中说“一百行左右可以码完”。
首先,为了方便,可以在字符串的末尾加上 \0,并认为它的字典序最小。
我们提取出所有 LMS 子串(\(S\) 的左边必须是 \(L\)),把他们进行排序。
如果子串互不相等,则直接桶排序,否则递归处理。
最后倒推出所有串的关系。
由于 LMS 子串的长度最多有 \(\left\lceil\frac{n}{2}\right\rceil\) 个,则递归的话时间复杂度可以保证线性,但前提是 LMS 子串的排序,和最后的倒推也是线性的。我们先考虑倒推。
可以发现后缀中首字母一样的肯定排一起,又因为 \(L\) 又一定在 \(S\) 之前,所以可以给每个字母开一个\(L\) 桶和一个 \(S\) 桶,并在 \(sa\) 中划分好空间。\(sa\) 可以初始化为 \(-1\)。
因为已将 LMS 子串排序,所以可以直接把它们放进去。
接下来正序扫描 \(sa\),若 \(sa_i-1\) 位置的后缀是 \(L\),则把它放入对应的 \(L\) 中。
然后倒序扫描 \(sa\),若 \(sa_i-1\) 位置的后缀是 \(S\),则把它放入对应的 \(S\) 中。
为什么后面两步成立?只看对 \(L\) 的排序,从左到右扫描 \(sa\) 相当于枚举已经放进去的后缀。现在讨论对于两个已知大小关系的后缀 \(X<Y\),\(aX\) 和 \(bY\) 的大小关系:
- \(a\ne b\),它们被放到不同的桶中。
- \(a=b\),\(aX<bY\),由于从左到右扫描 \(sa\),先扫描到 \(X\),所以也先插入 \(aX\)。
综上,上述排序算法对于 \(L\) 是正确的。对于 \(S\) 的证明类似,只不过要注意倒序。这叫做诱导排序。
对于 LMS 子串的排序,可以证明以任意顺序的 LMS 数组使用诱导排序,最后 LMS 子串是排好序的。
然后算法的整体流程就完了。
啥时候找个时间再写一遍。
pdf
height 数组
for(int i=0,k=0;i<=n;i++){
			k&&k--;
			if(!rk[i])continue;
			while(c[i+k]==c[sa[rk[i]-1]+k])k++;
			ht[rk[i]]=k;
		}
基于性质:\(height[rk[i]]\ge height[rk[i-1]]-1\)。之前推过证明,但是没有写上来。
不同子串的数目:\(\frac{n(n+1)}{2}-\sum\limits_{i=2}^nheight_i
\)
\(\operatorname{lcp}(suf_i,suf_j)=\underset{k=i+1}{\overset{j}{\min}}height_k\)
很多后缀自动机的题目后缀数组都可以做。不行的应该是在后缀自动机上 DP 等等。
后缀自动机(SAM)
void ins(int c){
	int p=la;
	len[la=++cnt]=len[p]+1;
	s[la]=1;
	while(p&&!to[p][c])
		to[p][c]=la,p=fail[p];
	if(!p)return (void)(fail[la]=1);
	int q=to[p][c];
	if(len[q]==len[p]+1)return (void)(fail[la]=q);
	int clo=++cnt;
	memcpy(to[clo],to[q],sizeof to[q]);
	fail[clo]=fail[q];
	len[clo]=len[p]+1;
	fail[q]=fail[la]=clo;
	while(p&&to[p][c]==q)
		to[p][c]=clo,p=fail[p];
}
刚写的。
分成三步:
- 从最后一个点跳 \(fail\),连边。
- 检查第一个有其它边的节点的转移是否连续。
- 如果不连续,就复制一个节点,将连续与不连续分开。
广义的注意是否有可直接利用的,有无的区别是更新的是 \(la\) 还是 \(fail[la]\)。
void ins(int c){
  int p=last;
  int &dif=to[p][c]?last:(len[last=++cnt]=len[p]+1,link[last]);
  while(p&&!to[p][c])to[p][c]=last,p=link[p];
  if(!p)return (void)(dif=1);
  int q=to[p][c];
  if(len[q]==len[p]+1)return (void)(dif=q);
  int ano=++cnt;
  memcpy(to[ano],to[q],sizeof to[q]);
  link[ano]=link[q];
  len[ano]=len[p]+1;
  dif=link[q]=ano;
  while(p&&to[p][c]==q)
    to[p][c]=ano,p=link[p];
}
\(\left[len[fail[u]]+1,len[u]\right]\)
空间要开两倍。
例题:弦论
求出 \(s[i]\),表示有多少个子串经过 \(i\) 号点,询问时直接遍历,遇到大的走进去即可。
例题:Standing Out from the Herd
在求本质不同子串的同时,还有一个条件:子串最多存在于一个串中。这提醒我们直接标记一个节点被一个还是多个字符串走过。最后在 fail 树上推一下即可。
Lyndon 分解
一个串的 Lyndon 分解是一个序列 \(A_1,A_2,\dots A_m\),其中 \(A_1\ge_{lex}A_2\ge_{lex}\cdots \ge_{lex}A_m\),且 \(A_1,A_2,\dots A_m\) 的 Lyndon 分解均仅包含自身。
若一个字符串的 Lyndon 分解仅包含自身,则它是一个 Lyndon 串。
一个串是 Lyndon 串,当且仅当它的所有共轭串(循环表示)中它的字典序最小。
Duval 算法可在 \(\mathcal O(n)\) 时间复杂度内计算一个字符串 \(S\) 的 Lyndon 分解。
它维护 \(i,j,k\) 三个变量,并且时刻满足:
- \([1,i-1]\) 是已确定的 Lyndon 分解,不再做出改动。
- \([i,k-1]\) 是一个没有确定下来的分解,形如 \(tt\cdots tt'\),其中 \(t\) 是 Lyndon 串,\(t'\) 是空串或 \(t\) 的真前缀,并且不能与前面已确定的 Lyndon 串合并。
- \(j=k-|t|\)。
对于 \(S_j\) 和 \(S_k\) 的大小关系,分三种情况讨论:
- \(S_j=S_k\),维持周期。
- \(S_j<S_k\),\([i,k]\) 形成新 Lyndon 串。
- \(S_j>S_k\),\(tt\cdots t\) 形成新 Lyndon 串,\(i\) 指向 \(t'\) 的开头的前一个字符。
代码:
for(int i=1,j,k;i<=n;){
	j=i,k=i+1;
	while(k<=n&&s[j]<=s[k])
		j=(s[j]<s[k++]?i:j+1);
	while(i<=j)
		ans^=(i+k-j-1),i+=k-j;
}
Nyldon 串和上述 Lyndon 串类似,只不过 \(\ge_{lex}\) 变成了 \(\le_{lex}\)。
注意:Nyldon 串和逆 Lyndon 串不同:逆 Lyndon 串是不能再分解成若干个 Lyndon 串,使 \(A_1\le_{lex}A_2\le_{lex}\dots\le_{lex}A_m\);Nyldon 串则是不能再分解成若干个 Nyldon 串。
任意字符串的逆 Lyndon 分解不一定是唯一的,但是 Nyldon 分解是唯一的。而且同样的长度和字符集下,Lyndon 串和 Nyldon 的个数是一致的。当 \(\Sigma=\{0,1\}\) 时,长度为 \(n\) 的 Lyndon 串的个数是 A001037。
没有找到 Nyldon 分解的 \(\mathcal O(n)\) 方法。
子序列自动机
很简单,\(to_{u,c}\) 为 \(u\) 之后的第一个为 \(c\) 字符的位置。
从后往前扫描,维护每个字符最前的出现位置,可以做到时间复杂度 \(\mathcal O(n|\Sigma|)\)。
但如果 \(|\Sigma|\) 太大,可以循其本源,发现:
\(to_{u,c}=\begin{cases}to_{u+1,c}&S_{u+1}\ne j\\u+1&\text{otherwise}\end{cases}\)
也就是说,\(to\) 每次只会单点修改,所以用主席树维护。
例题:子序列自动机
模板题。
例题:最短不公共子串
这题需要建出 SuffixAM 和 SequenceAM,在它们上 BFS,如果发现一个字符串在一个自动机上可接受,在另一个上不可接受,则找到答案。

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号