浅谈kmp,manacher

本人字符串方向萌新,刚学了题目中三个算法,就想写博客来总结记忆一下,之后也可以复习.

kmp

kmp算法,具体包含两步操作,第一步就是预处理next数组,第二部就是进行kmp匹配,先不说预处理next数组,举个例子,我们要求一个长串中是否包含一个短串,首先我们理解一下匹配的过程:

一般的暴力算法(bf)我们的过程都是先把文本串(需要去匹配的串,长串)和模式串(和文本串进行匹配的串,短串)直接进行匹配,第一个字符和第一个字符,第二个字符和第二个字符,两个串一直匹配到失配,也就是它们的第n个元素不相等,在暴力匹配法中我们都是直接把模式串往后移动一位,将模式串的第一位和文本串的第二位进行匹配,就以此循环,一直到某次直接把模式串匹配完了,那么就完成了,但是,在追求效率的acm江湖,暴力永远不会过时,但是太过暴力就会超时.那么聪明的科学Knuth,Morris,Pratt三人就想出了一种算法来,向他们敬礼,respect,为了纪念他们,就取他们名字首字母,kmp算法就诞生了.

其实kmp算法进行匹配的时候和bf类似,不同就在于失配的时候,bf默认只会退一格,而它则是退预先处理好的next数组中的步数.那么先对next数组进行理解:

next数组的实质其实是模式串当前位置的最长前缀与最长后缀相等的长度,因为在kmp算法中,我们在没有失配之前进行匹配匹配成功的长度都是属于模式串的一部分.举个例子,模式串abaab,next值分别是-1,0,1,1,2.他们所代表的意义如下:

next[1]=-1:长度为1时,匹配的模式串为a,不记录

next[2]=0:长度为2事,匹配串为ab,前后缀不相等

next[3]=1:长度为3时,匹配串为aba,最长相等的前后缀(不算它本身)为a.

next[4]=1:长度为4时,匹配串为abaa,最长相等的前后缀(不算它本身)为a.

next[5]=2,长度为5时,匹配串为abaab,最长相等的前后缀为ab,长度为2.

比如文本串abaabaa和模式串ababa,他们匹配成功的部分就是aba,这里的aba都是模式串ababa的一部分,当进行到下一步匹配时,因为会失配,那么就要移动到next[aba的长度]的位置,为什么呢,因为next数组记录的就是在该长度下,模式串最长前缀和最长后缀的匹配长度,按照例子也就是:

失配直接将匹配成功的文本串的最长的和模式串的最长的前缀匹配的后缀进行匹配移动.

T为文本串,S为模式串,我们如图移动,就可以省去s中的a与t中b的匹配,并且已经把它们两串的长度为3时的前缀已经匹配好了,就可以直接从t的第四位与s的b来匹配.这是因为直接移动next[len(长度)]可以直接让模式串的最长前缀与被匹配成功的部分的文本串的后缀直接进行匹配,它首先剪枝减去了不会匹配成功的匹配,再者省去了匹配后缀成功的时间.

进行next预处理的地方和kmp类似,因为都已经处理了之前匹配成功长度的next值,next预处理就是模式串和模式串自己匹配,kmp就是模式串和文本串匹配.上代码:

void getnex()
{
    int i=0,j=-1;
    nex[i]=j;
    len=strlen(t);
    while(i<len)
    {
        if(t[i]==t[j]||j==-1)
        {
            i++;
            j++;
            nex[i]=j;//记录在该位置的next
        }
        else
        {
            j=nex[j];//进行最长前缀最长后缀的匹配移动
        }
    }
    return;
}

int kmp()
{
    int i=0,j=0;
    len=strlen(t);
    len1=strlen(s);
    while(i<len1)
    {
        if(s[i]==t[j]||j==-1)
        {
            i++;
            j++;
        }
        else
        {
            j=nex[j];
        }
        if(j==len)return 1;//模式串匹配完了,说明存在模式串在文本串中
    }
    return 0;
}

manacher

manacher(马拉车)算法,是求一个字符串中最长的回文串的长度(当然也可以活用直接把这个串求出来).我们知道,回文字符串就是从前往后和从后往前都是一个样的字符串.但是由于分为两种,一种是长度为奇数,以字符为对称轴对称的字符串,例如abcba.另外一种就是以中间的空隙(字符之间的空隙)为对称轴对称的字符串,例如abccba.因为manacher算法涉及到记录对称轴,记录回文长度,所以我们需要对原字符串进行一些特殊的处理,把原字符串的空隙间加上同一种符号,在开头加一个特殊的开头符号.符号可以自己随便选.例如aba就可以处理为@#a#b#a#,abba可以变为@#a#b#b#a#.这样就可以保证找到回文的对称轴,而且从对称轴之后存在于该回文的字符的个数,就是原回文的长度,例如:aba对称轴是b,那么它之后就是#a#,长度就是3,而abba变形后对称轴为#,它之后是b#a#,长度就为4.

