有很多字符串的题可以使用
S
A
(
S
u
f
f
i
x
A
r
r
a
y
)
SA(Suffix Array)
SA(SuffixArray),
S
A
M
(
S
u
f
f
i
x
A
u
t
o
m
a
t
o
n
)
SAM(Suffix Automaton)
SAM(SuffixAutomaton)等高级的算法解决,但是有很多地方它们就显得不够方便,常数、复杂度也不够优了。
接下来就介绍一些指针扫描算法,时间复杂度都是
O
(
n
)
O(n)
O(n)且常数非常小。
最小表示法
我们可以把字符串拷贝一遍用后缀数组在
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)(或
d
c
3
dc3
dc3、
S
A
M
SAM
SAM的
O
(
n
)
O(n)
O(n))的复杂度内轻松解决这个问题,但是这样做法既不够简单,也不够快速,怎么办呢?
还是先把字符串拷贝一遍,然后考虑定义三个数值
i
,
j
,
k
i,j,k
i,j,k,初始时
i
=
k
=
0
,
j
=
1
i=k=0,j=1
i=k=0,j=1,接下来进行如下的操作:
1.若
s
[
i
+
k
]
=
=
s
[
j
+
k
]
s[i+k]==s[j+k]
s[i+k]==s[j+k],则
k
+
+
k++
k++;
2.若
s
[
i
+
k
]
<
s
[
j
+
k
]
s[i+k]<s[j+k]
s[i+k]<s[j+k],则
j
+
=
k
+
1
j+=k+1
j+=k+1,这是由于在
[
j
,
j
+
k
]
[j,j+k]
[j,j+k]的区间中,任选一个作为开头都可以从
[
i
,
i
+
k
]
[i,i+k]
[i,i+k]中选出一个更优的开头,再把
k
k
k置为0.
3.若
s
[
i
+
k
]
>
s
[
j
+
k
]
s[i+k]>s[j+k]
s[i+k]>s[j+k],则
i
+
=
k
+
1
,
k
=
0
i+=k+1,k=0
i+=k+1,k=0,理由同上;
4.若
i
=
=
j
i==j
i==j,则
j
+
+
j++
j++,因为两个开始指针相同了,随便把一个往右移一格就行。
最终当
i
=
n
i=n
i=n或
j
=
n
j=n
j=n或
k
=
n
k=n
k=n时终止,答案是
m
i
n
(
i
,
j
)
min(i,j)
min(i,j)。
显然每个字符最多被扫过两遍,复杂度
O
(
n
)
O(n)
O(n)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 200005;
char str[maxn];
int main(){
scanf("%s", str);
int n = strlen(str);
for(int i = n; i < n + n; i++) str[i] = str[i - n];
int i = 0, j = 1, k = 0;
while(i < n && j < n && k < n){
int t = str[i + k] - str[j + k];
if(t != 0){
if(t > 0) i += k + 1;
else j += k + 1;
if(i == j) ++j;
k = 0;
} else ++k;
}
i = min(i, j);
for(j = i; j < i + n; j++) putchar(str[j]);
return 0;
}
KMP算法
这是我们最熟悉的字符串匹配算法之一,复杂度
O
(
n
+
m
)
O(n+m)
O(n+m)。它通过计算每个位置上模式串匹配的最长前缀来解决,于是就有
n
e
x
t
[
i
]
next[i]
next[i]表示模式串长度为
i
i
i的前缀最长
b
o
r
d
e
r
border
border长度,一个
b
o
r
d
e
r
border
border指的是一个字符串能够找到相同后缀的真前缀,比如abbab中ab就是它的
b
o
r
d
e
r
border
border。
而我们可以证明,对于长度为
i
+
1
i+1
i+1的前缀,如果
n
e
x
t
[
i
]
+
1
next[i]+1
next[i]+1不是其
b
o
r
d
e
r
border
border,那么最长
b
o
r
d
e
r
border
border不可能出现在
(
n
e
x
t
[
n
e
x
t
[
i
]
]
+
1
,
n
e
x
t
[
i
]
]
(next[next[i]]+1,next[i]]
(next[next[i]]+1,next[i]],因为如果出现了,那么长度为
n
e
x
t
[
i
]
next[i]
next[i]的前缀的最长
b
o
r
d
e
r
border
border便不是
n
e
x
t
[
n
e
x
t
[
i
]
]
next[next[i]]
next[next[i]]了。同理,每次跳
n
e
x
t
next
next的时候,最长
b
o
r
d
e
r
border
border不可能出现在两次跳转的中间。并且,每次跳
n
e
x
t
next
next至少使当前指针减少1,而指针最多增加
n
n
n次,因此复杂度就是
O
(
n
)
O(n)
O(n)的。
匹配的时候也不断跳
n
e
x
t
next
next数组即可,总复杂度为
O
(
n
+
m
)
O(n+m)
O(n+m)。
下面的代码能够在
O
(
n
+
m
)
O(n+m)
O(n+m)的时间内打印出所有
T
T
T在
S
S
S中能够匹配的位置(实测字符串长度1E6时O2下70ms不到)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000005;
char S[maxn], T[maxn];
int nxt[maxn];
int main(){
scanf("%s%s", S, T);
int n = strlen(S), m = strlen(T);
nxt[0] = -1;
for(int i = 1, j = 0; i < m; i++){
while(j >= 0 && T[j] != T[i]) j = nxt[j];
nxt[i + 1] = ++j;
}
for(int i = 0, j = 0; i < n; i++){
while(j >= 0 && S[i] != T[j]) j = nxt[j];
if(++j == m) printf("%d\n", i - j + 1);
}
return 0;
}
manacher算法
m
a
n
a
c
h
e
r
manacher
manacher算法能够在
O
(
n
)
O(n)
O(n)的时间内计算出每个位置(包括空隙)为中心的最长回文串长度。
先把整个字符串每个相邻位置之间都插入一个#,开头插入$,结尾插入@(就是三个不会在题目中出现的特殊字符),这样可以减少特判,并且把长度为偶数的回文串也变成了奇数的回文串。记
l
e
n
[
i
]
len[i]
len[i]表示以
i
i
i为中心的最大回文串半径长度。
接下来从头开始扫字符串,令当前位置为
i
i
i,再定义
i
d
,
r
id,r
id,r分别表示当前右端点最大的回文串中心和右端点位置。
1.
i
≤
r
i\le r
i≤r,那么很显然,中心为
i
i
i的最长回文串长度至少是
min
(
l
e
n
[
i
d
∗
2
−
i
]
,
r
−
i
+
1
)
\min(len[id*2-i],r-i+1)
min(len[id∗2−i],r−i+1)。否则初始值为1.
2.暴力开始往后推,更新
i
d
,
r
id,r
id,r的值。
由于每个字符最多被扫过1遍,因此复杂度为
O
(
n
)
O(n)
O(n)。
下面这份代码可以输出字符串中的最长回文串长度(实测字符串长度1E7开O2下200ms不到)
#include <bits/stdc++.h>
using namespace std;
const int maxn = 25000005;
char S[maxn], T[maxn];
int len[maxn];
int main(){
fread(S, 1, maxn, stdin);
int m = strlen(S), n = 0;
T[n++] = '$';
for(int i = 0; i < m; i++){
T[n++] = '#';
T[n++] = S[i];
}
T[n++] = '#';
T[n++] = '@';
len[0] = 1;
int res = 0;
for(int i = 1, id = 0, r = 0; i < n - 1; i++){
if(r >= i) len[i] = min(r - i + 1, len[id * 2 - i]);
else len[i] = 1;
while(T[i + len[i]] == T[i - len[i]]) ++len[i];
if(i + len[i] - 1 > r) r = i + len[i] - 1, id = i;
int t = len[i] >> 1;
res = max(res, i & 1 ? t << 1 : (t << 1) - 1);
}
printf("%d\n", res);
return 0;
}
Z-function算法
Z
−
f
u
n
c
t
i
o
n
Z-function
Z−function实质上也是一种字符串匹配算法,不同的是,
K
M
P
KMP
KMP算法可以算出目标串每个位置之前能够匹配的模式串最长前缀,而
Z
−
f
u
n
c
t
i
o
n
Z-function
Z−function可以计算出目标串每个位置之后能够匹配的模式串最长前缀。
Z
−
f
u
n
c
t
i
o
n
Z-function
Z−function实际上做了一件很简单的事情,就是对于一个字符串,考虑把它和它自己分别错位1格、2格……能够匹配的最长前缀。那么我们怎么去计算它呢?
考虑错位
k
k
k格匹配的最长前缀为
l
e
n
[
k
]
len[k]
len[k],当前扫描到
i
i
i位置,再记录
i
d
,
r
id,r
id,r表示当前最长匹配前缀延伸到的最右端点为
r
r
r,是错位
i
d
id
id格时产生的(跟
m
a
n
a
c
h
e
r
manacher
manacher算法真的超级像……)。
1.
i
≤
r
i\le r
i≤r,注意到
S
[
i
d
.
.
.
r
]
=
S
[
0...
r
−
i
d
]
S[id...r]=S[0...r-id]
S[id...r]=S[0...r−id],则有
S
[
i
.
.
.
r
]
=
S
[
i
−
i
d
.
.
.
r
−
i
d
]
S[i...r]=S[i-id...r-id]
S[i...r]=S[i−id...r−id],也就是说
l
e
n
[
i
]
len[i]
len[i]的下界就是
min
(
r
−
i
+
1
,
l
e
n
[
i
−
i
d
]
\min(r-i+1,len[i-id]
min(r−i+1,len[i−id]。否则
l
e
n
[
i
]
=
0
len[i]=0
len[i]=0。
2.暴力往后推,更新
i
d
,
r
id,r
id,r的值。
这个和
m
a
n
a
c
h
e
r
manacher
manacher的复杂度是一模一样的,
O
(
n
)
O(n)
O(n)。那么接下来怎么用这个解决两个字符串的匹配问题呢?
我们可以把两个字符串接到一起,模式串在前,目标串在后,中间弄个$隔开,然后就做一遍上述过程,就可以知道了。
下述代码可以输出目标串每个位置上匹配模式串的最长前缀。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2000005;
int len[maxn], n, m;
char S[maxn], T[maxn];
int main(){
scanf("%s%s", S, T);
n = strlen(S);
m = strlen(T);
T[m++] = '$';
for(int i = 0; i < n; i++) T[m++] = S[i];
for(int i = 1, id = 0, r = 0; i < m; i++){
if(r >= i) len[i] = min(r - i + 1, len[i - id]);
while(i + len[i] < m && T[len[i]] == T[i + len[i]]) ++len[i];
if(i + len[i] - 1 > r) r = i + len[i] - 1, id = i;
}
for(int i = m - n; i < m; i++) printf("%d ", len[i]);
return 0;
}