字符串相关(更新至SA)
字符串哈希
将字符集通过一些方式映射到整数集中,可以在
线段树维护哈希
题目链接。
根据
平衡树维护哈希
P4036 [JSOI2008] 火星人:与线段树维护哈希类似,合并时计算即可。
KMP
一种字符串单模匹配算法。
原理
当模式串
KMP 单模匹配算法引入了一个失配数组 border。
定义一个字符串的 border 为一个最长的字符串
的长度,满足字符串 既是 的真前缀,又是 的真后缀。
当失配时模式串
KMP 算法分
-
将模式串
与其自己匹配,求出数组 border。具体的,定义两个指针
和 ,表示此时 与 已经匹配成功的极大状态(以 和 结尾时无法实现更多字符的匹配),正在匹配 与 。(指针 是匹配是横跳的指针,指针 从左到右移动)若
和 失配,首先明确此时应该尽量让 与 实现匹配。那么指针 保持不变(因为是用 去匹配 之前的一段),去移动指针 。引理:当
与 匹配成功时,若 和 失配,则可能满足 和 匹配成功的最长字串的指针 的位置,应在 处。根据 border 的定义,反证法易证。
每次
进行匹配时,都会进行 的标记,那么 应在失配前就已经被标记了。所以,每次 和 失配时,就将指针 跳到 ,直到 与 匹配成功。 -
将模式串
与文本串 进行匹配,与第一步类似。
性质
-
若一个长为
的字符串 的最短周期长度为 ,则该字符串的 为 。证明:根据条件,有
,所以 ,得证。(见例题2) -
一个字符串的最短
长度不大于该字符串长度的一半。证明:反证法。记字符串
的最短 长度为 ,且 。根据定义有 ,则有 。可得 为该字符串的一个 。这与“最短”的定义相矛盾。
例题
P3375 【模板】KMP
for(int i=2,j=0;i<=lenb;i++){//first step while(j>0&&b[i]!=b[j+1]) j=fail[j]; if(b[i]==b[j+1]) j++; fail[i]=j; } for(int i=1,j=0;i<=lena;i++){//second step while(j>0&&(j==lenb||a[i]!=b[j+1])) j=fail[j]; if(a[i]==b[j+1]) j++; f[i]=j; if(f[i]==lenb){ printf("%d\n",i-lenb+1); } }
P4391 [BOI2009] Radio Transmission 无线传输
变换一下性质一,可见最短的周期长度为
AC 自动机
AC 自动机是一种字符串多模匹配算法。
如果对每一个模式串,都对文本串跑一遍 KMP,时间复杂度显然不可接受。
AC 自动机先对于模式串建立字典树,然后再在字典树上构建失配指针,可以看作是两种算法的结合。
构建字典树的过程和普通的相同,构建
显然所有的
以P5357 【模板】AC 自动机为例,利用有向无环图性质拓扑排序,代码如下:
queue<int> q; struct ACAM{ inline void insert(string s,int id){ int p=0,len=s.size(); for(int i=0;i<len;i++){ int now=s[i]-'a'; if(!trie[p].ch[now]) trie[p].ch[now]=++idx; p=trie[p].ch[now]; } if(!trie[p].flag) trie[p].flag=++tot; vis[id]=trie[p].flag; } inline void getfail(){ int p=0; for(int i=0;i<26;i++) if(trie[0].ch[i]) trie[p].fail=0,q.push(trie[p].ch[i]); while(!q.empty()){ int now=q.front(); q.pop(); for(int i=0;i<26;i++){ int v=trie[now].ch[i]; if(v) trie[v].fail=trie[trie[now].fail].ch[i],ind[trie[v].fail]++,q.push(v); else trie[now].ch[i]=trie[trie[now].fail].ch[i]; } } } inline void solve(string t){ int p=0,len=t.size(); for(int i=0;i<len;i++){ int now=t[i]-'a'; p=trie[p].ch[now]; trie[p].cnt++; } } inline void topo(){ for(int i=1;i<=idx;i++) if(!ind[i]) q.push(i); while(!q.empty()){ int u=q.front(); ans[trie[u].flag]=trie[u].cnt; q.pop(); int v=trie[u].fail; if(!v) continue; ind[v]--; trie[v].cnt+=trie[u].cnt; if(!ind[v]) q.push(v); } } }A;
例题
BZOJ 4502 串
做完之后对 AC 自动机有了更深的理解。
显然可以对所有字符串的前缀去重,然后记去重后的前缀数量为
枚举所有这样的 “
考虑如何统计出这样的 “
SA
如何快速地对一个字符串的所有后缀排序呢?暴力地 sort 时间复杂度是
倍增求 SA 数组
还不会 DC3 和 SA-IS,不过感觉倍增已经很优秀了qwq
定义
先将字符串
排序的过程可以用基数排序优化,做到
倍增求 SA 数组的代码如下:
其中数组
实现和基数排序不同的是,由于我们已经知道了当前的
char s[maxn]; int n,m=122,sa[maxn],c[maxn],x[maxn],y[maxn]; inline void suffix_sort(){ for(int i=1;i<=m;i++) c[i]=0; for(int i=1;i<=n;i++) c[x[i]]++; for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i];//双关键字,从后往前。 } inline void get_SA(){//求解SA数组 for(int i=1;i<=n;i++) c[x[i]=s[i]]++; for(int i=1;i<=m;i++) c[i]+=c[i-1]; for(int i=1;i<=n;i++) sa[c[x[i]]--]=i; for(int k=1;k<=n;k<<=1){//k为步长 int num=0; for(int i=n-k+1;i<=n;i++) y[++num]=i;//这一部分第二关键字为空,排名在前 for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;//按照sa直接排第二关键字 suffix_sort();//基数排序(双关键字) swap(x,y); x[sa[1]]=1,num=1; for(int i=2;i<=n;i++) x[sa[i]]=((y[sa[i]]==y[sa[i-1]])&&(y[sa[i]+k]==y[sa[i-1]+k])?num:++num);//重新计算x数组(重排名) if(num==n) break;//已经不同了 m=num;//更新桶大小 } for(int i=1;i<=n;i++) printf("%d ",sa[i]); cout<<endl; }
LCP 与 height 数组
定义
根据
进一步地,可以推出:
第一个式子画个图就明白了,考虑第二个式子我们可以将
根据第二个式子,我们可以将 LCP 问题转化成 RMQ 问题,于是引入
定义
开始学的时候一直搞混
引入
一个重要的定理:
证明:记排名在
于是,根据
inline void get_height(){//求height函数,height_i表示后缀排名为i的与排名为i-1的后缀的LCP,求出后可以用RMQ求出任意后缀的LCP int j,k=0; for(int i=1;i<=n;i++) rk[sa[i]]=i; for(int i=1;i<=n;i++){ if(k) k--; j=sa[rk[i]-1]; while(s[j+k]==s[i+k]) k++; height[rk[i]]=k;//height[rk[i]]=h[i] } for(int i=1;i<=n;i++) cout<<height[i]<<" "; cout<<endl; }
后缀数组的应用
字符串的不同字串个数
字串一定是这个字符串其中一个后缀的前缀,将这个字符串的后缀排序,并求出
例题:P2408 不同子串个数。
字符串的最长重复子串长度
- 求可以重叠且重复
次以上的最长字串长度。先求出 和 ,贪心地,满足重复恰好 次的字串长度一定比重复 次以上的字串更优。相当于我们用一个长度为 的滑块在 数组中滑动,重复恰好 次的字串长度为这个区间的最小值,对所有的取最大值即可,可以用单调队列求最小值做到求解部分的线性复杂度。例题:P2852 [USACO06DEC] Milk Patterns G。 - 求可以重叠的最长重复字串长度。显然为
的最大值。 - 求不可重叠的最长重复字串长度。答案具有单调性,考虑二分。记当前二分的长度为
,我们可以将 中连续大于 的分成一组,这一组一定是极大的,记录一组中的 的最大最小值,当 的极差大于当前的 时,答案成立。
求若干个字符串的最长公共字串(LCS)
假设有
可以用来求字符串最长回文子串:将字符串复制一份,并将拷贝的字符串反转,转化为最长公共子串问题。不过时间复杂度瓶颈在于求 sa,Manacher 的
与单调栈结合
显然瓶颈在于求任意两个子串的
for(int i=2;i<=n;i++){ while(top&&height[sta[top]]>=height[i]) sta_ans-=(top==1?sta_ans:1ll*(sta[top]-sta[top-1])*height[sta[top]]),top--; sta_ans+=(top?1ll*height[i]*(i-sta[top]):1ll*(i-1)*height[i]); ans+=sta_ans,sta[++top]=i; } printf("%lld\n",1ll*(n-1)*(n+1)*n/2-2*ans);
例题
P3181 [HAOI2016] 找相同字符
和差异那一题相似,先分别对两个字符串求出答案,然后再将两个字符串拼起来求出答案
P5341 [TJOI2019] 甲苯先生和大中锋的字符串
用一个长为
本文作者:dayz_break
本文链接:https://www.cnblogs.com/dayz-break/p/18342280
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步