字符串
字符串
本文旨在复习字符串算法,包含一些传统算法和神秘乱搞,不涉及 border 论与 Lyndon。
字符串哈希
生日悖论指出,选取的映射集合大小应该超过所需判断集合 \(S\) 的 \(|S|^2\) 量级。
对于单哈希选取的质数 \(p\),其能处理的数据规模应不超过 \(\sqrt p\)。
双哈希是卡不掉的,可以放心使用双哈希。
KMP 与 AC 机
对于 KMP 算法,个人喜欢的理解是求前缀 \([1,i]\) 的 boder,记其长度为 \(\text{fail}_i\)。
int n;
char s[maxn];
//对于 s[1,n] 做 KMP。
void kmp(){
int i=2,j=1;
while(i<=n){
if(s[i]==s[j])fail[i]=j,i++,j++;
else{
if(j==1)i++;
else j=fail[j-1]+1;
}
}
return ;
}
border 理论中一点基础的结论是:
- border 结构呈树状,祖先是后代的 border。
- 一个串的最短 border 长度不会超过串长的一半。
称 \(i\) 走 \(s_{i+1}\) 到 \(i+1\) 的转移与失配的转移的并为 KMP 自动机。
最基础的应用是处理模板串在文本串中的出现次数。
使用 KMP 自动机作为 DP 状态是一种常见的技巧,可以处理字符串出现次数一类的 DP 问题。
一个有趣的问题是将 KMP 可持久化:
一个经典的做法是将 KMP 稍加改进,使得其跳失配树有严格的保证。
具体的,如果 \(fail_j>\dfrac{j}{2}\),这说明串有一个较短的周期,我们直接跳过所有整周期,令 \(j:=(j-1)\bmod(j-fail_j)+1\)。
否则我们直接跳 \(fail_j\) 就行了,保证了对于单个 \(i\),\(j\) 的移动次数不超过 \(\log n\)。
一些练习:
对于 AC 机,就是多个模式串的 KMP 自动机结构。定义 \(\text{fail}_k\) 为 trie 树上 \(k\) 节点的最长真后缀在 trie 树上的对应节点。
void pre(){
for(int i=0;i<26;i++)if(t[0][i])q.push(t[0][i]);
while(!q.empty()){
int k=q.front();
q.pop();
for(int i=0;i<26;i++){
int tmp=t[k][i];
if(tmp)fail[tmp]=t[fail[k]][i],q.push(tmp);
else t[k][i]=t[fail[k]][i];
}
}
return ;
}
一些多模式串的问题不要一来就直接广义 SAM,AC 机是一个很好的选择。
值得一提的是 AC 机对于字符集较大的情况需要使用主席树维护出边,即 \(t(k,i)\)。
AC 机的结构经常需要使用树上数据结构维护一些修改查询操作,但是 border 树上少见?
AC 机同样可以用于 DP 状态。
一些练习:
Manacher 与扩展 KMP
因为扩展 KMP 好像主要是直接使用板子得到的 Z 函数,而 Manacher 和扩展 KMP 几乎没有区别,就放一起吧。
//s[1,n]
//p[i] 表示位置 i 的最长回文半径(包含 i)。
for(int i=1;i<=2*n-1;i+=2)t[i]=s[i/2];
for(int i=0;i<=2*n;i+=2)t[i]='?';
int mid=0,r=0;
for(int i=1;i<=2*n;i++){
if(i<=r)p[i]=min(p[2*mid-i],r-i+1);
else p[i]=1;
while(i-p[i]>=0&&i+p[i]<=2*n&&t[i-p[i]]==t[i+p[i]])p[i]++;
if(i+p[i]>r)mid=i,r=i+p[i]-1;
ans=max(ans,p[i]-1);
}
//z[i] 表示 LCP(a[1,n],a[i,n])。
z[1]=n;
int l=0,r=0;
for(register int i=2;i<=n;i++){
if(i<=r)z[i]=fmin(z[i-l+1],r-i+1);
while(i+z[i]<=n&&a[i+z[i]]==a[z[i]+1])z[i]++;
if(i+z[i]-1>r)l=i,r=i+z[i]-1;
ans1^=1ll*i*(z[i]+1);
}
时间复杂度分析都是 \(r\) 递增,复杂度线性。
PAM
不是很会。
bitset
bitset 做字符串匹配实在是太神秘了!!1
假设文本串为 \(s\)。
核心的想法是对于字符集中的每一个字符 \(c\),开一个大小为 \(|s|\) 的 bitset \(T_c\),记录 \(c\) 出现在 \(s\) 中的哪些位置。
查询 \(t\) 在 \(s\) 中的出现的开头位置可以考虑使用一个大小为 \(|s|\) 的 bitset \(M\),初始为全 1,对于 \(t\) 的每个位置 \(i\),将 \(M\) 对 \(T_{t_i}\) 右移 \(i-1\) 位取交,最终得到的 \(M\) 即为所有开头位置。
一个巨大的优势是可以低复杂度修改文本串。
时间复杂度 \(O(\dfrac{|t||s|}{w})\)。
SAM
不会考。