字符串总结(更新中)

字符串总结(更新中)

By wawawa8

简介:字符串算法总结,包含哈希,trie,KMP,AC自动机,后缀数组,后缀自动机,manacher,回文自动机等

1. 哈希

把字符串按照顺序每一位赋一个值并求和,即将字符串视为一个 n进制 的数

通过预处理前缀和以及所有的 \(n^k\) 的值,即可快速算出 \([l,r]\) 区间内的子串的哈希值

常用于判断两个子串是否相同

注意哈希可能发生冲突,即两个不同字符串的哈希值相同

比较好的方式是双取模哈希,即对两个不同的模数计算子串哈希值,极难出错(目前bzoj上似乎还没有人叉掉双模哈希)

但是由于上述方式常数较大,当字符串长度较小或字符集大小很小时常采用自然溢出,即使用 unsigned long long 保存哈希值并不手动取模(等价于对 \(2^{64}\) 取模)

有时也会采用单模数哈希(使用哪个一般看心情?)

2. trie

trie 是一个有根树形结构,其中每个节点代表一个字符串,每条边代表一个字符。

每个节点代表的字符串即为从根到这个节点的路径上的字符顺次拼成的字符串

trie 的构建十分简单,在插入一个字符串的时候,就从根出发,每次走对应字符的边,如果没有这条边就新建一条边并走下去

一般配合自动机使用,偶尔有 trie 上直接 dfsdp 的题

3. KMP

KMP 是一个字符串匹配算法(或许是简单算法中较难理解的一个?)

KMP 用于处理一个串在另一个串中作为子串出现的次数以及位置的问题,复杂度为 \(\Theta(n+m)\)

具体实现:考虑使用两个指针,分别表示当前母串和匹配串各匹配到了哪个位置,那么如果下一位相同,显然两个指针同时右移,否则匹配串的指针得向前移动

如果我们暴力寻找下一个位置,复杂度为 \(\Theta(nm)\),无法接受。但是我们发现,假设当前的匹配串的指针为 \(p\),那么我们已知了母串的后 \(p\) 位和匹配串的前 \(p\) 位,这样可以预处理。

所以我们考虑预处理匹配串的前 \(p\) 个字符匹配上了,第 \(p+1\) 个字符失配时下一次可以从匹配串的哪里开始继续匹配,即匹配串的前缀和其前 \(p\) 个字符的后缀相同的最长长度。令前述为 fail[p],不难发现 fail[p] = fail[...fail[fail[p - 1]]] 直到某个位置满足其下一位的字符正好是 s[p],如果不存在这样的位置那么 fail[p] = -1

那么我们求出了 fail 数组后匹配过程十分简单,当第 \(i\) 位失配的的时候,i = nxt[i - 1] + 1 即可

根据势能分析,其复杂度为 \(\Theta(n+m)\)

常用于单串和单串的匹配,单串和多串的匹配一般使用 AC自动机

同时,由于 fail 数组的某些性质,比如以 fail[i]i 为结尾的串存在公共后缀等,会使用 fail 数组进行某些计数问题,比如 NOI2014 动物园

4. AC自动机

对于多次询问一个串和已知的一些串之间的匹配的情况,我们使用 AC自动机

之前说了,AC自动机trie 配合使用。具体的使用方法是,首先将一些串建成 trie树,并构建 fail 边。广为流传的说法是,AC自动机 就是多串的 kmp,都需要建 fail树

5. Z-function (扩展kmp)

跟kmp的算法似乎没有什么关系,解决的问题是求S的每一个后缀和T的最长公共前缀。
设第i个字符起始的S的后缀与T的最长公共前缀长度为extend[i]。
设第i个字符起始的T的后缀与T的最长公共前缀长度为next[i]。
假设next[1...k]和extend[1...k]和当前S匹配最远处已知,可以通过next数组立即得到在k+1处以匹配多远,若超过当前最远处,则从最远处往前推。复杂度线性。
如何求next数组,next数组相当于T本身的extend可以用同样的方法求。
loyinglin的思路:扩展kmp解决的问题同样可以用后缀自动机解决,将S和T反转,rev(T)建立后缀自动机,将rev(S)在里面跑,从后往前求extend数组。需要预先dp出每个点的fail指针经过的终止符的最大长度。
扩展kmp依然存在价值因为loyinglin的思路是需要rev(S)跑rev(T)的SAM的离线算法,若改成动态增删S的最后字符,loyinglin的思路就失效了。

