后缀自动机

应用

简便起见,我们假设字母表的大小k为常数。

存在性查询

问题.给定文本T,询问格式如下:给定字符串P,问P是否是T的子串。
分析:直接沿着go[][]对应着跳

复杂度要求.预处理O(length(T)),每次询问O(P)。

算法.我们对文本T用O(length(T))建立后缀自动机。

现在回答单次询问。假设状态——变量v,最初是初始状态T_0.我们沿字符串P给出的路径走,因此从当前状态经转移来到新的状态v。如果在某时刻,当前状态没有要求字符的转移,那么答案就是”no”。如果我们处理了整个字符串P,答案就是”yes”。

显然这一算法将在时间O(length(P))内运行完毕。并且,该算法实际上找出了P在文本中出现过的最长前缀——如果模式串使得这些前缀都很短,算法将比处理全部模式串要快得多。

不同的子串个数

问题.给定字符串S,问它有多少不同的子串。

复杂度要求.O(length(S))。

算法.我们将字符串S建立后缀自动机。

在后缀自动机中,S的任意子串都对应自动机中的一条路径。答案就是从初始节点t_0开始,自动机中不同的路径条数。

已知后缀自动机是一张有向无环图,我们可以考虑用动态规划计算不同的路径数量。

也就是,令d[v]为从状态v开始的不同路径条数(包括长度为零的路径),则有转移:

即d[v]是v所有后继节点的d值之和加上1.

最终答案就是d[t_0]-1(减一以忽略空串)。

不同子串的总长

问题.给定字符串S,求其所有不同子串的总长度。

复杂度要求.O(length(S)).

算法.这一问题的答案和上一题类似,但现在我们必须考虑两个状态:不同子串的个数d[v]和它们的总长ans[v].

上一题已描述了d[v]的计算方法,而ans[v]的计算方法如下:

即取所有后继节点w的ans值,并将它和d[w]相加。因为这是每个字符串的首字母。

字典序第k小子串

问题.给定字符串S,一系列询问——给出整数K_i,计算S的所有子串排序后的第K_i个。

复杂度要求.单次询问O(length(ans)*Alphabet),其中ans是该询问的答案,Alphabet是字母表大小。

算法.这一问题的基础思路和上两题类似。字典序第k小子串——自动机中字典序第k小的路径。因此,考虑从每个状态出发的不同路径数,我们将得以轻松地确定第k小路径,从初始状态开始逐位确定答案。

最小循环移位

问题.给定字符串S,找到和它循环同构的字典序最小字符串。

复杂度要求.O(length(S)).

算法.我们将字符串S+S建立后缀自动机。该自动机将包含和S循环同构的所有字符串。

从而,问题就简化成了在自动机中找出字典序最小的,长度为length(S)的路径,这很简单:从初始状态开始,每一步都贪心地走,经过最小的转移。

×出现次数查询

问题.给定文本T,询问格式如下:给定字符串P,希望找出P作为子串在文本T中出现了多少次(出现区间可以相交)。

复杂度要求.预处理O(length(T)),单次询问O(length(P)).

算法.我们将文本T建立后缀自动机。

然后我们需要进行预处理:对自动机中的每个状态v都计算cnt[v],等于其endpos(v)集合的大小。事实上,所有在T中对应同一状态的字符串都在T中出现了相同次数,该次数等于endpos中的位置数。

不过,我们无法对所有状态明确记录endpos集合,所以我们只计算其大小cnt.

为了实现这一点,如下处理。对每个状态,如果它不是由“拷贝”而来,最初就赋值cnt=1.然后我们按长度len降序遍历所有序列,并将当前的cnt[v]加给后缀链接:

cnt[link(v)]+=cnt[v].

你可能会说我们并没有对每个状态计算出了正确的cnt值。

为什么这是对的?不经“拷贝”而来的状态恰好有length(S)个,而且其中的第i个是我们添加第i个字符时得到的。因此,最初这些状态的cnt=1,其他状态的cnt=0.

然后我们对每个状态v执行如下操作:cnt[link(v)]+=cnt[v].其意义在于,如果某字符串对应状态v,曾在cnt[v]中出现过,那么它的所有后缀都同样在其中出现。

这样,我们就掌握了如何对自动机中所有状态计算cnt值的方法。

在此之后,询问的答案就变得平凡——只需要返回cnt[t],其中t是模式串P所对应的状态。

×首次出现位置查询

问题.给定文本T,询问格式如下:给定字符串P,求P在文本中第一次出现的位置。

复杂度要求.预处理O(length(T)),单次询问O(length(P)).

算法.对文本T建立后缀自动机。

为了解决这一问题,我们需要预处理firstpos,找到自动机中所有状态的出现位置,即,对每个状态v我们希望找到一个位置firstpos[v],代表其第一次出现的位置。换句话说,我们希望预先找出每个endpos(v)中的最小元素(我们无法明确记录整个endpos集合)。

维护这些firstpos的最简单方法是在构建自动机时一并计算,当我们创建新的状态cur时,一旦进入函数sa_extend(),就确定该值:

firstpos(cur)=len(cur)-1

(如果我们的下标从0开始)。

当拷贝节点q时,令:

firstpos(clone)=firstpos(q),

(因为只有一个别的可能值——firstpos(cur),显然更大)。

这样就得到了查询的答案——firstpos(t)-length(P)+1,其中t是模式串P对应的状态。

