kmp到拓展kmp到manacher算法(BM算法和exkmp待改进)

这三点完全可以放这一起学,都是把原来暴力的方法优化达到线性的运算,原理不难,活用的话有点挑战,推荐刷题吧。

一、KMP(关键词:next数组,前缀,循环节)
Knuth-Morris-Pratt 字符串查找算法,简称为 “KMP算法”,常用于在一个文本串S内查找一个模式串P 的出现位置,这个算法由Donald Knuth、Vaughan Pratt、James H. Morris三人于1977年联合发表,故取这3人的姓氏命名此算法。

kmp入门资料

kmp例题

kmp原理不难理解,但其中的next数组是关键。kmp算法的主要作用在于对next数组的运用。

(1)next数组的定义:

1、设模式串T[0,m-1],(长度为m),那么next[i]表示既是是串T[0,i-1]的后缀又是串T[0,i-1]的前缀的串最长长度(不妨叫做前后缀),注意这里的前缀和后缀不包括串T[0,i-1]本身。

2、代表当前字符之前的字符串中,有多大长度的相同前缀后缀。例如如果next [j] = k,代表j 之前的字符串中有最大长度为k 的相同前缀后缀。

3、简而言之,next[i]代表了前缀和后缀的最大匹配的值。

(2)next数组的性质:

性质1:对于每一个长度len的子串,该子串的最小循环节为len-next[len]
性质2:kmp的next不断向前递归的过程可以保证对于每一个当前前缀,都有一段后缀与之对应
性质1拓展一:求字符串的前缀是否为周期串时,len%(len-next[len])==0时,循环节为len,循环次数len/(len-next[len])。(hdu1358)
性质1拓展二:不论S是不完整的循环串还是完整的循环串,len-next[len]一定是串S中去除末尾残缺部分之后,存在的最小循环节长度。

模板(因为next可能在一些编译器中会重复含义,类似max,所以下面我用Next)

const int maxn=1e5+7;
int Next[maxn];
void getnext(string a){
	int i=0,j=-1,len=a.size();
	Next[0]=-1;
	while(i<len-1){
		if(j==-1||a[i]==a[j])	Next[++i]=++j;
		else	j=Next[j];
	}
}
int kmp(string s,string p){
	int i=0,j=0;
	int N=s.size(),M=p.size();
	while(i<N){
		if(j==-1||s[i]==p[j])	i++,j++;
		else	j=Next[j];
		if(j==M)	return i-j;
	}
	return -1;
}
int main(){
	string a,b;
	//字符串a中找字符串b 
	cin>>a>>b;
	getnext(b);
	int ss=kmp(a,b);
	cout<<ss;
}

其中getnext函数还有改进版,也是对这种算法的改进版本。

void getnext(const char *p) {//前缀函数(滑步函数)
    int i = 0, j = -1;
    nextval[0] = -1;
    while(i != len)
    {
        if(j == -1 || p[i] == p[j]) //(全部不相等从新匹配 || 相等继续下次匹配)
        {
            ++i, ++j;
            if(p[i] != p[j]) //abcdabce
                nextval[i] = j;
            else //abcabca
                nextval[i] = nextval[j];
        }
        else
            j = nextval[j]; //子串移动到第nextval[j]个字符和主串相应字符比较
    }
}

但大多数情况下,第一种简单的getnext函数就够用了,第二种很少用,有时反而更容易超时。

推荐补题:hdu3336 (kmp+线性dp), hdu3746 (KMP:补齐循环节)

二、拓展kmp

(1)BM算法

KMP的匹配是从模式串的开头开始匹配的,而1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了一种新的字符串匹配算法:Boyer-Moore算法,简称BM算法。该算法从模式串的 尾部 开始匹配,且拥有在最坏情况下O(N)的时间复杂度。在实践中,比KMP算法的实际效能高。

BM算法详解

模板(待更新)


(2)strstr()函数(泪目)

学完kmp才知道,实际上,单单判断字符串str2是否是str1的子串时,KMP算法并不比最简单的c库函数strstr()快多少。

定义:strstr(str1,str2) 函数用于判断字符串str2是否是str1的子串。如果是,则该函数返回str2在str1中首次出现的地址;否则,返回NULL。

在串中查找指定字符串的第一次出现
用法:

#include 
#include 
int main(){
   char *str1 = "Borland International", *str2 = "nation", *ptr;
   ptr = strstr(str1, str2);
   printf("%s\n", ptr);
   return 0;
}

输出为:national

注意:strstr()函数注意点
strstr(str1,str2)返回值能随str1变化而变化,因为他们内容有共用地址,地址一样,输出的内容也一样。
所以在使用或者处理strstr(str1,str2)返回值之前,切记不要对str1字符串进行更改,若要更改,应该等使用完返回值后再更改。

(3)Sunday算法

上面这两个算法在最坏情况下均具有线性的查找时间。

比BM算法更快的查找算法即Sunday算法。
模板就不更新了,比较难。

三、manacher算法(关键词:最长回文子串,)

Manacher算法是查找一个字符串的最长回文子串的线性算法。相对于前面介绍的两个算法,Manacher算法的应用范围要狭窄得多。

manacher算法题目

模板:

const int maxn=2e5+7;
char a[maxn*2],sz[maxn*2];
int p[maxn*2];
void mnc(char *s){
	sz[0]='%';	sz[1]='#';
	int j=2,k=0,R=0,ans=0;
	for(int i=0;s[i];i++,j+=2){	sz[j]=s[i];	sz[j+1]='#';	}
	sz[j]=0;
	for(int i=1;sz[i];i++){
		if(i<R) p[i]=min(p[k+k-i],R-i);
		else 	p[i]=1;
		while(sz[i-p[i]]==sz[i+p[i]]) p[i]++;
		if(i+p[i]>R){	k=i;	R=i+p[i];		}
		ans=max(ans,p[i]-1);
	}
	printf("%d\n",ans);
}
int main(){
	while(~scanf("%s",a))	mnc(a);
}

以i为对称中心时,长度为ans = p[i]-1,左端点为 l = (i-p[i])/2 。右端点 r = l+ans-1 。(hdu3294)
例题:
1、求最长向中心递增回文子串(hdu4513)

const int maxn=2e5+7;
int a[maxn*2],sz[maxn*2],p[maxn*2],n,t,m;
int main(){
	scanf("%d",&t);
	while(t--){
		scanf("%d",&n);
		for(int i=0;i<n;i++)	scanf("%d",&a[i]);
		sz[0]=0;	sz[1]=-1;	m=2;
		for(int i=0;i<n;i++,m+=2){	sz[m]=a[i];	sz[m+1]=-1;	}
		sz[m]=-1;
		//防止越界
		int k=0,R=0,ans=0;
		for(int i=1;i<=m;i++){
			// 1.计算p[i]初始值
			if(i<R) p[i]=min(p[k+k-i],R-i);
			else 	p[i]=1;
			// 2.扩张p[i],以适应达到p[i]最大值
			while(sz[i-p[i]]==sz[i+p[i]]){
				if(sz[i-p[i]]==-1)	p[i]++;
				else{
					if(sz[i-p[i]]<=sz[i-p[i]+2])	p[i]++;
					else	break;
				}
			}
			// 3.更新中点k,回文半径R
			if(i+p[i]>R){	k=i;	R=i+p[i];		}
			// 4.更新最长回文
			ans=max(ans,p[i]-1);
		}
		printf("%d\n",ans);
	}
}
/*
2
3
51 52 51
4
51 52 52 51

3
4
*/

你可能感兴趣的:(字符串,算法)