字符串总结

第二篇总结是关于字符串算法的,现在想把这种复习写成一个系列。

 

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

 

 

 

 

 

 

 

posted @ 2018-06-14 16:26  2018szb  阅读(101)  评论(0)    收藏  举报