字符串总结
第二篇总结是关于字符串算法的,现在想把这种复习写成一个系列。
1.kmp算法
kmp算法是来求一个字符串的nxt数组的
nxt[i]数组表示的是由前i个字符组成的字符串即string[1,i]的
最长公共前后缀,即同时是前缀和后缀的最长串
扩展的时候考虑如何由之前已经求得的nxt数组来求
比较新加的字符与string[nxt[i-1]+1],如果相同nxt[i]=nxt[i-1]+1
否则跳到string[nxt[nxt[i-1]]+1]进行比较,即原最长公共前后缀的最长公共前后缀比较
可以证明该算法是线性的
在匹配串的过程中就可以利用nxt数组来减少比较量
nxt数组同样也可用于找字符串的循环节
如何满足$l\%(l-nxt[l])=0$那么就存在循环节
最小循环节长度为$l-nxt[l]$
2.exkmp
exkmp算法是用来求子串的extend数组的
extend[i]表示母串S[i,n]与子串T的最长公共前缀
假设我们已经求得了子串T的nxt数组
nxt[i]表示T与T[i,m]的最长公共前缀
假设我们已经求得了extend[1~i-1],考虑怎么求extend[i]
我们设母串已经匹配到的最远位置为x,存在p+extend[p]=x
定义len=nxt[i-p+1],分情况讨论
如果i+len<=x,则extend[i]=len
反之,说明之前并未匹配过,再进行暴力匹配即可,同时更新p,x
注意到nxt数组就是以S为母串,S为子串的extend数组,故可由同样方法求出
时间复杂度为线性
3.manacher
manacher算法用来快速处理回文子串问题
为了解决偶数长度回文串没有中心的问题,我们在原串的每一个字符左右都加上一个'#'
我们定义l[i]表示以第i个字符为中心的回文串的长度
我们设已经匹配到的最远位置为p,p+l[p]=x
我们设i关于p的对称为j=2*p-i,分情况讨论
如果j的回文有一部分在p的回文之外,那么l[i]=l[j]
如果j的回文都在p的回文之内,那么l[i]=l[j]
如果j的回文左端刚好与p的回文重叠,那么需要从x暴力跑下去更新
时间复杂度为线性
4.Trie
字典树,就是将若干个字符串插入,以每个字符为一个节点,构建出一颗树。
可以在树上套用其他相关算法完成一些统计任务
5.AC自动机
AC自动机多用于多模式串问题
AC自动机就是在trie树的基础上做kmp,在每一个节点上加一个fail指向失陪后转移的节点,相当于kmp的nxt数组
但是需要一直跳fail直至有相应的字符子节点或者到达root
AC自动机可以在此基础上构造trie图,当你失配时可以O(1)找到转移到的节点
如果当前节点无对应子节点c,则nxt[x][c]=nxt[fail[x]][c]
如果存在对应子节点,则fail[nxt[x][c]]=nxt[fail[x]][c]
构建出trie图之后可以在trie图上记录相应信息
也可以在trie图上套用dp或者图论算法
6.SAM
6.1SAM
SAM相当于以母串S的每一个后缀建一棵trie树
一个子串的endpos指的是该子串在母串S里出现过的位置的集合
每个节点表示endpos相同的一类后缀集合,定义maxlen为该集合中最长的子串长度,minlen为该集合中最短的子串长度
易证该集合内子串长度刚好构成$[minlen,maxlen]$,因为短串一定是长串的一个前缀
定义par指向它的父亲节点,满足父亲节点表示的子串集合是它的前缀且len[par[x]]+1=len[x]
构建的同时要考虑nxt的变化,假设我们已经构建了S[1,i-1]对应的后缀自动机,考虑将S[i]插入
这样一定会有新的节点,因为endpos中多了元素{i},我们新建一个节点表示S[1,i]对应的endpos{i}
然后我们沿着par向上扫描,如果扫到的节点不存在转移nxt[x][S[i]],那么直接nxt[x][S[i]]=now
若存在转移函数,要分俩种情况考虑,我们定义len[i]表示该节点的maxlen
如果len[x]=len[nxt[x][S[i]]+1,说明nxt[x][S[i]]中只对应着一个子串
那么新添加的S[i]只会对这个节点的endpos进行改变,所以直接将par[now]=nxt[x][S[i]]即可
否则,则说明nxt[x][S[i]]中不止一个子串,但是添加的S[i]只会对其中最短的子串进行修改
于是就新加一个节点表示这个子串,复制原有节点的转移关系,相当于只有这个子串的endpos发生了改变
同时记得修改par,nxt关系,par[new]=x,par[now]=par[nxt[x][S[i]]=new
然后沿着x向上扫描,将所有nxt[x][S[i]]指向new即可
可以证明转移数不超过3n,状态数不超过2n
不难发现,构建完的后缀自动机如果只保留par,会构成一棵树即后缀树
只保留转移函数nxt会构成一个有向无环图
沿着root出发的任意一条路径都对应着母串的一个子串
可以用来解决的问题:
查询字符串P是否在母串Q中出现过,沿着转移函数走就可以
查询本质不同的子串个数,从任何节点走到叶子节点都对应一个本质不同的子串
查询本质不同的子串总长度,实现大概同上,考虑每个节点的贡献
查询字典序第k大子串,预处理每个子树对应的子串数量,同trie树
查询最小循环串,每次贪心地走能走的最小的转移函数
查询子串P的出现次数(允许重叠),给除了克隆的节点以外的所有节点赋初值cnt=1,由par树自底向上累加
查询子串p的出现位置(允许重叠),每个节点记录第一次出现的位置,即建立该节点时母串已扫描的长度,这样找到p对应的节点,它及它的子树对应节点的所有第一次出现位置就是p的出现位置
查询没有出现过的最短子串p,如果某个节点存在一个空转移,那么初值为1,沿树自底向上转移即可
查询俩个字符串S,T的最长公共子串,构建S的后缀自动机,考虑求T的每一个前缀与S的最长公共后缀,保留一个当前匹配长度,当沿par转移时更改l=len[par[x]]即可
查询S与多个字符串的最长公共子串,将多个字符串中间加特殊符号连接并构建后缀自动机,同上匹配即可
6.2广义后缀自动机
广义后缀自动机就是将多个字符串构建在一个后缀自动机上
只要每次新构建串的时候,从root重新构建即可
如果已经存在对应的转移则无需新建节点,直接走过去就行
如果是统计出现次数一类的问题需要额外标记
7.回文自动机
回文自动机与上文提到的SAM,AC非常相似,甚至更加简单
构建回文自动机由于要考虑到回文长度有奇偶之分,建立俩个初始节点一个表示长度为奇数,一个为偶数
添加字符时,尝试像kmp一样查找最长回文后缀进行匹配,直至跳到根为止,这个过程是暴力向上跳的
然后维护一下新的fail即可
统计出现次数时要自下而上地累加一遍cnt

浙公网安备 33010602011771号