简单字符串

最小循环表示法

求一个字符串的最小循环表示法的起始位置。

先把串倍长,维护两个指针\(i,j\)表示可能的最小循环表示的起始位置。
暴力求出\(k=lcp(i,j)\),然后比较\(s_{i+k},s_{j+k}\)
如果\(s_{i+k}<s_{j+k}\),那么\([j,j+k]\)范围内的所有下标均不可能成为最小循环表示的起始位置,所以令\(j=j+k+1\)
如果\(s_{j+k}<s_{i+k}\),那么\([i,i+k]\)范围内的所有下标均不可能成为最小循环表示的起始位置,所以令\(i=i+k+1\)
直到\(i,j\)有一个超过了\(n\)为止。
我们暴力求一次\(k=lcp(i,j)\)的复杂度是\(O(k)\)的,而\(i,j\)有一个会增加\(k\),因此时间复杂度是均摊\(O(n)\)的。

Manacher算法

求出一个字符串中以各个位置为中心的最长回文子串的长度。

为了解决回文中心在两个字符中间的问题,我们在相邻两个字符间都插入一个$
\(len_i\)表示以\(i\)为中心的最长回文子串的半径(\(i\)自己算\(1\)),同时记录前缀中\(i+len_i\)的最大值\(mx\)与其下标\(pos\)
考虑从左往右计算\(len\),假如当前的位置是\(i\),那么根据对称性我们知道\(len_i\ge\min(len_{pos*2-i},mx-i)\),然后我们再暴力扩展\(len_i\)。这样我们就可以求出\(len_i\)了。
因为每次成功的暴力扩展一定会使\(mx\)向右移动,因此时间复杂度是均摊\(O(n)\)的。

Knuth-Morris-Pratt算法

求出字符串\(s\)\(next\)数组,\(next_i\)表示\(pre_i\)的最长border的长度。

考虑从左往右计算\(next\),假如当前的位置是\(i\),我们先令\(k=next_{i-1}\),然后判断\(s_{k+1},s_i\)是否相等,相等就有\(next_i=k+1\),否则就令\(k=next_k\)继续跳,直到相等或者跳到\(0\)为止。
因为一个位置的\(next\)被跳过之后就不会再被跳了,因此时间复杂度是均摊\(O(n)\)的。

Some Applications

\(1.\)给定\(s,t\),求出\(t\)\(s\)中的出现次数以及出现位置。

把暴力匹配时的往后跳\(1\)改成跳失配位置的\(next\)就行了,复杂度是均摊\(O(n)\)的。

\(2.\)给定\(s,t\),求出\(s\)的每个\(pre\)\(t\)中的出现次数。

先考虑\(t=s\)的情况,直接做似乎不太好做,我们考虑计算以每个位置\(i\)结尾有哪些\(pre\)出现过。
根据KMP的过程我们发现就是\(i,next_i,next_{next_i-1},\cdots\)这些\(pre\)
\(i\)我们单独抠出来最后算,其它的东西构成了一个树型结构,即\(pre_k\)出现的地方\(pre_{next_k-1}\)一定会出现。
我们知道每个\(pre_{next_i}\)都至少出现了一次,然后我们再自底向上(实际上就是从后往前)做个后缀和就好了。
然后考虑\(t\ne s\)怎么做。先求出\(s\)的每个\(pre\)\(s\)中的出现次数,再求出\(s\)的每个\(pre\)s$t中的出现次数,然后减一下就好了。这个过程的含义已经不言而喻了。
很显然,时间复杂度都是\(O(n)\)

Z-算法

给定字符串\(s\),求\(s\)\(t\)每个\(suf\)\(lcp\)

先考虑\(t=s\)的情况,记\(len_i\)表示以\(lcp(s,suf_i)\),同时记录前缀中\(i+len_i\)的最大值\(mx\)与其下标\(pos\)
考虑从左往右计算\(len\),假如当前的位置是\(i\),那么我们知道\(len_i\ge\min(len_{i-pos+1},mx-i)\),然后我们再暴力扩展\(len_i\)。这样我们就可以求出\(len_i\)了。
因为每次成功的暴力扩展一定会使\(mx\)向右移动,因此时间复杂度是均摊\(O(n)\)的。
然后考虑\(t\ne s\)怎么做。求出\(s\)s$t每个\(suf\)\(lcp\)就好了。

SA

求出\(s\)的每个\(suf\)的相对大小关系。

\(rank_i\)表示\(suf_i\)的排名,\(sa_i\)表示排名为\(i\)\(suf\)的起始位置。
SA-IS

\(height\)数组

定义\(h_i=lcp(suf_{sa_i},suf_{sa_{i-1}}),h_1=0\)
我们可以得到一个引理:\(h_{rank_i}\ge h_{rank_{i-1}}-1\)
因此我们可以暴力扩展求出\(h\),复杂度为均摊\(O(n)\)

Some Applications

\(1.\)给定\(s\),求最小的从\(s\)首尾取字符得到的新串

如果首尾字符不同那么可以简单贪心。
如果相同的话我们比较当前串的顺序和逆序(即原串的某个\(suf\)\(pre\)),然后根据结果选择。
因此我们可以求s$t\(t\)\(s\)的逆序)的SA,这样就可以比较\(s\)的任意两个前后缀的大小了。
时间复杂度\(O(n)\)

\(2.\)给定\(s\),求\(lcp(suf_i,suf_j)\)

\(lcp(suf_{sa_i},suf_{sa_j})=\min\limits_{k=i+1}^jh_k\)
利用\(O(n)-O(1)\)RMQ可以做到\(O(n)-O(1)\)