那么进行了预处理,就可以字符串进行处理了.

manacher的思路,其实就是找到一个目前为止右端能到达的最右端最长的回文串,然后再分情况考虑来进行寻找回文串的算法.id为当前这个回文串的对称轴,mx为回文长度.分二种情况:

1.当前遍历的位置i>=mx,此时表明还没有匹配新的末端在最右的回文串,那我们就进行回文串匹配.初始化该处回文串长度为1,在进行回文串匹配.

2.当i<mx时,此时可以确定i在id之后,mx之前,又可以根据回文串的性质,i的会问长度len[i]就等于i关于id对称的那个点的回文长度,该点的回文长度已经求出来了,那么就可以确定该点i回文的长度就是2*id-i点回文的长度.然后根据i+len[i]又要分出两种情况:

(1)i+len[i]<mx,这种情况就基本确定了,他的回文长度就是和他关于id对称的那个点的回文长度.

(2)i+len[i]>=mx,这里因为当i+len[i]>=mx时,在mx之后的字符没有进行回文匹配,所以还要继续进行匹配,就以i为对称轴,mx-i为长度,继续向两边延伸,看看进行匹配,直到失配,失配了再将此时i匹配完的最右端和mx对比,如果大于mx,那么id要更新,mx更新.因为此时以i为对称轴的回文串是回文末尾在最右端的回文串了.

话不多说,manacher也不难,上代码:

int manacher()
{
    int id=0,mx=0;
    len[0]=0;
    int ans=0;//ans存的是最长回文长度
    len1=strlen(str);
    for(int i=1;i<len1;i++)
    {
        if(i<mx)len[i]=min(len[i],mx-i);
        else len[i]=1;
        while(str[i+len[i]]==str[i-len[i]])
        {
            len[i]++;
        }
        if(i+len[i]>mx)
        {
            id=i;
            mx=i+len[i];
        }
        ans=max(max,len[i]);
    }
    return ans-1;    
}

在这里把洛谷的两个板子题贴上来:

kmp

https://www.luogu.com.cn/problem/P3375

#include<iostream>
#include<string>
using namespace std;
int ne[1000007];
void pre(string str1)
{
	int i=0,j=-1,len1=str1.size();
	ne[i]=j;
	while(i<len1)
	{
		if(str1[i]==str1[j]||j==-1)
		{
			i++;
			j++;
			ne[i]=j;
		}
		else
		{
			j=ne[j];
		}
	}
	return ;
}
int main()
{
	string str,str1;
	cin>>str>>str1;
	int len=str.size(),len1=str1.size(),i=0,j=0;
	pre(str1);
	while(i<len)
	{
		if(str[i]==str1[j]||j==-1)
		{
			i++;
			j++;
		}
		else if(str[i]!=str1[j])
		{
			j=ne[j];
		}
		if(j==len1)
		{
			cout<<i-len1+1<<endl;
		}
	}
		for(int i=1;i<=str1.size();i++)cout<<ne[i]<<" ";
	return 0;
}

manacher

https://www.luogu.com.cn/problem/P3805

#include<iostream>
#include<cstring>
using namespace std;
char str[32000007];
char s[31000007];
int len[31000007];
int manacher(int len1)
{
    int mx=0,id=0,sum=0;
    len[0]=0;
    for(int i=1;i<len1;i++)
    {
        if(i<mx)len[i]=min(mx-i,len[2*id-i]);
        else len[i]=1;
        while(str[i+len[i]]==str[i-len[i]])len[i]++;
        if(i+len[i]>mx)
        {
            mx=len[i]+i;
            id=i; 
        }
        sum=max(sum,len[i]);
    }
    return (sum-1);
}
int main()
{
    long long cnt=2;
    scanf("%s",s);
    int len=strlen(s);
    str[0]='@';
    str[1]='#';
    for(int i=0;i<len;i++)
    {   
        str[cnt++]=s[i];
        str[cnt++]='#';
    }
    printf("%d",manacher(cnt));
}

 去看拓展kmp了.

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值