首页 > 其他分享 >z函数|exkmp|拓展kmp 笔记+图解

z函数|exkmp|拓展kmp 笔记+图解

时间:2023-06-01 13:45:37浏览次数:49  
标签:exkmp p0 相等 int kmp p1 now ans 图解

题外话,我找个什么时间把kmp也加一下图解

z函数|exkmp

别担心

这个exkmp和kmp没毛点关系,请放心食用。

本文下标以1开始,为什么?因为1开始就不需要进行长度和下标的转换,长度即下标。

定义

给出模板串S和子串T,长度分别为n和m,对于每个ans[i](1<=i<=n),求出S[i...n]与T的最长公共前缀长度。

……我一脸懵逼

举个例子:

S=aabbabaaab
T=aabb

当i=0时,S[1...n]为aabbabaaab
与aabb的公共前缀是aabb,长度为4,即ans[1]=4

当i=1时,S[2...n]为abbabaaab
与aabb的公共前缀是a,长度为1,即ans[2]=1

当i=2时,S[3...n]为bbabaaab
与aabb的公共前缀是,长度为0,即ans[3]=0

.
.
.

当i=n-1时,S[n...n]为b
与aabb的公共前缀是,长度为0,即ans[4]=0

最终ans为4 1 0 0 1 0 2 3 1 0

大家可以自己计算并进行检验,这是第一步,搞懂这个算法求啥

exkmp与z函数区别

没区别

国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP。

暴力为王

暴力的算法复杂度为 O(n^2) ,非常巨大,所以我们要考虑优化,使用“转移”。

从入门到入土

我们为了简单,先简化一下条件:S=T,假如S与T完全相同,应该怎么做呢?

我们可以先发现ans[1]=n。这非常容易得到:

因为S=T,当求ans[1]时,S[1...n]是和S完全相等的,所以相等的长度就是n了,即ans[1]=n。

我们可以快速写出代码:

ans[1]=n

ans[2]?

ans[2]需要我们的暴力求解,为什么,因为如果我们不暴力求解,我们就无法通过某些奇怪的操作转移出后面的ans。

所以只能牺牲一下暴力ans[2]了。

代码很简单:

int now=0;
while(now+2<=n && S[now+1]==S[now+2]) now++;
ans[2]=now;

开始转移!!

我们设目前要求的是k。为了通过转移求出ans[k],我们要知道目前已求解部分中覆盖最远的地方,我们希望这个最远的已求解部分可以覆盖我们的ans[k]部分,这样我们就可以转移出来了。

如图,k是我们要求的,p0是已覆盖最远位置的起点,p1是已覆盖最远位置的终点,即p1=p0+ans[p0]-1。就是说,我们要求的ans[k]就在ans[p0]中。

我们给S平移p0位,即:

那么因为S[0...(p1-p0+1)]=S[p0...p1](这是定义),即下图中绿色部分相等:

所以,下图中的蓝色部分相等,紫色部分也相等,对应的部分都相等ヾ(≧▽≦*)o:

欸,那么也就是设有长度\(l\), \(S[(k-p_0+1)\dots (k-p_0+1)+l]\) 也会等于 \(S[k...k+l]\) ?即下图褐色部分相同:

假如l就是ans[k-p0+1],这个区间不就是它们的最长公共前缀吗?

又因为定义,得知下图褐色①与褐色②相等,上面得出褐色②和褐色③相等,进而得出褐色①和褐色③相等(等量代换)。

又因为褐色④和褐色①相等(都是同一个位置的还能不相等?!),所以褐色③和褐色④相等:

那么不就得出来了吗?褐色③的ans就是褐色②的ans,即 \(ans[k]=ans[k-p_0+1]\)

所以我们就计算出来了吗?并没有。

意料之外

当我们的l在p1之外,那么就会出现:

尽管紫色部分是相等的,可我们却不知道马赛克部分是否相等,如果相等就比转移后结果大,如果不相等就是转移后结果,所以我们无从可知,只能暴力ヽ(*。>Д<)o゜