\(3.\)给定\(s\),比较\(s_{l,r}\)\(s_{L,R}\)

\(lcp(suf_l,suf_r)\ge\min(r-l+1,R-L+1)\),那么说明一个串包含于另一个串,大小关系就是\(r-l+1\)\(R-L+1\)的关系。否则直接比较\(rank_l,rand_L\)即可。
时间复杂度\(O(n)-O(1)\)

\(4.\)给定字符串\(s\),求本质不同的子串个数

考虑简单容斥,答案等于总数减重复数,即\({n+1\choose 2}-\sum\limits_{i=1}^nh_i\)
时间复杂度\(O(n)\)

\(5.\)给定\(s\),求至少出现\(k\)次的最长子串

出现至少\(k\)次的子串就是\(k\)\(suf\)\(lcp\),显然取\(rank\)连续的一段是最优的。那么单调队列随便搞搞就完事了。
时间复杂度\(O(n)\)

\(6.\)给定\(s\),求不重叠的最长重复子串

先二分答案\(k\),我们知道\(lcp(sa_i,sa_j)=\min\limits_{k=i+1}^jh_k\),因此我们把\(sa\)分成若干个连续段,满足对于一段\([l,r]\),有\(\forall i\in(l,r],h_i\ge k\)。这样每个连续段中的后缀之间的\(lcp\)就会\(\ge k\)了,我们只需要查询是否存在某个连续段满足\(\max\limits_{k=l}^r sa_k-\min\limits_{k=l}^r sa_k\ge k\)即可。
利用\(O(n)-O(1)\)RMQ可以做到\(O(n\log n)\)

\(7.\)给定字符串\(s\),已知\(s=t^r(r\in\mathbb Q)\),求最大\(r\)

枚举\(t\)的长度\(l\),先判断是否有\(l\mid n\),然后是否有\(lcp(suf_1,suf_{l+1})=n-l\)
预处理出\(h\)数组中任意一个位置到\(h_{rank_1}\)的最小值即可。
时间复杂度\(O(n)\)

\(8.\)给定\(s\),求\(s\)的子串中可以表示成\(t^r(r\in Q)\)的最大\(r\)以及对应的\(t\)

枚举\(t\)的长度\(l\),首先\(t\)至少会出现一次,所以我们只考虑\(r\ge2\)的情况。
很显然这个子串至少会包含\(s_l,s_{2l},s_{3l},\cdots\)中相邻的两个。
那么我们看\(s_{il},s_{(i+1)l}\)能够向前向后匹配多远,如果匹配的长度为\(m\),这里的重复次数就是\(r=\lfloor\frac ml\rfloor+1\)
相当于我们需要支持查询两个\(suf\)\(lcp\)和两个\(pre\)\(lcs\),正序倒序跑两边SA然后\(O(n)-O(1)\)RMQ即可。
时间复杂度\(O(n\log n)\)。实际上可以用Lyndon Array做到\(O(n)\),这里就不讲了。

\(9.\)给定\(s,t\),求\(s,t\)\(lcs\)(最长公共子串)

s$t的SA,然后枚举\(h_i\),若\(sa_{i-1},s_i\)分别在s部分和t部分,那么\(h_i\)就是一个可能的\(lcs\),把所有可能的答案取个\(\max\)就好了。
时间复杂度\(O(n)\)

\(10.\)给定\(s,t\),求长度\(\ge k\)\(cs\)(公共子串)个数

这里的\(cs\)定义为\(s_{l,r}=t_{L,R}\)\((l,r,L,R)\)四元组。
先求s$t的SA,那么我们要求的就是每个在t部分的\(sa_i\)只经过\(\ge k\)\(h_j\)能够到达多少在s部分的\(sa_i\)。开两个单调栈随便搞搞就完事了。
时间复杂度\(O(n)\)

\(11.\)给定\(m\)个字符串,求在至少\(k\)个字符串中出现的最长的串

先把所有串连起来,中间插一个$得到\(t\)并求\(t\)的SA。
然后我们二分一下答案\(r\),像例题\(6\)中的做法一样把\(sa\)分成若干个连续段,然后判断是否有某一段中包含至少\(k\)个字符串部分的\(suf\),开个桶随便搞搞就完事了。
时间复杂度\(O((\sum n)\log(\sum n))\)

\(12.\)给定\(m\)个字符串,求在至少\(k\)个字符串中至少出现\(2\)次的不重叠的最长重复子串

例题\(11\)+例题\(6\)
\(s\)连起来得到\(t\)求出SA,二分答案\(r\),把\(sa\)分段,判断是否有某一段中包含了至少\(k\)个字符串部分的\(suf\)并且在这\(k\)个字符串部分的\(\max sa-\min sa\ge r\)即可。
时间复杂度\(O((\sum n)\log(\sum n))\)

\(13.\)给定\(m\)个字符串,求自己本身或者自己的反转在至少\(k\)个字符串中都出现的最长子串

把每个串和自己的反转直接接起来然后跑例题\(11\)

\(14.\)给定\(s\),求\(s\)的字典序第\(k\)小的子串

找到满足\(t=\sum\limits_{j=1}^{i-1}n+1-sa_j-h_j\)的最大的\(i\),那么这个子串就是\(s_{sa_i,sa_i+h_i+k-t-1}\)
可以通过预处理+查询时二分做到\(O(n)-O(\log n)\)

自动机

Link

posted @ 2020-01-09 15:49  Shiina_Mashiro  阅读(402)  评论(2编辑  收藏  举报