×所有出现位置查询

问题.给定文本T,询问格式如下:给定字符串P,要求给出P在T中的所有出现位置(出现区间可以相交)。

复杂度要求.预处理O(length(T))。单次询问O(length(P)+answer(P)),其中answer(P)是答案集合的大小,即,要求时间复杂度和输入输出同阶。

算法.对文本T建立后缀自动机,和上一个问题相似,在构建自动机的过程中对每个状态计算第一次出现的终点firstpos。

假设我们收到了一个询问——字符串P。我们找到了它对应的状态t。

显然应当返回firstpos(t)。还有哪些位置?我们考虑自动机中那些包含了字符串P的状态,即那些P是其后缀的状态。

换言之,我们需要找出所有能通过后缀链接到达状态t的状态。

因此,为了解决这一问题,我们需要对每个节点储存指向它的所有后缀链接。为了找到答案,我们需要沿着这些翻转的后缀链接进行DFS/BFS,从状态t开始。

这一遍历将在O(answer(P))时间内结束,因为我们不会访问同一状态两次(因为每个状态的后缀链接仅指向一个点,因此不可能有两条路径通往同一状态)。

然而,两个状态的firstpos值可能会相同,如果一个状态是由另一个拷贝而来。但这不会影响渐进复杂度,因为每个非拷贝得到的节点只会有一个拷贝。

此外,你可以轻松地除去那些重复的位置,如果我们不考虑那些拷贝得来的状态的firstpos。事实上,所有拷贝得来的状态都被其“母本”状态的后缀链接指向。因此,我们对每个节点记录标签is_clon,我们不考虑那些is_clon=true的状态的firstpos。这样我们就得到了answer(P)个不重复地状态。

查询不在文本中出现的最短字符串

问题.给定字符串S和字母表。要求找出一个长度最短的字符串,使得它不是S的子串。

复杂度要求.O(length(S)).

算法.在字符串S的后缀自动机上进行动态规划。

令d[v]为节点v的答案,即,我们已经输入了字符串的一部分,匹配到v,我们希望找出有待添加的最少字符数量,以到达一个不存在的转移。

计算d[v]非常简单。如果在v处某个转移不存在,那么d[v]=1:用这个字符来“跳出”自动机,以得到所求字符串。

否则,一个字符串无法达到要求,因此我们必须取所有字符中的最小答案:

原问题的答案等于d[t_0],而所求字符串可以用记录转移路径的方法得到。

求两个字符串的最长公共子串

问题.给定两个字符串S和T。要求找出它们的最长公共子串,即一个字符串X,它同时是S和T的子串。

复杂度要求.O(length(S)+length(T)).

算法.我们对字符串S建立后缀自动机。

我们按照字符串T在自动机上走,查找它每个前缀在S中出现过的最长后缀。换句话说,对字符串T中的每个位置,我们都想找出S和T在该位置结束的最长公共子串。

为了实现这一点,我们定义两个变量:当前状态v和当前长度l。这两个变量描述了当前的匹配部分:其长度和状态,对应哪个字符串(如果不储存长度就无法确定这一点,因为一个状态可能匹配多个有不同长度的字符串)。

最初,p=t_0,l=0,即,匹配部分为空。

现在我们考虑字符T[i],我们希望找到这个位置的答案。

如果自动机中的状态v有一个符号T[i]的转移,我们可以简单地走这个转移,然后将长度l加一。
如果状态v没有该转移,我们应当尝试缩短当前匹配部分,为此应当沿着后缀链接走:
v=link(v).
在此情况下,当前匹配长度必须被减少,但留下的部分尽可能多。显然,应令l=len(v):
l=len(v).
若新到达的状态仍然没有我们想要的转移,那我们必须再次沿着后缀链接走,并且减少l的值,直到我们找到一个转移(那就返回第一步),或者我们最终到达了空状态-1(这意味着字符T[i]并未在S中出现,所以令v=l=0然后继续处理下一个i)。

原问题的答案就是l曾经达到的最大值。

这种遍历方法的渐进复杂度是O(length(T)),因为在一次移动中我们要么将l加一,要么沿着后缀链接走了若干次,每次都会严格减少l。因此,l总共的减少值之和不可能超过length(T),这意味着线性时间复杂度。

多个字符串的最长公共子串

问题.给出K个字符串S_1~S_K。要求找出它们的最长公共子串,即一个字符串X,它是所有S_i的子串。

复杂度要求.O(∑length(S_I)*K).

算法.将所有S_i连接在一起成为一个新的字符串T,其中每个S_i后要加上一个不同的分隔符D_i(即加上K个额外的不同特殊字符D_1~D_K):

我们对字符串T构建后缀自动机。

现在我们需要在后缀自动机找出一个字符串,它是所有字符串S_i的子串。注意到如果一个子串在某个字符串S_j中出现过,那么后缀自动机中存在一条以这个子串为前缀的路径,包含分隔符D_j,但不包含其他分隔符D_1,…,D_j-1,D_j+1,…,D_k。

因此,我们需要计算“可达性”:对自动机中的每个状态和每个字符D_i,计算是否有一条从该状态开始的路径,包含分隔符D_i,但不包含别的分隔符。很容易用DFS/BFS或者动态规划实现。在此之后,原问题的答案就是字符串longest(v),其中v能够到达所有的分隔符。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值