有很多字符串的题可以使用 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;
}