O(n)字符串指针算法总结(最小表示,KMP,manacher,Z-function)

有很多字符串的题可以使用 S A ( S u f f i x A r r a y ) SA(Suffix Array) SASuffixArray S A M ( S u f f i x A u t o m a t o n ) SAM(Suffix Automaton) SAMSuffixAutomaton等高级的算法解决,但是有很多地方它们就显得不够方便,常数、复杂度也不够优了。
接下来就介绍一些指针扫描算法,时间复杂度都是 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 ] &lt; s [ j + k ] s[i+k]&lt;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 ] &gt; s [ j + k ] s[i+k]&gt;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 ir,那么很显然,中心为 i i i的最长回文串长度至少是 min ⁡ ( l e n [ i d ∗ 2 − i ] , r − i + 1 ) \min(len[id*2-i],r-i+1) min(len[id2i],ri+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 Zfunction实质上也是一种字符串匹配算法,不同的是, K M P KMP KMP算法可以算出目标串每个位置之前能够匹配的模式串最长前缀,而 Z − f u n c t i o n Z-function Zfunction可以计算出目标串每个位置之后能够匹配的模式串最长前缀。
Z − f u n c t i o n Z-function Zfunction实际上做了一件很简单的事情,就是对于一个字符串,考虑把它和它自己分别错位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 ir,注意到 S [ i d . . . r ] = S [ 0… r − i d ] S[id…r]=S[0…r-id] S[id...r]=S[0...rid],则有 S [ i . . . r ] = S [ i − i d . . . r − i d ] S[i…r]=S[i-id…r-id] S[i...r]=S[iid...rid],也就是说 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(ri+1,len[iid]。否则 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;
}
    原文作者:KMP算法
    原文地址: https://blog.csdn.net/WAautomaton/article/details/83003397
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