可是如果如此暴力,复杂度又会退化为O(n^n),所以我们可以考虑优化:

上图中我们已经得知紫色部分相等,只需暴力 深红色 部分即可。所以我们比对从 \(p_1-k\) 开始。

now=max(0,p1-k);      // 因为紫色部分相等,我们选择不遍历这部分,可以省下一些时间,但如果紫色部分不存在,就会出现负下标,在此判断防止负下标的出现
while(now+k<=n && S[now+1]==S[now+k]) now++;
ans[k]=now;
p0=k;
p1=k+ans[k]-1;

Code

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char S[1010];
int ans[1010];
int n;
int p0,p1;
int main() {
	scanf("%s",S+1);
	n=strlen(S+1);
	ans[1]=n;
	int now=0;
	while(now+2<=n && S[now+1]==S[now+2]) now++;
	ans[2]=now;
	p0=2;
	p1=2+ans[2]-1;
	for(int k=3; k<=n; k++) {
		if(k+ans[k-p0+1]-1<p1) ans[k]=ans[k-p0+1];
		else {
			now=max(0,p1-k);
			while(now+k<=n && S[now+1]==S[now+k]) now++;
			ans[k]=now;
			p0=k;
			p1=k+ans[k]-1;
		}
	}
	for(int i=1; i<=n; i++) {
		printf("%d ",ans[i]);
	}
}

为什么是 k+ans[k-p0+1]-1<p1 而不是 k+ans[k-p0+1]-1<=p1

当ans[k-p0+1]=0 时,p0和p1在同一点上,就有:

那么如果我们单纯判读 k+ans[k-p0+1]-1<=p1 ,p1就无法覆盖k,从而得出错误结果。

但如果我们舍弃一点复杂度,用 k+ans[k-p0+1]-1<p1 ,就不会出现无法覆盖的情况。

回归原题目, \(S\neq T\)

其实也差不多解决了。我们还是用之前的ans,因为这是拓展kmp,所以说我们再建一个exans ,表示S[i..n]与T的最长公共前缀的长度。

exans的第一位还是要暴力,理由同上:

int now=0;
while(now+1<=m && S[now+1]==T[now+1]) now++;
exans[1]=now;

还记得这张图吗:

模仿求ans的过程,只不过p0p1表示exans已求解部分中覆盖最远的地方,即:

在平移p0位时,我们对应的是T:

此外均相同,故可以直接复制求ans的代码了:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
char S[1010],T[1010];
int ans[1010],exans[1010];
int n,m;
int p0,p1;
void qAns() {
	ans[1]=m;
	int now=0;
	while(now+2<=m && T[now+1]==T[now+2]) now++;
	ans[2]=now;
	p0=2;
	p1=2+ans[2]-1;
	for(int k=3; k<=m; k++) {
		if(k+ans[k-p0+1]-1<p1) ans[k]=ans[k-p0+1];
		else {
			now=max(0,p1-k);
			while(now+k<=m && T[now+1]==T[now+k]) now++;
			ans[k]=now;
			p0=k;
			p1=k+ans[k]-1;
		}
	}
}

void qExans() {
	int now=0;
	while(now+1<=m && S[now+1]==T[now+1]) now++;
	exans[1]=now;
	p0=1;
	p1=p0+exans[p0]-1;
	for(int k=2; k<=n; k++) {
		if(k+ans[k-p0+1]-1<p1) exans[k]=ans[k-p0+1];
		else {
			now=max(0,p1-k);
			while(now+k<=n && T[now+1]==S[now+k]) now++;
			exans[k]=now;
			p0=k;
			p1=k+exans[k]-1;
		}
	}
}
int main() {
	scanf("%s %s",S+1,T+1);
	n=strlen(S+1);
	m=strlen(T+1);
	qAns();
	qExans();
	for(int i=1; i<=n; i++) {
		printf("%d ",exans[i]);
	}
}

标签:exkmp,p0,相等,int,kmp,p1,now,ans,图解
From: https://www.cnblogs.com/znpdco/p/17442961.html

