kmp的理解

第一个kmp,这个是重要的字符串处理算法,呃我现在在努力的复习字符串,所以这是第一步!通常来时这个的作用是,呃,先放几篇文章:(https://zhuanlan.zhihu.com/p/83334559),(https://blog.csdn.net/dark_cy/article/details/88698736)感觉写的不错的,然后呢本质上就是求出两个字符串的匹配数,呃这是个b站的视频讲的确实不错(https://www.bilibili.com/video/BV1jb411V78H/?spm_id_from=333.788.recommend_more_video.-1&vd_source=f51708a79de2172437e75278f8c832e0)。
然后处理方法听我慢慢说,就是一般乱搞的话O(n*m)的复杂度嘛,但是你要是用kmp的哈,就是O(n+m),因为你不会走回头路的。比如说有两个s1,s2的要求len1>len2嘛,求其数量,然后具体的就是拿一个数组 next [ ] 来记录s2位置1到 j 子串的最长公共前后缀,然后就可以通过最长公共前后缀来快速跳跃,具体的证明与思路就看别人的吧。这里给出(https://www.luogu.com.cn/problem/P3375)luogu例题的代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
int net[1011101];
char s1[1011010],s2[110101];
int len1,len2;
int main()
{
	scanf("%s",s1+1);
	scanf("%s",s2+1);
	len1=strlen(s1+1);
	len2=strlen(s2+1);
	for(int i=2,j=0;i<=len2;i++)
	{
		while(j>0&&s2[i]!=s2[j+1]) j=net[j];
		if(s2[j+1]==s2[i]) j++;
		net[i]=j;
	}
	for(int i=1,j=0;i<=len1;i++)
	{
		while(j>0&&s2[j+1]!=s1[i]) j=net[j];
		if(s2[j+1]==s1[i]) j++;
		if(j==len2) 
		{
			printf("%d\n",i-len2+1);
			j=net[j];
		}
	}
	for(int i=1;i<=len2;i++) printf("%d ",net[i]);
	return 0;
} 

这里有些别人写了注释的代码,理解下:
这是求next的:

j=0;
for (int i=2;i<=lb;i++)
{     
	while(j&&b[i]!=b[j+1]) j=kmp[j];    
	//此处判断j是否为0的原因在于,如果回跳到第一个字符就不 用再回跳了
	//通过自己匹配自己来得出每一个点的kmp值 
	if(b[j+1]==b[i])j++;    
	kmp[i]=j;
	//i+1失配后应该如何跳 
}

然后这是例题正常匹配的:

 int j=0;//j可以看做表示当前已经匹配完的模式串的最后一位的位置 
 //如果楼上看不懂,你也可以理解为j表示模式串匹配到第几位了 
 for(int i=1;i<=la;i++)
 {
      while(j&&b[j+1]!=a[i]) j=kmp[j]; //如果失配 ,那么就不断向回跳,直到可以继续匹配 
      if (b[j+1]==a[i]) j++; //如果匹配成功,那么对应的模式串位置++ 
      if (j==lb) 
      {
      	cout<<i-lb+1<<endl;
	 	j=kmp[j];//继续匹配 
	  }
}

next的应用:比方说这道 P4391 [BOI2009]Radio Transmission 无线传输(https://www.luogu.com.cn/problem/P4391)就要用到最长公共前后缀,代码简单不放了,结论的话证明起来还好吧,放一张别人的图。
以下是别人的话如果涉嫌抄袭请私信我:
假设这两段是整个字符串ss的最大公共前后缀;
第一段: 1~next[n]
第二段: n-next[n]+1~n
为了方便观察,我将前缀和后缀分开,令它们上下一一对应;在这里插入图片描述
二、 现在我们人为地把字符串按照红色段的长度划分为若干段并标号(图中的第1,2,3,4,5,6,7,8,9段);

容易看出,红色的一段和后缀合起来就是原字符串;

所以推出:

  1. 因为上下对应相等,故第1段等于红色段;

  2. 因为是公共前后缀,故第2段等于第1段;

  3. 因为上下对应相等,故第3段等于第2段;

  4. 因为是公共前后缀,故第4段等于第3段;

  5. 红色段就是循环子串;

三、从而,我们知道了原字符串ss除去公共前后缀(图中的黑色段)中的一个剩下的就是循环子串(图中的红色段);同样,易知原串ss除去开头(或结尾)的循环子串(图中的红色段)剩下的部分就是公共前后缀(图中的黑色段)。

所以就搞定这个证明了

然后看下一题:P4824 [USACO15FEB] Censoring S(https://www.luogu.com.cn/problem/P4824)删除字符串s中的t,删完一个t后可能会构造成一个新的t,又可以删,开始的时候没什么思路,然后就看题解,发现可以用栈来维护,于是就有了以下的代码,不过先提一提思路(别人写的):
1、KMP板子跑一遍
2、在 KMPKMP 过程中,把遍历到的 i(不是字符,而是下标)入栈,当匹配上 B 时,就把匹配的部分出栈,然后 j 从栈顶的 i 所能匹配到的最大的位置开始(就是 f[i] 记录的值),继续做 KMP
时间复杂度:B自身匹配一次 +A 与 B 匹配一次+ A 中最多每个字符进出栈一次,为 O(|A|)(大雾)。
行吧代码如下:

#include<bits/stdc++.h>//数组范围有点锅但懒得管
using namespace std;
int n,m;
char a[200001],t[200001];
int len1=0,len2=0;
int net[10001];
int f[10001];	
int q[10001],tot=0;//栈
int main()
{
	scanf("%s%s",a+1,t+1);
	len1=strlen(a+1),len2=strlen(t+1);
	net[0]=net[1]=0;
	for(int i=2,j=0;i<=len2;i++)
	{
		while(j&&t[j+1]!=t[i]) j=net[j];
		if(t[i]==t[j+1]) j++;
		net[i]=j;
	}
	for(int i=1,j=0;i<=len1;i++)
	{
		while(j&&a[i]!=t[j+1]) j=net[j];
		if(a[i]==t[j+1]) j++;//记录位置
		q[++tot]=i;//入栈
		f[i]=j;//f指的是它对应的那个j的匹配位置
		if(len2==j)//如果能成功匹配
		{	
			tot-=len2;//删掉那一段
			j=f[q[tot]];//从那个匹配的位置开始
		}
	}
	for(int i=1;i<=tot;i++) printf("%c",a[q[i]]);//栈中元素!
	return 0;
}

下一题!这个确实难!(https://www.luogu.com.cn/problem/P2375)P2375 [NOI2014] 动物园,我对kmp的理解直接上一层好吧真的,这个题,看似无从下手,实则是kmp的变式,
就本质上,统计的确实有意思先放代码,实质上就说统计以 i 结尾长度不超过 i/2 的最长公共前后缀的数量嗯就是这样,用num [ i ] 记录一下每一个以 i 结尾的贡献然后就可以O(n)求,最后i/2的情况可以用while((j<<1)>i) j=net[j];这一句来做用别人的话说就是:

我们将递归用的变量 j 的值不更新,这样,求完了 i 的答案以后,j 的位置一定在 i/2
的左边,也就是它已经满足要求了(对时间的优化)

代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int m;
char a[1000001];
int net[1000001],num[1000001];
int ans=0;
int len;
signed main()
{
	int t;
	scanf("%lld",&t);
	while(t--)
	{
		scanf("%s",a+1);
		len=strlen(a+1);
		num[0]=0,num[1]=1;
		net[0]=net[1]=0;
		for(int i=2,j=0;i<=len;i++)
		{
			while(j&&a[i]!=a[j+1]) j=net[j];
			if(a[i]==a[j+1]) j++;
			net[i]=j;num[i]=num[j]+1;
		}
		int ans=1;
		for(int i=2,j=0;i<=len;i++)
		{
			while(j&&a[i]!=a[j+1]) j=net[j];
			if(a[i]==a[j+1]) j++;
			while((j<<1)>i) j=net[j];
			ans=ans*(num[j]+1);
			ans%=1000000007;
		}	
		printf("%lld\n",ans);
	}
	return 0;
}

然后是下一题,呃,就是 CF1200E Compress Words(https://www.luogu.com.cn/problem/CF1200E)就随便维护一下新的就好,不会的就看别人的题解吧,不过有一两个小坑点,第一个是不能跨越两个字符串进行操作,就是新字符串加上原字符串的一部分后求的值可能会超过新字符串的长度,所以我们可以在新字符串的结尾加上点奇怪的东西比如 ‘*%&……%&’这类的,然后另一个就是直接把答案串和输入串连起来求net可能会爆炸,所以我们可以连结一小部分。就呃这句, int minn=min(len1,len2);
放代码:

#include<bits/stdc++.h>
using namespace std;
int n,m;
char s[2000001],s1[2000001];
int len1=0,len2;
int net[2000001];
int main()
{
	int t;
	scanf("%d",&t);
	while(t--)
	{
		scanf("%s",s1+1);
		len2=strlen(s1+1);
		int minn=min(len1,len2);
		int tot=len2;//记录的是新加入的字符串改值后的长度 
		s1[++tot]='&&';
		net[0]=net[1]=0;
		for(int i=1;i<=minn;i++) s1[++tot]=s[len1-(minn-i)];
		for(int i=2,j=0;i<=tot;i++)
		{
			while(j&&s1[j+1]!=s1[i]) j=net[j];
			if(s1[i]==s1[j+1]) j++;
			net[i]=j;
		}
		for(int i=net[tot]+1;i<=len2;i++) s[++len1]=s1[i];
	}
	for(int i=1;i<=len1;i++) printf("%c",s[i]);
	return 0;
}

下一个就是最后一道题了,P3435 [POI2006] OKR-Periods of Words(https://www.luogu.com.cn/problem/P3435)本质上是求最短公共前后缀,为什么?看别人的解释吧讲的确实好(https://blog.csdn.net/jziwjxjd/article/details/113803877),求法则是用那个net [ net [ net [ … ] ] ] 来求。还有就是优化更新,打一个记忆化呗。
代码:

#include<bits/stdc++.h>
#define int long long
using namespace std;
int n,m;
char s[2000001];
int net[20000001];
int ans=0,len=0;
main()
{
	scanf("%lld",&n);
	scanf("%s",s+1);
	net[1]=net[0]=0;
	for(int i=2,j=0;i<=n;i++)
	{
		while(j&&s[i]!=s[j+1]) j=net[j];
		if(s[i]==s[j+1]) j++;
		net[i]=j;
	}
	int ans=0;
	for(int i=2,j=i;i<=n;i++,j=i)
	{
		while(net[j]) j=net[j];
		if(net[i]) net[i]=j;//这是记忆化
		ans+=i-j;
	}
	printf("%lld",ans);
	return 0;
}

CF808G Anthem of Berland(https://www.luogu.com.cn/problem/CF808G)一道绝对nb的dp加kmp,确实nb。以下是别人的题解:(https://www.luogu.com.cn/problem/solution/CF808G)
看到数据范围,决定dp。开始想做一个nm的dp,确实可行但数组做不到,那就对其改变,设 f[ i ] ​表示 t 在 s 的前 i 个位置最大的出现次数。那么如果一个位置想从之前的位置转移过来,就必须满足t能在这个位置与s匹配,这一部分可以O(m)暴力判断。
然后就是转移嘛,明显可以从f [ i - m + 1] 中转移过来表示这一段是一个整t。不过,可能会有t重叠的情况,那就只能用求最长公共前后缀的方式来做为什么捏,因为这个那个感性理解一下,就是能用的直接用一下就好了。我们可以通过从m开始一直跳next,来保证前缀与后缀相等。但还有一个问题,因为f的定义,导致我们无法直接从 f [ i - m + 1 ] 来转移,所以我们要多设置一个g [ i ] 来表示s的前i个位置,强制最后放一个t的最大出现次数。、
ok上代码!在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
int n,m;
char s[1000001],t[1000001];
int g[1000001],f[1000001];
int net[1000001];	
bool pd(int p)
{
	for(int i=1;i<=m;i++)
	{
		if(s[p-i+1]!=t[m-i+1]&&s[p-i+1]!='?') return false;
	}
	return true ;
}
int main()
{
	scanf("%s%s",s+1,t+1);
	n=strlen(s+1),m=strlen(t+1);
	net[0]=net[1]=0;
	for(int i=2,j=0;i<=m;i++)
	{
		while(j&&t[i]!=t[j+1]) j=net[j];
		if(t[i]==t[j+1])j++;
		net[i]=j;
	}
	for(int i=1;i<=n;i++)
	{
		f[i]=f[i-1];
		if(pd(i))//判断能否能用
		{
			g[i]=f[i-m]+1;
			for(int j=net[m];j!=0;j=net[j]) g[i]=max(g[i],g[i-(m-j)]+1);//就是这个高级的好东西啦!
		}
		f[i]=max(f[i],g[i]);
	}
	printf("%d",f[n]);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值