字符串
大多数字符串介绍算法的关键:
运用自动机的思想寻找限制条件下的状态转移函数,使得可以借助之前的状态来加速计算新的状态,比如:
- KMP算法
- Z函数(扩展KMP)
- BM算法
字符串基础
参考文献: [ 1 ] [1] [1] https://oi-wiki.org/string/basic/
字符串匹配
哈希算法、KMP算法、暴力算法 O ( n m ) O(nm) O(nm)
字符串哈希
- 思想
思想就是将不同的字符串映射为不同的哈希值,这样字符串匹配的时候直接判断字符串对应的哈希值是否一样就可以了。
- 方法
而方法通常是将长度为一个字符串
s
s
s的哈希值看成一个
b
b
b进制的数对
M
M
M取模的结果s,利用公式:
f
(
s
)
=
∑
i
=
1
j
s
[
i
]
×
b
l
−
i
(
m
o
d
M
)
f(s)=\sum_{i=1}^{j}s[i]\times b^{l-i} (mod M)
f(s)=i=1∑js[i]×bl−i(modM)
其中一般选
M
M
M为一个大质数,当
b
b
b和
M
M
M互质时,出错概率为
1
M
\frac{1}{M}
M1,可以选取两个大质数分别求以减小错误率。
例如字符串 x y z xyz xyz的哈希函数值为 x b 2 + y b + z xb^2+yb+z xb2+yb+z。
- 应用
字符串匹配,最长回文子串,确定字符串中不同子字符串的数量
字典树
- 思想
思想是该字典树的边表示字母,根结点到树上某一结点的路径就代表一个字符串,记录 δ ( u , c ) \delta(u,c) δ(u,c)表示结点 u u u的 c c c字符指向的下一个结点。
基本功能就是插入字符串和查找字符串
- 代码
struct trie {
int nex[100000][26], cnt;
bool exist[100000]; // 该结点结尾的字符串是否存在
void insert(char *s, int l) { // 插入字符串
int p = 0;
for (int i = 0; i < l; i++) {
int c = s[i] - 'a';
if (!nex[p][c]) nex[p][c] = ++cnt; // 如果没有,就添加结点
p = nex[p][c];
}
exist[p] = 1;
}
bool find(char *s, int l) { // 查找字符串
int p = 0;
for (int i = 0; i < l; i++) {
int c = s[i] - 'a';
if (!nex[p][c]) return 0;
p = nex[p][c];
}
return exist[p];
}
};
- 拓展— 01 − t r i e 01-trie 01−trie
01 − t r i e 01-trie 01−trie是指字符集为 { 0 , 1 } \{0,1\} {0,1}的 t r i e trie trie。 01 − t r i e 01-trie 01−trie可以用来维护一些数字的异或和。
前缀函数
对于一个字符串 s s s,其前缀函数 π [ i ] \pi[i] π[i]表示子串 s [ 0... i ] s[0...i] s[0...i]最长的相等的真前缀与真后缀的长度。(也可以理解为 π [ i ] \pi[i] π[i]为右端点在 i i i且同时为一个前缀的最长真子串的长度)
- 计算方法
朴素算法 O ( n 3 ) O(n^3) O(n3),优化后的算法 O ( n ) O(n) O(n)
- 举例
举例来说,对于字符串abcabcd
,
π
[
0
]
=
0
\pi[0]=0
π[0]=0,因为 a
没有真前缀和真后缀,根据规定为 0
π
[
1
]
=
0
\pi[1]=0
π[1]=0,因为 ab
无相等的真前缀和真后缀
π
[
2
]
=
0
\pi[2]=0
π[2]=0,因为 abc
无相等的真前缀和真后缀
π
[
3
]
=
1
\pi[3]=1
π[3]=1,因为 abca
只有一对相等的真前缀和真后缀a
,长度为 1
π
[
4
]
=
2
\pi[4]=2
π[4]=2,因为 abcab
相等的真前缀和真后缀只有 ab
,长度为 2
π
[
5
]
=
3
\pi[5]=3
π[5]=3,因为 abcabc
相等的真前缀和真后缀只有 abc
,长度为 3
π
[
6
]
=
0
\pi[6]=0
π[6]=0,因为 abcabcd
无相等的真前缀和真后缀
同理可以计算字符串 aabaaab
的前缀函数为
[
0
,
1
,
0
,
1
,
2
,
2
,
3
]
[0,1,0,1,2,2,3]
[0,1,0,1,2,2,3]。
- 应用
- KMP算法
假如给定一个文本 t t t和一个字符串 s s s ,我们尝试找到并展示 s s s在 t t t中的所有出现 ( o c c u r r e n c e ) (occurrence) (occurrence),假设 s s s长度为 n n n, t t t长度为 m m m,则创建字符串 s + # + t s+\#+t s+#+t,求该字符串的前缀函数,这样对于 i > n + 1 i>n+1 i>n+1的部分,其前缀函数 π [ i ] = n \pi[i]=n π[i]=n就表示字符串 s s s在字符串 t t t的 i − 2 n i-2n i−2n位置处出现了一次,遍历满足这个的 π [ i ] \pi[i] π[i]的个数就是答案。同时在这种情况下意味着只需要存储字符串 s + # s+\# s+# 以及相应的前缀函数值即可,我们可以一次读入字符串 t t t 的一个字符并计算当前位置的前缀函数值。时间: O ( n + m ) O(n+m) O(n+m),内存: O ( n ) O(n) O(n)。
-
求字符串周期
-
统计每个前缀出现次数
-
找出一个字符串中本质不同子串的数目
-
字符串压缩
假如给定一个字符串 s s s,希望找到其最短的“压缩”表示,也就是找到一个最短的字符串 t t t,使得 s s s可以被 t t t的一份或者多分拷贝的拼接表示。
显然,我们只需要找到 t t t的长度即可。知道了该长度,该问题的答案即为长度为该值的 s s s的前缀。
计算字符串 s s s的前缀函数,定义 k = n − π [ n − 1 ] k=n-\pi[n-1] k=n−π[n−1],其中 n n n为 s s s的长度, π [ n − 1 ] \pi[n-1] π[n−1]表示字符串 s s s前缀函数的最后一个值。如果 k k k能整除 n n n,那么 k k k就是答案,否则不存在一个有效的压缩,即答案为 n n n。
BM算法
B M BM BM算法是比 K M P KMP KMP算法更高效的字符串匹配算法。其基本思想是通过后缀匹配获得比前缀匹配更多的信息来实现更快的字符跳转。(而 K M P KMP KMP利用的是前缀匹配)
B M BM BM算法规则 [ 2 ] ^{[2]} [2]:
-
“坏字符规则”:后移位数=坏字符的位置-搜索词中的上一次出现的位置
注:如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -1,搜索词从0开始编号。
-
“好后缀规则”:后移位数 = 好后缀的位置 - 搜索词中的上一次出现位置
注:计算时,位置的取值以”好后缀”的最后一个字符为准。如果”好后缀”在搜索词中没有重复出现,则它的上一次出现位置为 -1,搜索词从0开始编号。
Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。
-
参考文献:
[ 2 ] [2] [2] https://blog.csdn.net/zhang0558/article/details/50187083
[ 3 ] [3] [3] https://blog.csdn.net/qpzkobe/article/details/80716922?ops_request_misc=%7B%22request%5Fid%22%3A%22161372740816780265476333%22%2C%22scm%22%3A%2220140713.130102334..%22%7D&request_id=161372740816780265476333&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~baidu_landing_v2~default-1-80716922.pc_search_result_hbase_insert&utm_term=Boyer-Moore算法
Z函数(扩展KMP)
- 定义
对于长度为 n n n的字符串 s s s,定义函数 z [ i ] z[i] z[i]表示 s s s和 s [ i , n − 1 ] s[i,n-1] s[i,n−1](即以 s [ i ] s[i] s[i]开头的后缀)的最长公共前缀( L C P LCP LCP)的长度。 z z z被称为 s s s的 z z z函数。特别地, z [ 0 ] = 0 z[0]=0 z[0]=0;同时约定:字符串下标以 0 0 0为起点。
-
Z Z Z函数求解算法
-
朴素算法 O ( n 2 ) O(n^2) O(n2)
-
线性算法 O ( n ) O(n) O(n):
思想就是:维护一个右端点最靠右的匹配段 [ l , r ] [l,r] [l,r](这里对于 i i i,我们称区间 [ i , i + z [ i ] − 1 ] [i,i+z[i]-1] [i,i+z[i]−1]是 i i i的 匹配段,也可以叫 Z-box),从 1 1 1到 n − 1 n-1 n−1顺次计算 z [ i ] z[i] z[i]的值,根据 i i i和 r r r的关系进行状态转移。
-
-
应用
- 匹配所有子串 O ( ∣ t ∣ + ∣ p ∣ ) O(|t|+|p|) O(∣t∣+∣p∣):与 K M P KMP KMP类似,创造字符串 s = p + # + t s=p+\#+t s=p+#+t,此时计算 s s s的 Z Z Z函数。
- 本质不同子串数:字符串本质不同代表字符串长度不一样或者长度相同但某些位不同
- 字符串最小整周期:给定长度为 n n n的字符串, s s s计算 s s s的 Z Z Z函数,则其最小整周期的长度为满足 i + z [ i ] = n i+z[i]=n i+z[i]=n的最小的 n n n的因数 i i i。
自动机
- O I OI OI中所说的“自动机”一般都指“确定有限状态自动机”。
- 自动机是一个对信号序列进行判定的数学模型。(可以抽象成一个有向图,自动机的每一个结点都是一个判定结点,是一个状态。)
- 形式化定义(**确定有限状态自动机(DFA)**由以下五个部分构成)
[
3
]
^{[3]}
[3]:
- 字符集( ∑ \sum ∑):该自动机只能输入这些字符。
- 状态集合( Q Q Q):如果把一个 D F A DFA DFA看成一张有向图,那么 D F A DFA DFA中的状态就相当于图上的顶点。
- 起始状态( s t a r t start start): s t a r t ∈ Q start\in Q start∈Q,是起始的状态。
- 接受状态集合( F F F): F Q ⊆ Q FQ\subseteq Q FQ⊆Q,表示可以被接受的状态的集合
- 转移函数( δ \delta δ): δ \delta δ是一个接受两个参数返回一个值的函数,其中第一个参数和返回值都是一个状态,第二个参数是字符集中的一个字符。如果把一个 D F A DFA DFA看成一张有向图,那么 D F A DFA DFA中的转移函数就相当于顶点间的边,而每条边上都有一个字符。
用一句话概括就是:自动机就是通过转移函数,对输入的信号序列进行判定和状态转移的数学模型。
- 常用的自动机
- 字典树
- K M P KMP KMP自动机
- A C AC AC自动机
- 后缀自动机
- 广义后缀自动机
- 回文自动机
- 序列自动机
参考文献: [ 4 ] [4] [4] https://oi-wiki.org/string/automaton/#_2
A C AC AC自动机
AC 自动机是以字典树( T r i e Trie Trie)的结构为基础,结合KMP的思想建立的。
- 思想
简单来说,建立一个 A C AC AC自动机有两个步骤:
- 基础的 T r i e Trie Trie结构:将所有的模式串构成一棵 T r i e Trie Trie。
-
K
M
P
KMP
KMP的思想:对
T
r
i
e
Trie
Trie树上所有的结点构造失配指针。
状态 u u u的 f a i l fail fail指针指向另一个状态 v v v,其中 v ∈ Q v\in Q v∈Q,且 v v v是 u u u的最长后缀(即在若干个后缀状态中取最长的一个作为 f a i l fail fail指针)。
-
构建 [ 5 ] ^{[5]} [5]
-
字典树构建:将若干模式串构建一颗字典树 ( T r i e ) (Trie) (Trie)
-
构建实配指针( f a i l fail fail指针):
考虑字典树中当前的结点 u u u, u u u的父结点是 p p p, p p p通过字符
c
的边指向 u u u,即 t r i e [ p , c ] = u trie[p,c]=u trie[p,c]=u。假设深度小于 u u u的所有结点的 f a i l fail fail指针都已求得。- 如果
t
r
i
e
[
f
a
i
l
[
p
]
,
c
]
trie[fail[p],c]
trie[fail[p],c]存在:则让
u
u
u的
f
a
i
l
fail
fail指针指向
t
r
i
e
[
f
a
i
l
[
p
]
,
c
]
trie[fail[p],c]
trie[fail[p],c]。相当于在
p
p
p和
f
a
i
l
[
p
]
fail[p]
fail[p]后面加一个字符
c
,分别对应 u u u和 f a i l [ u ] fail[u] fail[u]。 - 如果 t r i e [ f a i l [ p ] , c ] trie[fail[p],c] trie[fail[p],c]不存在:那么我们继续找到 t r i e [ f a i l [ f a i l [ p ] ] , c ] trie[fail[fail[p]],c] trie[fail[fail[p]],c]。重复 1 1 1的判断过程,一直跳 f a i l fail fail指针直到根结点。
- 如果真的没有,就让 f a i l fail fail指针指向根结点。
如此即完成了 f a i l [ u ] fail[u] fail[u]的构建。
- 如果
t
r
i
e
[
f
a
i
l
[
p
]
,
c
]
trie[fail[p],c]
trie[fail[p],c]存在:则让
u
u
u的
f
a
i
l
fail
fail指针指向
t
r
i
e
[
f
a
i
l
[
p
]
,
c
]
trie[fail[p],c]
trie[fail[p],c]。相当于在
p
p
p和
f
a
i
l
[
p
]
fail[p]
fail[p]后面加一个字符
-
-
例子 [ 5 ] ^{[5]} [5]
对字符串
i
he
his
she
hers
组成的字典树构建 f a i l fail fail指针:- 黄色结点:当前的结点 u u u。
- 绿色结点:表示已经 B F S BFS BFS遍历完毕的结点。
- 橙色的边: f a i l fail fail指针。
- 红色的边:当前求出的 f a i l fail fail指针。
参考文献: [ 5 ] [5] [5] https://oi-wiki.org/string/ac-automaton/
- 应用
多模匹配算法,能在一个文本串中同时查找多个不同的模式串,是 K M P KMP KMP的升级版。( K M P KMP KMP是单模匹配算法,处理在一个文本串中查找一个模式串的问题)
后缀数组( S A SA SA)
后缀数组( S u f f i x A r r a y Suffix Array SuffixArray)主要是两个数组: s a sa sa和 r k rk rk。
其中, s a [ i ] sa[i] sa[i]表示将所有后缀排序后第 i i i小的后缀的编号; r k [ i ] rk[i] rk[i]表示后缀 i i i的排名。因此,这两个数组满足性质: s a [ r k [ i ] ] = r k [ s a [ i ] ] = i sa[rk[i]]=rk[sa[i]]=i sa[rk[i]]=rk[sa[i]]=i。
- 方法
- O ( n 2 l o g n ) O(n^2logn) O(n2logn):普通方法, S t r i n g + s o r t String+sort String+sort。
- O ( n l o g 2 n ) O(nlog^2n) O(nlog2n):倍增思想,利用 s o r t ( n l o g n ) sort(nlogn) sort(nlogn)进行排序。
-
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn):倍增思想,利用计数排序、基数排序进行优化排序
- 计数排序 [ 6 ] ^{[6]} [6]: ( O ( n ) ) (O(n)) (O(n)),该排序算法不是基于元素比较,而是利用下标来确定元素的正确位置,适用于取值范围不大的情况。两种不适用的情况:一是当数列最大值和最小值差距过大;二是当数列元素不是整数。
- 基数排序: ( O ( k n ) ) (O(kn)) (O(kn))( k k k表示分成的关键词的个数,对于数来说就是最大的数的位数)对元素进行“分类” ,先看最低位,进行排序,之后看到时第二位进行排序,一直到最高位,排序后就是最终的排序。
- O ( n ) O(n) O(n):诱导排序与 SA-IS 算法。
参考文献:
[ 6 ] [6] [6] https://www.sohu.com/a/258222713_684445
-
应用
-
寻找最小的循环移动位置:将字符串 S S S复制一份变成 S S SS SS就转化成了后缀排序问题。
-
在字符串中找子串:
任务是:在线地在主串 T T T中寻找模式串 S S S,也就是,预先知道主串 T T T,但是当且仅当询问时才知道模式串 S S S。
步骤:先构造主串 T T T的后缀数组,然后查找子串 S S S。这里假设子串 S S S长度为 n n n,由于子串 S S S在 T T T中出现,它必定是 T T T的一些后缀的前缀,又由于 s a [ i ] sa[i] sa[i]中根据 i i i从 1 1 1到 n n n, s a [ i ] sa[i] sa[i]表示的就是从小到大的主串 T T T的后缀字符串的第一个字符的位置,因此,利用二分法二分 s a [ i ] sa[i] sa[i](时间复杂度 O ( ∣ T ∣ ) O(|T|) O(∣T∣)),比较子串 s s s与当前后缀的大小(时间复杂度 O ( n ) O(n) O(n)),因此总的时间复杂度为 O ( n l o g ∣ T ∣ ) O(nlog|T|) O(nlog∣T∣)。注意,如果该子串在 T T T中出现了多次,每次出现都是在 s a sa sa数组中相邻的。因此出现次数可以通过再次二分找到,输出每次出现的位置也很轻松。
-
从字符串首尾取字符最小化字典序
-
-
h e i g h t height height数组
-
L C P LCP LCP(最长公共前缀):两个字符串 S S S和 T T T的 L C P LCP LCP,就是最大的 x x x( x ≤ m i n ( ∣ S ∣ , ∣ T ∣ ) x\leq min(|S|,|T|) x≤min(∣S∣,∣T∣))使得 S i = T i ( ∀ 1 ≤ i ≤ x ) S_i = T_i (\forall 1 \leq i \leq x) Si=Ti(∀1≤i≤x)。后面以 l c p ( i , j ) lcp(i,j) lcp(i,j)表示后缀 i i i和后缀 j j j的最长公共前缀(的长度)。
-
h e i g h t height height数组的定义: h e i g h t [ i ] = l c p ( s a [ i ] , s a [ i − 1 ] ) height[i] = lcp(sa[i],sa[i-1]) height[i]=lcp(sa[i],sa[i−1]),即第 i i i名的后缀与它前一名的后缀的最长公共前缀。 h e i g h t [ 1 ] height[1] height[1]可以视作 0 0 0。
-
求 h e i g h t height height数组: O ( n ) O(n) O(n)。
-
应用
-
求两子串的最长公共前缀: l c p ( s a [ i ] , s a [ j ] ) = m i n { h e i g h t [ i + 1.. j ] } lcp(sa[i],sa[j])=min\{height[i+1..j]\} lcp(sa[i],sa[j])=min{height[i+1..j]}。
-
比较一个字符串的两个子串的大小关系:
假设需要比较的是 A = S [ a . . b ] A=S[a..b] A=S[a..b]和 B = S [ c . . d ] B=S[c..d] B=S[c..d]的大小关系。若 l c p ( a , c ) ≥ m i n ( ∣ A ∣ , ∣ B ∣ ) , A < B ⟺ ∣ A ∣ < ∣ B ∣ lcp(a,c)\geq min(|A|,|B|),A<B \Longleftrightarrow |A|<|B| lcp(a,c)≥min(∣A∣,∣B∣),A<B⟺∣A∣<∣B∣。否则, A < B ⟺ r k [ a ] < r k [ c ] A<B \Longleftrightarrow rk[a]<rk[c] A<B⟺rk[a]<rk[c]。
-
不同子串的数目: n ( n + 1 ) 2 − ∑ i = 2 n h e i g h t [ i ] \frac{n(n+1)}{2}-\sum_{i=2}^{n}{height[i]} 2n(n+1)−∑i=2nheight[i]。
-
等等
-
-
后缀自动机( S A M SAM SAM)
后缀自动机 ( s u f f i x a u t o m a t o n , S A M ) (suffix \ automaton, SAM) (suffix automaton,SAM)是用于处理单个字符串的子串问题的强力工具
-
定义
字符串 s s s的 S A M SAM SAM是一个接受 s s s的所有后缀的最小DFA(确定性有限自动机或确定性有限状态自动机)。
换句话说:
- S A M SAM SAM是一张有向无环图。结点被称作状态,边被称作状态间的转移。
- 图存在一个源点 t 0 t_0 t0,称作初始状态,其它各结点均可从 t 0 t_0 t0出发到达。
- 每个转移都标有一些字母。从一个结点出发的所有转移均不同。
- 存在一个或多个终止状态。如果我们从初始状态 t 0 t_0 t0出发,最终转移到了一个终止状态,则路径上的所有转移连接起来一定是字符串 s s s的一个后缀。 s s s的每个后缀均可用一条从 t 0 t_0 t0到某个终止状态的路径构成。
- 在所有满足上述条件的自动机中, S A M SAM SAM的结点数是最少的。
SAM 最简单、也最重要的性质是,它包含关于字符串 s s s的所有子串的信息。
- 举例
在这里展示一些简单的字符串的后缀自动机。用蓝色表示初始状态,用绿色表示终止状态。
-
性质
- 状态数:对于一个长度为 n n n的字符串 s s s,它的 S A M SAM SAM中的状态数不会超过 2 n − 1 2n-1 2n−1(假设 n ≥ 2 n\geq 2 n≥2)。
- 转移数:对于一个长度为 n n n的字符串 s s s,它的 S A M SAM SAM中的转移数不会超过 3 n − 4 3n-4 3n−4(假设 n ≥ 3 n\geq 3 n≥3)。
-
应用
-
检查字符串是否出现
给一个文本串 T T T和多个模式串 P P P我们要检查字符串 P P P是否作为 T T T的一个子串出现
我们在 O ( ∣ T ∣ ) O(|T|) O(∣T∣)的时间内对文本串 T T T构造后缀自动机。为了检查模式串 P P P是否在 T T T中出现,我们沿转移(边)从 t 0 t_0 t0开始根据 P P P的字符进行转移。如果在某个点无法转移下去,则模式串 P P P不是 T T T的一个子串。如果我们能够这样处理完整个字符串 P P P,那么模式串在 T T T中出现过。
对于每个字符串 P P P,算法的时间复杂度为 O ( ∣ P ∣ ) O(|P|) O(∣P∣)。此外,这个算法还找到了模式串 P P P在文本串中出现的最大前缀长度。
-
不同子串个数
给一个字符串 S S S,计算不同子串的个数。
对字符串 S S S构造后缀自动机。每个 S S S的子串都相当于自动机中的一些路径,因此不同子串的个数等于自动机中以 t 0 t_0 t0为起点的不同路径的条数。考虑到 S A M SAM SAM为有向无环图,不同路径的条数可以通过动态规划计算。即令 d v d_v dv为从状态 v v v开始的路径数量(包括长度为零的路径),则我们有如下递推方程:
d v = 1 + ∑ w : ( v , w , c ) ∈ D A W G d w d_v = 1 + \sum_{w:(v,w,c)\in DAWG}{d_w} dv=1+w:(v,w,c)∈DAWG∑dw
即, d v d_v dv可以表示为所有 v v v的转移的末端的和。所以不同字串的个数为 d t 0 − 1 d_{t_0}-1 dt0−1(因为要去掉空子串)。
总时间复杂度为: O ( ∣ S ∣ ) O(|S|) O(∣S∣)。
-
计算所有不同子串的长度
-
最小循环移位
给定一个字符串 S S S。找出字典序最小的循环移位。
容易发现字符串 S + S S+S S+S包含字符串 S S S的所有循环移位作为子串。
所以问题简化为在 S + S S+S S+S对应的后缀自动机上寻找最小的长度为 ∣ S ∣ |S| ∣S∣的路径,这可以通过平凡的方法做到:我们从初始状态开始,贪心地访问最小的字符即可。
总的时间复杂度为 O ( ∣ S ∣ ) O(|S|) O(∣S∣)。
-
广义后缀自动机
后缀自动机 ( s u f f i x a u t o m a t o n , S A M ) (suffix automaton, SAM) (suffixautomaton,SAM)是用于处理单个字符串的子串问题的强力工具
而广义后缀自动机 ( G e n e r a l S u f f i x A u t o m a t o n ) (General Suffix Automaton) (GeneralSuffixAutomaton)则是将后缀自动机整合到字典树中来解决对于多个字符串的子串问题。
- 常见的为广义后缀自动机:通过用特殊符号将多个串直接连接后,再建立 S A M SAM SAM。(实现方式简单,而且在面对题目时通常可以达到和广义后缀自动机一样的正确性,但其时间复杂度较为危险。)
- 应用
- 所有字符中的不同字串个数。
- 多个字符串间的最长公共子串。
后缀树
- 定义
后缀树,就是包含一则字符串所有后缀的压缩 T r i e Trie Trie。
- 构建过程
- 根据文本 T e x t Text Text生成所有后缀的集合。
- 将每个后缀作为一个单独的关键词,构建一颗压缩字典树 [ 7 ] ^{[7]} [7]( C o m p r e s s e d T r i e Compressed Trie CompressedTrie)。
参考文献: [ 7 ] [7] [7] https://www.cnblogs.com/gaochundong/p/suffix_tree.html
M a n a c h e r Manacher Manacher
- 题目描述
给定一个长度为 n n n的字符串 s s s,找出所有对 ( i , j ) (i,j) (i,j)使得子串 s [ i . . . j ] s[i...j] s[i...j]为一个回文串。
-
解决思路
-
首先定义两个数组 d 1 [ i ] , d 2 [ i ] d_{1}[i],d_{2}[i] d1[i],d2[i]分别表示以位置 i i i为中心的长度为奇数和长度为偶数的回文串个数。换个角度来讲,二者也表示了以位置 i i i为中心的最长回文串的半径长度(半径长度 d 1 [ i ] , d 2 [ i ] d_{1}[i],d_{2}[i] d1[i],d2[i]均为从位置 i i i到回文串最右端位置包含的字符个数)。
举例:
-
对于字符串 s = a b a b a b c s=abababc s=abababc,以 s [ 3 ] = b s[3]=b s[3]=b为中心有三个奇数长度的回文串,最长回文串半径为 3 3 3,也即 d 1 [ 3 ] = 3 d_{1}[3]=3 d1[3]=3:
-
对于字符串 s = c b a a b d s=cbaabd s=cbaabd,以 s [ 3 ] = a s[3]=a s[3]=a为中心有两个偶数长度的回文串,最长回文串半径为 2 2 2,也即 d 2 [ 3 ] = 2 d_{2}[3]=2 d2[3]=2:
-
-
之后,关键就是如何求这两个数组。 M a n a c h e r Manacher Manacher算法可以用线性的时间复杂度 ( O ( n ) ) (O(n)) (O(n))以及很小的空间复杂度求出。
-
-
M a n a c h e r [ 8 ] Manacher^{[8]} Manacher[8]
算法参考文献中算法部分写的很清晰,很详细,参考文献: [ 8 ] [8] [8] https://oi-wiki.org/string/manacher/
-
复杂度 ( O ( n ) ) (O(n)) (O(n))
-
算法实现 [ 8 ] ^{[8]} [8]
-
计算 d 1 [ ] d_{1}[] d1[]
vector<int> d1(n); for (int i = 0, l = 0, r = -1; i < n; i++) { int k = (i > r) ? 1 : min(d1[l + r - i], r - i); while (0 <= i - k && i + k < n && s[i - k] == s[i + k]) { k++; } d1[i] = k--; if (i + k > r) { l = i - k; r = i + k; } }
-
计算 d 2 [ ] d_{2}[] d2[]
vector<int> d2(n); for (int i = 0, l = 0, r = -1; i < n; i++) { int k = (i > r) ? 0 : min(d2[l + r - i + 1], r - i + 1); while (0 <= i - k - 1 && i + k < n && s[i - k - 1] == s[i + k]) { k++; } d2[i] = k--; if (i + k > r) { l = i - k - 1; r = i + k; } }
-
也可以将 d 1 [ ] , d 2 [ ] d_{1}[],d_{2}[] d1[],d2[]的计算统一为的一个 d 1 [ ] d_{1}[] d1[]计算
给定一个长度为 n n n的字符串 s s s,我们在其 n + 1 n+1 n+1个空中插入分隔符 # \# #,从而构造一个长度为 2 n + 1 2n+1 2n+1的字符串 s ′ s' s′。举例来说,对于字符串 s = a b a b a b c s=abababc s=abababc,其对应的 s ′ = # a # b # a # b # a # b # c # s'=\#a\#b\#a\#b\#a\#b\#c\# s′=#a#b#a#b#a#b#c#,对 s ′ s' s′计算 d 1 [ ] d_{1}[] d1[]。
容易证明 [ 8 ] ^{[8]} [8](见参考文献 [ 8 ] [8] [8]):在 s ′ s' s′中, d 1 [ i ] d_{1}[i] d1[i]表示在 s s s中以对应位置为中心的极大子回文串的总长度加一。
-
回文树 ( P a l i n d r o m i c T r e e ) (Palindromic\ Tree) (Palindromic Tree)
- 定义
回文树(也被称为回文自动机)是一种可以存储一个串中所有回文子串的高效数据结构。
- 结构
回文树大概长这样:
回文树也是由转移边和后缀链接( f a i l fail fail指针)组成。其中,一个节点的 f a i l fail fail指针(图中的虚线)指向的是这个节点所代表的回文串的最长回文后缀所对应的节点,但是转移边(图中的实线)并非代表在原节点代表的回文串后加一个字符,而是表示在原节点代表的回文串前后各加一个相同的字符(不难理解,因为要保证存的是回文串)。
因为回文串长度分为奇数和偶数,因此最方便的是建两棵树,一棵树中的节点对应的回文子串长度均为奇数,另一棵树中的节点对应的回文子串长度均为偶数。
-
应用
-
求本质不同回文子串个数:一个串的本质不同回文子串个数等于回文树的状态数(排除奇根和偶根两个状态)。
-
求回文子串出现次数:由于回文树的构造过程中,节点本身就是按照 拓扑序 ( 1 ) ^{(1)} (1)插入,因此只需要逆序枚举所有状态,将当前状态的出现次数加到其 fail 指针对应状态的出现次数上即可。
注:(1)拓扑排序:对一个有向无环图 ( D i r e c t e d A c y c l i c G r a p h (Directed Acyclic Graph (DirectedAcyclicGraph简称 D A G ) G DAG)G DAG)G进行拓扑排序,是将 G G G中所有顶点排成一个线性序列,使得图中任意一对顶点 u u u和 v v v,若边 < u , v > ∈ E ( G ) <u,v>\in E(G) <u,v>∈E(G),则 u u u在线性序列中出现在 v v v之前。通常,这样的线性序列称为满足拓扑次序 ( T o p o l o g i c a l O r d e r ) (Topological Order) (TopologicalOrder)的序列,简称拓扑序列。实质就是将有向图的顶点排成一个线性序列
-
最小回文划分
-
序列自动机
序列自动机是接受且仅接受一个字符串的子序列的自动机。
最小表示法
最小表示法是用于解决字符串最小表示问题的方法。
-
循环同构
当字符串 S S S可以选定一个位置 i i i满足:
S [ i . . . n ] + S [ 1... i − 1 ] = T S[i...n]+S[1...i-1]=T S[i...n]+S[1...i−1]=T
则称 S S S与 T T T循环同构。 -
最小表示
字符串 S S S的最小表示为与 S S S循环同构的所有字符串中字典序最小的字符串。
-
算法流程
- 初始化指着 i i i为 0 0 0, j j j为 1 1 1;初始化匹配长度 k k k为 0 0 0。
- 比较第 k k k位的大小,根据比较结果跳转相应指针,若跳转后两个指针相同,则随意选一个加一以保证比较的连个字符串不同。
- 重复上述过程,直到比较结束。
- 答案为 i i i, j j j中较小的一个。
-
代码
int k = 0, i = 0, j = 1;
while (k < n && i < n && j < n) {
if (sec[(i + k) % n] == sec[(j + k) % n]) {
k++;
} else {
sec[(i + k) % n] > sec[(j + k) % n] ? i = i + k + 1 : j = j + k + 1;
if (i == j) i++;
k = 0;
}
}
i = min(i, j);
L y n d o n Lyndon Lyndon分解
- 定义
L y n d o n Lyndon Lyndon串:对于字符串 s s s,如果 s s s的字典序严格小于 s s s的所有后缀的字典序,我们称 s s s是简单串,或者Lyndon串。
L y n d o n Lyndon Lyndon分解:串 s s s的 L y n d o n Lyndon Lyndon分解记为 s = w 1 w 2 ⋯ w k s=w_{1}w_{2}\cdots w_{k} s=w1w2⋯wk,其中所有的 w i w_{i} wi为简单串,并且他们的字典序按照非严格单减排序,即 w 1 ≥ w 2 ≥ ⋯ ≥ w k w_{1}\geq w_{2}\geq \cdots \geq w_{k} w1≥w2≥⋯≥wk。可以发现,这样的分解存在且唯一。
- D u v a l Duval Duval算法
D u v a l Duval Duval算法可以在 O ( n ) O(n) O(n)的时间内求出一个串的 L y n d o n Lyndon Lyndon分解。