相关文章

  • 图解LeetCode——98. 验证二叉搜索树
    一、题目给你一个二叉树的根节点root,判断其是否是一个有效的二叉搜索树。有效二叉搜索树定义如下:节点的左子树只包含小于当前节点的数。节点的右子树只包含大于当前节点的数。所有左子树和右子树自身必须也是二叉搜索树。二、示例2.1>示例1:【输入】root=[......
  • 图解VirtualBox安装CentOS 7
    VirtualBox简介VirtualBox是由德国InnoTek软件公司出品的虚拟机软件,现在则由甲骨文公司进行开发,是甲骨文公司xVM虚拟化平台技术的一部分。VirtualBox提供用户在32位或64位的Windows、Solaris及Linux操作系统上虚拟其它x86的操作系统。用户可以在VirtualBox上安装并且执行Solari......
  • 图解LeetCode——102. 二叉树的层序遍历
    一、题目给你二叉树的根节点root,返回其节点值的层序遍历。(即逐层地,从左到右访问所有节点)。二、示例2.1>示例1:【输入】root=[3,9,20,null,null,15,7]【输出】[[3],[9,20],[15,7]]2.2>示例2:【输入】root=[1]【输出】[[1]]2.3>示例3:【输入】root=[]......
  • 图解LeetCode——146. LRU 缓存
    一、题目请你设计并实现一个满足 LRU(最近最少使用)缓存约束的数据结构。实现LRUCache类:LRUCache(intcapacity)以正整数作为容量 capacity初始化LRU缓存intget(intkey)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。voidput(intkey,intva......
  • 图解Redis和Zookeeper分布式锁
    1.基于Redis实现分布式锁Redis分布式锁原理如上图所示,当有多个Set命令发送到Redis时,Redis会串行处理,最终只有一个Set命令执行成功,从而只有一个线程加锁成功2:SetNx命令加锁利用_Redis的setNx命令在Redis数据库中创建一个<Key,Value>记录,这条命令只有当Redis中没有这个Key的时候......
  • OSI图解
    OSI七层模型通过七个层次化的结构模型使不同的系统不同的网络之间实现可靠的通讯,因此其最主要的功能就是帮助不同类型的主机实现数据传输。完成中继功能的节点通常称为中继系统。在OSI七层模型中,处于不同层的中继系统具有不同的名称。   一个设备工作在哪一层,关键看它......
  • 每天一颓: 均摊分析, pi函数和KMP算法
    资料内容:https://oi-wiki.org/string/kmp/很久以前学过,写一些笔记作复习资料一些概念:真前缀,真后缀等等不作介绍(真前后缀匹配函数)前缀函数(pi函数):\[\pi[i]=\max_{k=0\dotsi}\{k:s[0\dotsk-1]=s[i-(k-1)\dotsi]\}\]特别规定,\[\pi[0]=0\]/......
  • 字符串匹配|kmp笔记
    很久之前学的了。做个笔记回忆一下:kmp朴素比对字符串所谓字符串匹配,是这样一种问题:“字符串T是否为字符串S的子串?如果是,它出现在S的哪些位置?”其中S称为主串;T称为模式串。如在字符串sabcabcabcabd中找到子串Tabcabd:先设两个指针i、j,i表示S的指针,j表示T的指针......
  • KMP算法
    就我学过的所有处理字符串的算法(包括匹配算法、回文算法、后缀算法、字符串哈希),都离不开两个恒定的主题:递推构建和压缩信息。这一特征很明显和字符串的性质有关:子串众多,而子串之间互相关联性强。字符串的算法大多数都是\(O(n)\)的时间或空间复杂度,和“字符串本身包含的信息只有......
  • KMP算法
    KMP算法一.问题场景有字符串A和字符串B,求B在A中首次出现的位置。力扣题目链接:28.找出字符串中第一个匹配项的下标-力扣(LeetCode)为方便说明,规定A为主串,B为子串(模式串)二.朴素的匹配算法为方便说明,举A="aabaaf",B="aaf"使用如下图所示的算法(图中只显示了前两趟匹配)......