前言
开这个坑的目的是巩固一下字符串的基础内容,毕竟自己对这一块的接触还不是很多。
其实字符串算法最大的特点就是 最大化利用已经求出的信息 ,几乎所有算法都是基于这句话的。
一些定义:
lcp ( i , j ) lcp ( i , j ) 为以 i i 开头的后缀和以 j j 开头的后缀的最长公共前缀。
σ σ 为字符集。
s i , j s i , j 为 s s 下标在 [ i , j ] [ i , j ] 之间的字串。
| S | | S | 为字符串 S S 的长度。
B max ( S i , j ) B max ( S i , j ) 为字符串 S i , j S i , j 的最长 border。
Hash
概述
Hash 就是把一个字符串映射成一个数字的过程,常见的构造函数类似于:
h a s h ( S ) = | S | ∑ i = 1 S i × B a s e | S | − i mod p h a s h ( S ) = ∑ i = 1 | S | S i × B a s e | S | − i mod p
B a s e B a s e 可以取一个小质数,p p 可以用一个大质数。如果觉得不保险还可以用双哈希,即选取两个 B a s e B a s e 和 p p 。
例题 1:「CTSC2014」企鹅QQ
题面
题意:
给定 n n 个长度为 L L 的字符串,问有多少对字符串只有一位不同。
数据范围:1 ≤ n ≤ 30000 1 ≤ n ≤ 30000 ,1 ≤ L ≤ 200 1 ≤ L ≤ 200 。
对每个前缀和后缀分别哈希,枚举哪一位不同然后统计。
最小表示法
概述
模板
题意:给定一个字符串 S S ,求一个 i ∈ [ 1 , | S | ] i ∈ [ 1 , | S | ] ,使得 S i , | S | + S 1 , i − 1 S i , | S | + S 1 , i − 1 的字典序最小。
维护两个指针 i i ,j j ,表示当前比较的两个循环表示的起点。
暴力求出 k = lcp ( i , j ) k = lcp ( i , j ) ,然后比较 S i + k S i + k 和 S j + k S j + k ,不妨假设 S i + k < S j + k S i + k < S j + k ,那么肯定 [ j , j + k ] [ j , j + k ] 范围内的所有下标都不可能成为最小循环表示的起始位置(因为以 j + x j + x 起始的循环表示字典序一定大于以 i + x i + x 起始的循环表示)。令 j ← j + k + 1 j ← j + k + 1 ,继续暴力做这个过程。
注意以上下标都是在 mod n mod n 意义下的。
每个指针只会扫一遍字符串,所以时间复杂度为 O ( | S | ) O ( | S | ) 。
代码
view code int i = 1 , j = 0 , k = 0 ;
while ( i < n && j < n && k < n)
{
if ( a[ ( i + k) % n] == a[ ( j + k) % n] ) ++ k;
else
{
if ( a[ ( i + k) % n] > a[ ( j + k) % n] ) i += k + 1 ;
else j += k + 1 ;
if ( i == j) ++ i;
k = 0 ;
}
}
Manacher
概述
Manacher 算法可以求出以每个下标为中心的最长的 长度为奇数 的回文子串。
为了求出长度为偶数的回文子串,我们可以在原串相邻两个字符中间插入一个不属于 σ σ 的字符。
记 p i p i 为以 i i 为中心的最长回文半径,当前最大的 i + p i − 1 i + p i − 1 (即已经被某个点为中心的回文串所覆盖到的最右端点) 为 m r m r ,这个最大的 i i 为 m i d m i d 。
考虑如何利用上述信息求得一个新的 p i p i 。若 i > r i > r ,那么暴力扩展求;否则有 i ≤ r i ≤ r ,根据回文的对称性,可以得出 p i = min ( m r − i + 1 , p 2 × m i d − i ) p i = min ( m r − i + 1 , p 2 × m i d − i ) 。
为什么这样是对的?考虑在 [ m i d − p m i d + 1 , m i d + p m i d − 1 ] [ m i d − p m i d + 1 , m i d + p m i d − 1 ] 的范围内,2 × m i d − i 2 × m i d − i 和 i i 对称,所以以 2 × m i d − i 2 × m i d − i 为中心的回文串同时也是以 i i 为中心的回文串。而这个性质只能在当前范围内满足,所以还要和 m r − i + 1 m r − i + 1 取 min min 。
代码
模板
view code scanf ( "%s" , s + 1 ) ;
t[ k = 1 ] = '$' , t[ ++ k] = '#' ;
n = strlen ( s + 1 ) ;
rep ( i, 1 , n) t[ ++ k] = s[ i] , t[ ++ k] = '#' ;
t[ ++ k] = '#' , t[ ++ k] = '@' ;
int mid = 0 , mr = 0 ;
rep ( i, 1 , k)
{
if ( i <= mr) p[ i] = min ( mr - i + 1 , p[ mid * 2 - i] ) ;
else p[ i] = 0 ;
while ( t[ i + p[ i] ] == t[ i - p[ i] ] ) ++ p[ i] ;
if ( i + p[ i] - 1 > mr) mr = i + p[ i] - 1 , mid = i;
}
printf ( "%d\n" , * max_element ( p + 1 , p + 1 + k) - 1 ) ;
例题 2:「THUPC2018」绿绿和串串
题面
长度为 n n 的串肯定满足条件。
对于每一个 i i ,如果存在以它为中心的回文串包括 n n 这个位置,或者 存在以它为中心且以 1 1 开头的回文串且 2 i − 1 2 i − 1 符合题目条件,那么 i i 就是一个合法答案。
Z 算法 / exKMP
概述
Z-algorithm 可以求出所有的 z i = lcp ( 1 , i ) z i = lcp ( 1 , i ) 。
类似 Manacher,每次维护最靠右的 r = l + z l − 1 r = l + z l − 1 和这个最大的 l l ,可以发现 z i ≥ min ( r − i + 1 , z i − l + 1 ) z i ≥ min ( r − i + 1 , z i − l + 1 ) 。然后和 Manacher 一样暴力扩展并更新 l , r l , r 即可。
利用 Z-algorithm 解决字符串匹配问题:将两个字符串拼接在一起,中间用一个不属于 σ σ 的字符隔开。
代码
模板
view code string s, t;
int z[ M] ;
inline void getz ( string s)
{
int l = 0 , r = 0 , n = s. size ( ) ;
rep ( i, 2 , n - 1 )
{
if ( i > r) z[ i] = 0 ;
else z[ i] = min ( r - i + 1 , z[ i - l + 1 ] ) ;
while ( s[ i + z[ i] ] == s[ 1 + z[ i] ] ) ++ z[ i] ;
if ( i + z[ i] - 1 > r) r = i + z[ i] - 1 , l = i;
}
}
int main ( )
{
cin >> s >> t;
int n = s. size ( ) , m = t. size ( ) ;
t = " " + t + "*" + s;
getz ( t) ;
LL ans = 0 ;
z[ 1 ] = m;
rep ( i, 1 , m) ans ^= ( 1ll * i * ( min ( m - i + 1 , z[ i] ) + 1 ) ) ;
printf ( "%lld\n" , ans) ;
ans = 0 ;
rep ( i, m + 2 , m + n + 1 ) ans ^= ( 1ll * ( i - m - 1 ) * ( z[ i] + 1 ) ) ;
printf ( "%lld\n" , ans) ;
return ! ! 0 ;
}
例题 3:「NOIP2020」字符串匹配
题面
题意:
T T 组数据,每次给定一个字符串 S S ,求 S = ( A B ) i C S = ( A B ) i C 的方案数,其中 F ( A ) ≤ F ( C ) F ( A ) ≤ F ( C ) ,F ( S ) F ( S ) 表示字符串 S S 中出现奇数次的字符的数量。
两种方案不同当且仅当拆分出的 A A 、B B 、C C 中有至少一个字符串不同。
数据范围:1 ≤ T ≤ 5 1 ≤ T ≤ 5 ,1 ≤ | S | ≤ 2 20 1 ≤ | S | ≤ 2 20 。
首先可以求出每个后缀 / 前缀出现奇数次的字符数量,枚举 A B A B 和 A B A B 的出现次数,用哈希暴力判断是否合法,这样就可以知道 F ( C ) F ( C ) 了,然后用一个树状数组统计合法的 A A 的个数,时间复杂度 O ( T | S | log | S | log 26 ) O ( T | S | log | S | log 26 ) ,可以得到 84 84 分。
考虑优化这个过程。先求出 S S 的 z z 函数,枚举 A B A B 的长度 i i ,那么可以扩展的次数就是 ⌊ z i + 1 i ⌋ + 1 ⌊ z i + 1 i ⌋ + 1 。
如何处理 F ( A ) ≤ F ( C ) F ( A ) ≤ F ( C ) 的限制?我们把扩展的次数按奇偶性讨论。
若 A B A B 出现了奇数次,即 k k 次,那么我们只看第 1 1 个 A B A B ,则 [ i + 1 , k i ] [ i + 1 , k i ] 中的字符出现次数的奇偶性并没有被改变(因为 k − 1 k − 1 为偶数),所以 F ( C ) = F ( S i + 1 , | S | ) F ( C ) = F ( S i + 1 , | S | ) ,可以直接算出。
若 A B A B 出现了 偶数次,那么 F ( C ) = F ( S ) F ( C ) = F ( S ) ,因为这个前缀出现的字符都出现了偶数次,不会改变奇偶性。
对于以上两种情况分别用树状数组统计合法的 A A 之后加起来即可。
代码:https://paste.ubuntu.com/p/2WSwnkhbVm/ 。
例题 4:「CF526D」Om Nom and Necklace
题面
同样枚举 A B A B 的长度 i − 1 i − 1 ,那么 A B A B 要出现奇数次的充要条件就是 z i ≥ k ( i − 1 ) z i ≥ k ( i − 1 ) ,此时能贡献到的区间就是 [ k ( i − 1 ) , min ( k i , z i + i − 1 ) ] [ k ( i − 1 ) , min ( k i , z i + i − 1 ) ] ,差分后前缀和即可。
例题 5:「CF432D」Prefixes and Suffixes
题面
求出 S S 的 z z 函数。枚举 S S 的一个长度不超过 | S | | S | 的后缀 [ i , | S | ] [ i , | S | ] ,如果 z i = | S | − i + 1 z i = | S | − i + 1 ,说明这个后缀是一个 border。
怎么统计一个 border 的出现次数?可以发现对于每个位置 i i ,以它开头的字符串对长度为 [ 1 , z i ] [ 1 , z i ] 的前缀有一次贡献,差分后做一遍后缀和即可。
代码:https://paste.ubuntu.com/p/xzCPwbhBTF/ 。
KMP
概述
KMP 一般用来解决字符串匹配问题。
KMP 的核心在于一个 n x t n x t 数组,n x t i n x t i 存储的是 [ 1 , i ] [ 1 , i ] 的最长 border(即前缀和后缀相等)。
维护两个指针 i , j i , j ,假设我们已经知道 s i − j + 1 , i s i − j + 1 , i 和 t 1 , j t 1 , j 匹配,那么肯定就有 s i − j + n x t j + 1 , i s i − j + n x t j + 1 , i 和 t 1 , n x t j t 1 , n x t j 也能匹配。因为 s i − j + 1 , i − j + n x t j = s i − n x t j + 1 , i = t 1 , n x t j = t j − n x t j + 1 , j s i − j + 1 , i − j + n x t j = s i − n x t j + 1 , i = t 1 , n x t j = t j − n x t j + 1 , j ,所以当前串 s s 的前 n x t j n x t j 个字符和串 t t 的后 n x t j n x t j 个字符相等,当 s i + 1 s i + 1 和 t j + 1 t j + 1 失配的时候就可以直接 j ← n x t j j ← n x t j 。
如何求得 n x t i n x t i ?这个可以通过 t t 串自己和自己匹配求得。具体来说,假设我们已经知道了 n x t 1 … i − 1 n x t 1 … i − 1 ,想要求 n x t i n x t i ,那么 t 1 , i t 1 , i 的最长 border 肯定是由 t 1 , i − 1 t 1 , i − 1 的一个 border(不一定是最长)在后面加上 t i t i 得到。那么我们从 j = n x t i − 1 j = n x t i − 1 开始匹配,如果失配就 j ← n x t j j ← n x t j ,直到 t j + 1 = t i t j + 1 = t i 。如果 j = 0 j = 0 就需要判断一下 t 1 t 1 是否等于 t i t i 。
注意:如果题目中对于 n x t n x t 的定义给出了若干限制,那么我们必须要先不考虑限制求出 n x t n x t ,然后再去处理限制,否则可能会导致求出的 n x t n x t 错误。
代码
模板
view code const int N = 1000003 , M = N << 1 ;
int n, m;
char s[ N] , t[ N] ;
int nxt[ N] ;
int main ( )
{
scanf ( "%s%s" , s + 1 , t + 1 ) ;
n = strlen ( s + 1 ) , m = strlen ( t + 1 ) ;
int p = 0 ;
rep ( i, 2 , m)
{
while ( p && t[ p + 1 ] != t[ i] ) p = nxt[ p] ;
if ( t[ p + 1 ] == t[ i] ) ++ p;
nxt[ i] = p;
}
p = 0 ;
rep ( i, 1 , n)
{
while ( p && t[ p + 1 ] != s[ i] ) p = nxt[ p] ;
if ( t[ p + 1 ] == s[ i] ) ++ p;
if ( p == m) printf ( "%d\n" , i - m + 1 ) , p = nxt[ p] ;
}
rep ( i, 1 , m) printf ( "%d " , nxt[ i] ) ;
return ! ! 0 ;
}
例题 6:「NOI2014」动物园
题面
先求出 n x t i n x t i ,顺便计算出 c n t i c n t i 表示 i i 要跳多少次 n x t n x t 才能变成 0 0 ,即 i i 的 border 数量。
在求 n u m i n u m i 的时候,先把指针 p p 跳到 ≤ i 2 ≤ i 2 的最大位置上,然后就有 n u m i = c n t p + [ p > 0 ] n u m i = c n t p + [ p > 0 ] ,[ p > 0 ] [ p > 0 ] 的意思是 [ 1 , p ] [ 1 , p ] 这个 border 没有在 c n t p c n t p 中统计到。
代码:https://paste.ubuntu.com/p/xw83TsXhbH/ 。
例题 7:「POI2006」PAL-Palindromes
题面
由于给出的串都是回文串,所以有结论:若 a + b a + b 是一个回文串,当且仅当它们的最短回文整周期串相同。
充分性显然。必要性考虑反证法,具体留给读者作为练习。
有了结论之后就可以先用 KMP 求出 n x t n x t 数组,若 n − n x t n | n x t n n − n x t n | n x t n 说明 n x t n n x t n 就是这个字符串的最短回文整周期串长度,否则就是它本身。开个哈希表把这些串的最短回文整周期串存下来,直接计算贡献即可。
代码:https://paste.ubuntu.com/p/d2GZS9GHMw/ 。
例题 8:「POI2012」前后缀 Prefixuffix
题面
如果两个串满足「循环等价」,那么肯定它们分别形如 A B A B 和 B A B A 。不妨假设前缀形如 A B A B ,后缀形如 B A B A 。
所有满足条件的 A A 其实就是这个串的 border,暴力跳 next 即可求出。
问题变成了怎么求一个子串 S i , n − i + 1 S i , n − i + 1 的最长 border。
这个可以考虑增量法构造。假设我们已经求出了 S i + 1 , n − i S i + 1 , n − i 的最长 border,那么肯定有 B max ( S i , n − i + 1 ) ≤ B max ( S i + 1 , n − i ) + 2 B max ( S i , n − i + 1 ) ≤ B max ( S i + 1 , n − i ) + 2 ,这是因为 S i + 1 , n − i S i + 1 , n − i 的 border 可以通过 S i , n − i + 1 S i , n − i + 1 的 border 去掉头和尾的字符得到。那么从最中间的字符开始暴力往两边扩展即可。
代码:https://loj.ac/s/1412375 。
例题 9:「POI2005」SZA-Template
题面
思路巧妙.jpg
设 d p i d p i 表示要覆盖 [ 1 , i ] [ 1 , i ] 所需要的印章长度的最小值。
考虑 d p i d p i 实际上只有两种取值:i i 和 d p n x t i d p n x t i ,因为不可能没有覆盖 n x t i n x t i 就覆盖了 i i ,不然最后会没办法填到 i i 。
问题变成在什么时候 d p i d p i 能取到 d p n x t i d p n x t i 。其实很简单,考虑印印章的过程,在印 i i 的时候肯定起点在 [ i − n x t i + 1 , i ] [ i − n x t i + 1 , i ] 中,所以只有在满足 ∃ i − n x t i ≤ j ≤ i − 1 , d p j = d p n x t i ∃ i − n x t i ≤ j ≤ i − 1 , d p j = d p n x t i 的时候 d p i = d p n x t i d p i = d p n x t i 。
代码就很好写了:https://pastebin.ubuntu.com/p/zzMsTwB6Y3/ 。
KMP 自动机
概述
KMP 自动机是一种 确定有限状态自动机 。
实质就是在 KMP 求出的 n x t n x t 数组的基础上,额外求出 t r a n s i , j t r a n s i , j 表示在 i i 之后接上字符 j j 会转移到什么状态,即:
t r a n s i , j = ⎧ ⎨ ⎩ i + 1 s i + 1 = j 0 s i + 1 ≠ j ∧ i = 0 t r a n s n x t i , j s i + 1 ≠ j ∧ i > 0 t r a n s i , j = { i + 1 s i + 1 = j 0 s i + 1 ≠ j ∧ i = 0 t r a n s n x t i , j s i + 1 ≠ j ∧ i > 0
和 KMP 的转移类似,应该很好理解。
代码
view code rep ( i, 0 , n - 1 ) rep ( j, 0 , 25 )
{
if ( j + 'a' == s[ i + 1 ] ) trans[ i] [ j] = i + 1 ;
else if ( i) trans[ i] [ j] = trans[ nxt[ i] ] [ j] ;
else trans[ i] [ j] = 0 ;
}
例题 10:「HNOI2008」GT考试
题面
对输入的字符串建出 KMP 自动机。
设 g i , j g i , j 表示现在匹配的长度为 i i ,加入一个字符后匹配长度变成 j j 的方案数,f i , j f i , j 表示已经填了 i i 个字符,匹配长度为 j j 的方案数。转移可以用矩阵乘法加速。
代码:https://paste.ubuntu.com/p/Q4ZfQ4bqQT/ 。
border 与周期理论
定理
r r 是 S S 的周期当且仅当 S S 有长度为 | S | − r | S | − r 的 border。
Weak Periodicity Lemma
内容
若 p p ,q q 是字符串 S S 的周期,且 p + q ≤ | S | p + q ≤ | S | ,则 gcd ( p , q ) gcd ( p , q ) 是 S S 的周期。
证明
不妨设 p < q p < q ,d = q − p d = q − p 。
若 i > p i > p ,则有 s i = s i − p = s i − p + q = s i + d s i = s i − p = s i − p + q = s i + d ;
否则 i ≤ p i ≤ p ,有 s i = s i + q = s i + q − p = s i + d s i = s i + q = s i + q − p = s i + d 。
这样我们就证明了 d d 是字符串 S S 的一个周期。
根据更相减损术,最终能得到 gcd ( p , q ) gcd ( p , q ) 是 S S 的一个周期。
引理
S S 所有不超过 | S | 2 | S | 2 的周期都是其最短周期的倍数。
或者等价的,S S 所有长度不小于 | S | 2 | S | 2 的 border 长度构成等差数列。
不难通过 Weak Periodicity Lemma 得出。
定理
S S 的所有 border 长度(周期)构成 O ( log n ) O ( log n ) 个值域不交的等差数列。
Periodicity Lemma
内容
若 p p ,q q 是 S S 的周期,且 p + q − gcd ( p , q ) ≤ | S | p + q − gcd ( p , q ) ≤ | S | ,则 gcd ( p , q ) gcd ( p , q ) 也是 S S 的周期。
失配树
概述
在 KMP 算法中,观察到 n x t i < i n x t i < i ,那么如果我们连一条边 ( n x t i , i ) ( n x t i , i ) ,就可以得到一棵树,我们称之为 失配树 。
性质 :失配树上每个节点的祖先都是它的一个 border。
因此,如果我们要求两个前缀 S 1 , p S 1 , p 和 S 1 , q S 1 , q 的最长公共 border 的长度,可以直接在失配树上求出 p , q p , q 两点的 LCA。注意如果 p p 和 q q 具有 祖先-后代 关系,那么应该输出深度较低的那个点的父亲,因为一个串的 border 不能是它自己。
代码
模板
view code const int N = 1000003 , M = N << 1 ;
char s[ N] ;
int n, m, nxt[ N] , fa[ 20 ] [ N] ;
int dep[ N] ;
int main ( )
{
scanf ( "%s" , s + 1 ) ;
n = strlen ( s + 1 ) ;
int p = 0 ;
fa[ 0 ] [ 1 ] = 0 ;
dep[ 0 ] = 1 ;
dep[ 1 ] = 2 ;
rep ( i, 2 , n)
{
while ( p && s[ p + 1 ] != s[ i] ) p = nxt[ p] ;
if ( s[ p + 1 ] == s[ i] ) ++ p;
nxt[ i] = p, fa[ 0 ] [ i] = p;
dep[ i] = dep[ p] + 1 ;
}
rep ( j, 1 , 19 ) rep ( i, 1 , n) fa[ j] [ i] = fa[ j - 1 ] [ fa[ j - 1 ] [ i] ] ;
m = gi < int > ( ) ;
while ( m-- )
{
int p = gi < int > ( ) , q = gi < int > ( ) ;
if ( dep[ p] < dep[ q] ) swap ( p, q) ;
per ( j, 19 , 0 ) if ( dep[ fa[ j] [ p] ] >= dep[ q] ) p = fa[ j] [ p] ;
per ( j, 19 , 0 ) if ( fa[ j] [ p] != fa[ j] [ q] ) p = fa[ j] [ p] , q = fa[ j] [ q] ;
printf ( "%d\n" , fa[ 0 ] [ p] ) ;
}
return ! ! 0 ;
}
例题 11:「BOI2009」Radio Transmission 无线传输
题面
根据周期理论那一套,一个字符串的最短周期就是 n − n x t n n − n x t n 。
输出 n − n x t n n − n x t n 即可。
例题 12:「POI2006」OKR-Periods of Words
题面
根据失配树的定义,一个字符串的最长周期就是它在根节点之下深度最小的点,在失配树上倍增跳即可。
代码:https://loj.ac/s/1415484 。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效