我想,字符串的艺术,是在走过的字符上留下路标,让每一次眺望,都成为对过往的回应。
——Zhl
Manacher
P3805 【模板】Manacher
首先暴力就是直接对于每一个点向外扩展。
然后需要优化,就是利用已知的更新未知的。
首先我们现在在节点 ,然后我们记录下了右节点最远的一个回文串,。
分类讨论:
-
,此时,我们可以用 关于 对称的点来更新 ,然后超出原来的对称区间的直接暴力。
-
直接暴力扩展。
因为右边界扩展时 的,所以算法复杂度 。
然后一个经典的实现就是为了防止判断奇偶,直接在两两之间塞 #。
所以 Manacher 的核心思想在于前面的信息怎么对后面有用。
代码
#include<bits/stdc++.h>
using namespace std;
int n,ans=0;
char c[11000010],s[22000010];
int p[22000010],cnt=0;
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>(c+1);
n=strlen(c+1),s[++cnt]='#',s[++cnt]='#';
for(int i=1;i<=n;i++)s[++cnt]=c[i],s[++cnt]='#';n=cnt;
int mid=0,r=0;
for(int i=1;i<=n;i++){
if(i<=r)p[i]=min(p[mid*2-i],r-i+1);
while(s[i-p[i]]==s[i+p[i]])p[i]++;
if(p[i]+i-1>r)r=p[i]+i-1,mid=i;
ans=max(ans,p[i]);
}
cout<<ans-1<<endl;
return 0;
}
KMP
P3375 【模板】KMP
KMP 的思想主要是有一个 kmp 数组,然后记录匹配失败后,需要移动到哪里。
每一个位置只需要进行一次比较。
然后代码比较直观。
代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
char s[1000010],t[1000010];
int kmp[1000010];
signed main(){
cin>>(s+1)>>(t+1);
int n=strlen(s+1),m=strlen(t+1);
int j=0;
for(int i=2;i<=m;i++){
while(j&&t[j+1]!=t[i])j=kmp[j];
if(t[i]==t[j+1])j++;
kmp[i]=j;
}
j=0;
for(int i=1;i<=n;i++){
while(j&&s[i]!=t[j+1])j=kmp[j];
if(t[j+1]==s[i])j++;
if(j==m){
cout<<i-m+1<<'\n';
j=kmp[j];
}
}
for(int i=1;i<=m;i++)cout<<kmp[i]<<' ';
cout<<'\n';
return 0;
}
P2375 [NOI2014] 动物园
这道题我们需要在长度超限的情况下 。
然后问的是数量,就直接在预处理的时候处理出来,表示子串中符合的个数(对长度没有限制)。
扩展 KMP
P5410 【模板】扩展 KMP / exKMP(Z 函数)。
这篇 TJ 讲的清楚,C 过来。
结论: 对于 ,对任意 都可以递推得:
证明:
主要思想是将这个位置转换成前面的一个位置加上一个位移,然后这个位置的 函数我们是知道的,只需要移动到 就可以转换成下一个函数了。
初始化 ,然后暴力判断字符相等增加 。
这里 选满足 最大的 ,这样每个字符只会被暴力判断一次(如果最小值是 就会第一次就失配),所以时间复杂度可以做到 。
对于题目中的问题其实把 和 接起来做个 就可以了。
最小表示法
先断环成链放两个。
先放 和 ,表示最小的和当前的(下标小的设置为最小的)。
然后如果 是最小的,然后现在有来了一个 ,先把能匹配的 位匹配。
如果 更小, 就变成 ,因为这中间的一定比 到 中间的某一个大(他们匹配了一些位置了)。
如果 更小, 变成 。
复杂度易证。
代码
#include<bits/stdc++.h>
using namespace std;
int n;
char s[20000010];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
cin>>(s+1);
for(int i=1;i<=n;i++){
s[i+n]=s[i];
}
int i=1,j=2;
for(;i<=n&&j<=n;){
int k=0;
while(k<=n&&s[i+k]==s[j+k])k++;
if(s[i+k]>s[j+k]) i+=k+1;
else j+=k+1;
if(i==j)j++;//不要漏
}
int ans=min(i,j);
for(int i=ans;i<=ans+n-1;i++)cout<<s[i];
cout<<endl;
return 0;
}
AC 自动机
P5357 【模板】AC 自动机
AC 自动机是一个基于 Trie,结合 KMP 思想的算法。
首先,先对模式串建立一个 Trie。
fail 指针
然后我们需要定义一个失配指针 , 指向 表示 是 的最长在 Trie 树存在的后缀(从根节点开始的路径)。
与 KMP 不同,KNP 表示最长相同的前后缀。
所以有可能 的指针指向另一个模式串。
指针计算
类比一下 KMP。
我们发现这一类字符串的主要思想就是用已知更新未知。
首先,我们现在在 ,然后它的父节点是 , 通过字符 的边指向 ,即 。分类讨论:
-
如果 存在,那么我们就可以在 和 之后扩展一下,就可以让 指向 了。
-
否则,我们继续找到 ,一直重复,直到匹配成功或者到达根节点。
然后我们就能写出预处理函数了,询问的话就用 KMP 类似思路就行了。
询问我们失配的时候就指向 ,就是最大限度利用已经匹配的东西,找最大的后缀继续。
优化
我们发现这样暴力跳回超时,所以我们需要优化一下。
直接拓扑排序,也就是先遍历会对后面有贡献的。
P2444 [POI 2000] 病毒
这道题中,只需要建完 AC 自动机的 Trie 后,判断是否又有不经过插入字符串的最后一个节点的环就行了。
然后有一个需要注意的地方就是需要 ed[v]|=ed[fail[v]],因为如果如果一个节点的一个后缀是病毒,这个节点也是。
P3041 [USACO12JAN] Video Game G
P3041 [USACO12JAN] Video Game G。
AC 自动机 + DP。
:长度为 ,在 AC 自动机节点 的最大得分。
就是走到 能获得的权值。
P4052 [JSOI2007] 文本生成器
AC 自动机上 dp。
容斥一下, 表示长度为 ,在 的时候的情况数。
回文树
P5496 【模板】回文树 / 回文自动机(PAM)
基础形态
我们想知道一个字符串是不是回文串,可以根据它的字串来判断。
我们利用回文字符串这样性质把它们放到树上。
长度为奇数的回文子串有中心,而长度为偶数的回文子串没有,所以回文树有两个初始的状态, 和 分别表示奇根和偶根,与 AC 自动机的根作用差不多。
构建回文自动机的方法就是,根节点到这个节点表示这个回文串的一半。
线性状态数证明
我们需要证明树的大小是线性的。
直接扒 OI-WIKI 的。
我们需要先证明:对于一个字符串 ,它的本质不同回文子串个数最多只有 。
当 时,设 ,其中 表示 最后增加一个字符 后形成的字符串。
假设结论对 串成立.考虑以最后一个字符 结尾的回文子串,假设它们的左端点由小到大排序为 。
由于 是回文串,因此对于所有位置 ,有 .所以,对于 , 已经在 中出现过,因此,每次增加一个字符,本质不同的回文子串个数最多增加 个。
构建方法
初始阶段,偶根的 指向奇根。
我们考虑一个一个插入,现在树里面已经有了 ,然后我们现在要插入 。
我们从以上一个字符结尾的最长回文子串对应的节点开始,不断沿着 fail 指针走,直到找到一个节点满足 ,即满足此节点所对应回文子串的上一个字符与待添加字符相同。
代码
代码
#include<bits/stdc++.h>
using namespace std;
int n;
char s[500010];
int len[500010],t[500010][26],tot=1,fail[500010],num[500010],now=0;
int getfail(int x,int i){
while((i-len[x]-1<1)||s[i-len[x]-1]!=s[i])x=fail[x];
return x;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>(s+1);
n=strlen(s+1);
fail[0]=1;
len[1]=-1;
int lastans=0;
for(int i=1;i<=n;i++){
//if(i>=2)s[i]=(char)(s[i]+lastans-97)%26+97;
if(i>=2)s[i]=(char)((s[i]+lastans-97)%26+97);
int v=getfail(now,i);
if(!t[v][s[i]-'a']){
fail[++tot]=t[getfail(fail[v],i)][s[i]-'a'];
t[v][s[i]-'a']=tot;
len[tot]=len[v]+2;
num[tot]=num[fail[tot]]+1;
}
now=t[v][s[i]-'a'];
lastans=num[now];
cout<<lastans<<' ';
}
cout<<endl;
return 0;
}
后缀数组
定义
我们需要先定义两个数组:
- 表示后缀排序之后第 小后缀的编号。这个就是后缀数组。
- 表示后缀 的排名。
性质:。
求法
我们需要利用倍增的思想,就是对于长度为 的子串进行排序处理,。
然后这个倍增很妙,就是利用这个子串的前一半作为第一关键字,第二半作为第二关键字,进行排序。
复杂度 。桶排序可以做到单 。
height
表示 这两个后缀的最长公共前缀。
其中 。
数组可以根据这个暴力求。
性质:
注意: 个数有 个 height。
P2178 [NOI2015] 品酒大会
第一个问题就是问有多少个 使得 。
倒序枚举问题就转化成了有多少个 的了。然后这个我们可以用 数组转化成 (排序之后)。
然后问题可以转化成有多少 满足这里面的 的最小值恰好等于 。
然后可以继续优化,按照 数组降序排序插入,计算答案。计算方法就是当前这条“边”,然后两边的堆的个数相乘。
第二问我们只需要维护最大值就行了。不过有负数,所以最小值也需要维护上。
P4248 [AHOI2013] 差异
还是跟上一道题一样,需要先转化成 数组,然后利用性质:
然后问题就转化成了区间最小值的和了,用单调栈。
Lyndon 分解
引理 1:如果 和 都是 串并且 ,则 也是 串。
如果 就易证了,因为 在 后面的一定比 小。
否则,如果 不是 前缀,易证,是的话,如果 那么 ,矛盾。
唯一性
存在性
,然后每次不断找到 并且合并为一个串,最后一定能使得所有的 。
唯一性
(不想写了,直接看题解的吧)
假设对于字符串 存在两个 分解:
设 。
观察 在第二种分解中的对应情况。假设 。
那么由 串的性质可知:
矛盾。
引理2:若字符串 和字符 满足 是某个 串的前缀,则对于字符 有 是 串。
这个可以感性理解。
Duval 算法
这个算法的本质就是维护一个周期串。
维护 , 到 已经固定,现在在 , 就是一个周期的长度。
的时候周期保持。
的时候根据引理 ,我们可以形成一个新的 Lyndon 串。然后这个点重新作为周期,因为多余的这一块可以证明是能合并的,然后引理 告诉我们这个也可以跟前面合并。
,我们直接右移 就行了。