有很多字符串的题可以使用SA(SuffixArray)SA(Suffix Array)SA(SuffixArray),SAM(SuffixAutomaton)SAM(Suffix Automaton)SAM(SuffixAutomaton)等高级的算法解决,但是有很多地方它们就显得不够方便,常数、复杂度也不够优了。
接下来就介绍一些指针扫描算法,时间复杂度都是O(n)O(n)O(n)且常数非常小。
最小表示法
我们可以把字符串拷贝一遍用后缀数组在O(nlogn)O(nlogn)O(nlogn)(或dc3dc3dc3、SAMSAMSAM的O(n)O(n)O(n))的复杂度内轻松解决这个问题,但是这样做法既不够简单,也不够快速,怎么办呢?
还是先把字符串拷贝一遍,然后考虑定义三个数值i,j,ki,j,ki,j,k,初始时i=k=0,j=1i=k=0,j=1i=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+1j+=k+1j+=k+1,这是由于在[j,j+k][j,j+k][j,j+k]的区间中,任选一个作为开头都可以从[i,i+k][i,i+k][i,i+k]中选出一个更优的开头,再把kkk置为0.
3.若s[i+k]>s[j+k]s[i+k]>s[j+k]s[i+k]>s[j+k],则i+=k+1,k=0i+=k+1,k=0i+=k+1,k=0,理由同上;
4.若i==ji==ji==j,则j++j++j++,因为两个开始指针相同了,随便把一个往右移一格就行。
最终当i=ni=ni=n或j=nj=nj=n或k=nk=nk=n时终止,答案是min(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)。它通过计算每个位置上模式串匹配的最长前缀来解决,于是就有next[i]next[i]next[i]表示模式串长度为iii的前缀最长borderborderborder长度,一个borderborderborder指的是一个字符串能够找到相同后缀的真前缀,比如abbab中ab就是它的borderborderborder。
而我们可以证明,对于长度为i+1i+1i+1的前缀,如果next[i]+1next[i]+1next[i]+1不是其borderborderborder,那么最长borderborderborder不可能出现在(next[next[i]]+1,next[i]](next[next[i]]+1,next[i]](next[next[i]]+1,next[i]],因为如果出现了,那么长度为next[i]next[i]next[i]的前缀的最长borderborderborder便不是next[next[i]]next[next[i]]next[next[i]]了。同理,每次跳nextnextnext的时候,最长borderborderborder不可能出现在两次跳转的中间。并且,每次跳nextnextnext至少使当前指针减少1,而指针最多增加nnn次,因此复杂度就是O(n)O(n)O(n)的。
匹配的时候也不断跳nextnextnext数组即可,总复杂度为O(n+m)O(n+m)O(n+m)。
下面的代码能够在O(n+m)O(n+m)O(n+m)的时间内打印出所有TTT在SSS中能够匹配的位置(实测字符串长度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算法
manachermanachermanacher算法能够在O(n)O(n)O(n)的时间内计算出每个位置(包括空隙)为中心的最长回文串长度。
先把整个字符串每个相邻位置之间都插入一个#,开头插入$,结尾插入@(就是三个不会在题目中出现的特殊字符),这样可以减少特判,并且把长度为偶数的回文串也变成了奇数的回文串。记len[i]len[i]len[i]表示以iii为中心的最大回文串半径长度。
接下来从头开始扫字符串,令当前位置为iii,再定义id,rid,rid,r分别表示当前右端点最大的回文串中心和右端点位置。
1.i≤ri\le ri≤r,那么很显然,中心为iii的最长回文串长度至少是min(len[id∗2−i],r−i+1)\min(len[id*2-i],r-i+1)min(len[id∗2−i],r−i+1)。否则初始值为1.
2.暴力开始往后推,更新id,rid,rid,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−functionZ-functionZ−function实质上也是一种字符串匹配算法,不同的是,KMPKMPKMP算法可以算出目标串每个位置之前能够匹配的模式串最长前缀,而Z−functionZ-functionZ−function可以计算出目标串每个位置之后能够匹配的模式串最长前缀。
Z−functionZ-functionZ−function实际上做了一件很简单的事情,就是对于一个字符串,考虑把它和它自己分别错位1格、2格……能够匹配的最长前缀。那么我们怎么去计算它呢?
考虑错位kkk格匹配的最长前缀为len[k]len[k]len[k],当前扫描到iii位置,再记录id,rid,rid,r表示当前最长匹配前缀延伸到的最右端点为rrr,是错位ididid格时产生的(跟manachermanachermanacher算法真的超级像……)。
1.i≤ri\le ri≤r,注意到S[id...r]=S[0...r−id]S[id...r]=S[0...r-id]S[id...r]=S[0...r−id],则有S[i...r]=S[i−id...r−id]S[i...r]=S[i-id...r-id]S[i...r]=S[i−id...r−id],也就是说len[i]len[i]len[i]的下界就是min(r−i+1,len[i−id]\min(r-i+1,len[i-id]min(r−i+1,len[i−id]。否则len[i]=0len[i]=0len[i]=0。
2.暴力往后推,更新id,rid,rid,r的值。
这个和manachermanachermanacher的复杂度是一模一样的,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;
}