6. 后缀数组

后缀数组的定义很简单,sa[i] 表示排名为第 i 个的后缀是从哪个位置开始的,rnk[i] 表示从 i 开始的后缀的排名。难点在于构建,倍增易于理解,而基数排序较为复杂,大体过程如下:

1、初始化辅助数组c[i]。

2、遍历每一个输入元素,如果一个输入元素为i,则辅助数组中相应的c[i]的值加1。执行完毕之后。数组c中存储的就是各个键值在输入数组中出现的次数。

3、再计算一下前缀和,我们就知道了小于每个数的个数了。

4、将a[i]放到它在输出数组的正确位置上。对于一个值来说,c[a[i]]的值就是它在输出数组中的正确位置。在做这步的时候,把c[i]减1,这样就能处理相同值的情况了。

计数排序的时间复杂度就为O(n)O(n)了。可这有什么用呢,我们是字典序排序。

这时候,我们就可以使用倍增。因为倍增的话,每次的关键字长度都会变成它原来的两倍,而在后缀中,满足在同一个字符串中的性质,它其中有很多重叠的部分。实际上,我们可以把倍增出来的关键字变成两份,一份是上次关键字排序出来的数组,因为有重复的元素,所以对于另一半来说,它们一定是上一次排出来序的字符串的某个后缀!因为上一次已经让关键字有序了,所以我们直接把上一次排序的数组后移就可以了!前面留空的就是长度不到关键字长度的串,直接按它们的长度摆上就行了。

现在我们就得到了O(nlogn)构建后缀数组的方法了

至于其应用,过于广泛,列几个常见的:

详细的见于2009年国家集训队论文 罗穗骞 《后缀数组——处理字符串的有力工具》

子串的个数
给定一个字符串,判断其中存在多少子串(可以用字典树)

回文子串(也可用回文树)

求所有后缀之间的不相同的前缀的个数

最长回文字串

求一个后缀 和 一个反过来写的后缀 的最长公共前缀
将整个字符串反过来写在原字符串后面,中间用一个特殊的字符隔开
问题变为求这个新的字符串的某两个后缀的最长公共前缀

连续重复子串
定义::如果一个字符串 L 是由某个字符串 S 重复 R 次而得到的,则称L 是一个连续重复串。R 是这个字符串的重复次数。
步骤:
1、穷举字符串 S 的长度 k,判断其是否满足。
判断过程:
字符串 L 的长度能否被 k 整除,
suffix(1)和 suffix(k+1)的最长公共前缀是否等于 n-k

2、因为suffix(1)是固定的,预处理RMQ问题,求出height 数组中的每一个数到
height[rank[1]]之间的最小值即为最长公共前缀

3、两个字符串的相关问题
先连接这两个字符串,然后求后缀数组和height 数组,再利用 height 数组进行求解
A:aabba
B:aab
新串: aabba#aab
对新串进行SA处理

公共字串(同时出现在字符串 A 和字符串 B 中的字符串L)
给定两个字符串 A 和 B,求最长公共子串,等于求‘A的后缀’和‘B的后缀’的最长公共前缀 的最大值。
将第二个字符串写在第一个字符串后面,中间用一个未出现过的字符隔开,再求这个新的字符串的后缀数组。
求height[i]的最大值,但是要注意,只有suffix(sa[i-1])和suffix(sa[i])不是同一个字符串中的两个后缀时,height[i]才是满足条件的。

子串的个数
计算 A 的所有后缀和 B 的所有后缀之间的最长公共前缀的长度,把最长公共前缀长度不小于 k 的部分全部加起来。
步骤:
按 height 值分组,求每组中后缀之间的最长公共前缀之和
扫描一遍,每遇到一个 B 的后缀就统计与前面的 A 的后缀能产生多少个长度不小于 k 的公共子串,这里 A 的后缀需要用一个单调的栈来高效的维护。然后对 A 也这样做一次。

4、多个字符串的相关问题

先将所有的字符串连接起来,,中间用不相同的且没有出现在字符串中的字符隔开,然后求后缀数组和 height 数组,再利用 height 数组进行求解。这中间可能需要二分答案。

不小于 k 个字符串中的最长子串

通过height数组进行分组,判断每组的后缀是否出现在不小于 k 个的原串中。

每个字符串至少出现两次且不重叠的最长子串

出现或反转后出现在每个字符串中的最长子串

posted @ 2019-06-07 00:33  wawawa8  阅读(277)  评论(0编辑  收藏  举报