后缀数组 manacher 回文自动机 后缀自动机
后缀数组
后缀数组可以把字符串的所有后缀存起来,然后干各种奇怪的事情。
现在给你一个字符串 banana
,给他的后缀A,NA,ANA,NANA,ANANA,BANANA
跑一个后缀的trie。
然后把字典序小的字母排在左边,给每个后缀对应的叶节点标一下这个后缀首字母在文本串的位置。
从左到右连接下标就是后缀数组,比如这个的后缀数组是 sa[1]=3
表示第3+1=4个字母开头的后缀 ANA
在所有后缀中的字典序为1,sa[3]=0
就是 BANANA
的字典序为3。
int cnt[MAXN],lsa[MAXN],lrk[MAXN];
int sa[MAXN],rk[MAXN],hei[MAXN];
void getSA(){
m=128;
for(int i=1;i<=n;i++)cnt[rk[i]=txt[i]]++;
for(int i=1;i<=m;i++)cnt[i]+=cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[i]]--]=i;
int p=0;
for(int k=1;k<n;k<<=1,m=p){
int cur=0;
for(int i=n-k+1;i<=n;i++)lsa[++cur]=i;
for(int i=1;i<=n;i++)if(sa[i]>k)lsa[++cur]=sa[i]-k;
memset(cnt,0,sizeof cnt);
for(int i=1;i<=n;i++)cnt[rk[lsa[i]]]++;
for(int i=1;i<=m;i++)cnt[i] += cnt[i-1];
for(int i=n;i>=1;i--)sa[cnt[rk[lsa[i]]]--]=lsa[i];
for(int i=1;i<=n;i++)lrk[i]=rk[i];
p=0;
for(int i=1;i<=n;i++){
if(lrk[sa[i]]==lrk[sa[i-1]] && lrk[sa[i]+k]==lrk[sa[i-1]+k])rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
}
}
inline void getH(){
for(int i=1;i<=n;i++)rk[sa[i]]=i;
for(int i=1,k=0;i<=n;i++){
if(rk[i]==1)continue;
if(k)--k;
int j=sa[rk[i]-1];
while(i+k<=n&&j+k<=n&&txt[i+k]==txt[j+k])++k;
hei[rk[i]]=k;
}
}
板题代码。
相当于给后缀拼上前缀排序,我们可以直接给字符串扩一倍复制一下拿新的后缀跑后缀排序,找前
这个要涉及到SA的一种用法叫最长公共前缀。用
。
这个就非常牛逼了。上面的码已经写了怎么求这个数组。
在本题中对于排好序的两个后缀他俩的 aaaab
和 aaab
的 a,aa,aaa
三个相同子串。
Que:求一个字符串的最长回文串长度。
提出技巧:在原串后加入一个分隔符 {
,在复制一份颠倒的字符串拼接到后面跑后缀数组。
进而一旦出现回文串就一定存在一个原串部分的后缀和一个新串部分的后缀有 LCP,取height最大值即可。
对刚才那个技巧的延伸。
可以把所有串拼成一个大串,分隔符就是 '{'+i
,加是因为分隔符一样会影响后缀排序。
从
和上一道题是一样的,界定相同串的规则变成了差值相同,维护查分数组的SA即可,最后答案+1。
长的很ac自动机的一道题,但是我不会ac机。
先按套路把文本串赋id后拼长串跑SA。可以发现对于一次点名可以二分找出后缀序列中字典序和模式串满足权值偏序的转折点,进而考虑对模式串
进而问题一转化成处理后的区间
在
序列上的全点对问题极值可以用单调栈的套路处理,就是维护每个后缀成为答案贡献点的最长左右延伸,判定用
manacher
不难。板子思路就是维护当前最右侧的一段回文串
PAM
没学过ac自动机,略微吃力不过板子还是不难的。
就是先建trie树(稍微改造一下成回文版)然后建fail。每次新加一个字符会形成若干新回文串,考虑最长的那个回文串中一定包含了较短的那些新回文串(画图理解)而且较短的那些一定先前被存过了所以本质不同回文串是
SAM
我该怎么描述这个东西。。
就是说,在
那显然这个dag应该会比较多且丑,考虑用一种方案让这个dag变得好看就是parent树和后缀链接,就是把一个子串的所有结束位置称作它的
然后关于怎么搓,就是说对每个状态存它的最长的那个子串
然后说parent树就是把每次插入形成的字符串(即文本串的前缀)对应状态叫终点那parent树上的点的 终点集合和他子树内的终点节点的集合是相等的。然后
然后再说一下广义后缀自动机,有一种很好用的假做法是拿一个文本串建一个一般的sam然后后面的文本串每次清空last继续建,这样正确性是对的但是时空没有标准的优。然后我用的是tj区里的在线做法,就是说在这种假作法上加一些特判,我没太明白但是会敲。因为短。以后看懂了再记。
#include<bits/stdc++.h>
#define MAXN 1000005
#define ll long long
using namespace std;
int m,n;
ll ans;
char txt[MAXN];
struct Suffix_Automaton{
int tot=1,pos[MAXN<<1];
int fa[MAXN<<1],len[MAXN<<1],nxt[MAXN<<1][26];
inline int insert(int c,int lst){
if(nxt[lst][c]&&len[nxt[lst][c]]==len[lst]+1)return nxt[lst][c];
int cur=++tot,p=lst,f=0;
len[cur]=len[lst]+1;
while(p&&!nxt[p][c])nxt[p][c]=cur,p=fa[p];
if(!p){fa[cur]=1;return cur;}
else{
int q=nxt[p][c];
if(len[q]==len[p]+1){fa[cur]=q;return cur;}
if(p==lst)f=1,cur=0,--tot;
int cl=++tot;
len[cl]=len[p]+1;
fa[cl]=fa[q],fa[q]=fa[cur]=cl;
for(int i=0;i<26;i++)nxt[cl][i]=nxt[q][i];
while(nxt[p][c]==q)nxt[p][c]=cl,p=fa[p];
return f?cl:cur;
}
}
}SAM;
#define pos SAM.pos
#define nxt SAM.nxt
#define fa SAM.fa
#define len SAM.len
signed main(){
scanf("%d",&m);
for(int i=1;i<=m;i++){
scanf("%s",txt+1);
n=strlen(txt+1);
pos[0]=1;
for(int j=1;j<=n;j++)pos[j]=SAM.insert(txt[j]-'a',pos[j-1]);
}
for(int i=2;i<=SAM.tot;i++)ans+=(ll)(len[i]-len[fa[i]]);
printf("%lld\n%d",ans,SAM.tot);
return 0;
}
板子题大概就是这么个东西。然后整理一下题。
拿parent树定义说话就是说当前等价类的到它后缀链接最长长度之间的这一段子串肯定都是有的,每次的贡献就是
这题用SA早秒了。。
可以重的话,树上一个点的出现次数就是它子树出现次数之和,跑treedp,要不然直接全给成1。然后求的是第k小,在dag上贪心从小到大选,减去贡献并统计答案,可以记忆化一下。
这种题sa,sam,广义sam都能做。
sam做法其实是伪广义sam的一种实现,说一下就是上面提到的那种假做法,先跑第一个文本串,后面的当模式串一个一个在文本串上匹配,每次记录一个
sandy卡片跑个差分预处理就行,没啥说的。
看到了一种不拆式子直接算的方法,就是说答案形式很想一个parent树上lca的形式,所以直接求parent树上全点对路径和即
显然从所有叶子出发跑完所有路径就是答案,而且叶子还只有20个,然后就写广义SAM,每次从它的
不是很难。就是说二分答案然后 dp 验证一下,设
然后那个
瓶颈在dp,然后发现
就是说,求一下文本串的 sam,一个询问问的就是段区间内 parent树 上若干个点中 lca 对应
一个套路是把询问按右端点离线了,然后每次对于一个节点扫一下不同的枝杈,枝杈间点的lca肯定是这个点
合并完就会得到若干三元组
就是说和上个题是一样的,给文本串倒过来搓sam,然后处理一下异或,又因为想和上一题一样用启发式合并所以学了一下01trie合并。复杂度
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律