KMP&Z函数详解
KMP
一些简单的定义:
- 真前缀:不是整个字符串的前缀
- 真后缀:不是整个字符串的后缀
当然不可能这么简单的,来个重要的定义
- 前缀函数:
给定一个长度为\(n\)的字符串\(s\),其 \(前缀函数\) 为一个长度为\(n\)的数组\(\pi\),其中\(\pi_i\)表示- 如果字串\(s[0...i]\)存在一对相等的真前缀和真后缀\(s[0...k]~and~s[i-(k-1)...i]\),则\(\pi_i\)为这个真前缀(真后缀)的长度\(k\)
- 如果有不止一对,则\(\pi_i\)为其中最长的一对
- 若没有最长的,则\(\pi_i=0\)
简而言之,\(\pi_i\)为字串\(s[0...i]\)最长相等的真前缀和真后缀长度
特别的,规定\(\pi_0\)=0
举个例子,对于字符串\(s=\)"\(abcabcd\)",其前缀函数\(\pi=\{0,0,0,1,2,3,0\}\)
考虑如何去求前缀函数,最暴力的做法肯定使\(O(n^3)\)的,枚举前缀位置、真前缀(真后缀)的长度和真前缀(真后缀)的每一位,代码就不放出来了(其实是没写)
优化一
我们发现当\(i+1\)时,\(\pi_i\)最多\(+1\),也就是说我们枚举\(\pi_i\)的长度时,上界为\(\pi{i-1}+1\),这样复杂度可以被优化为\(O(n^2)\)
代码如下
void Getnex(string str){
int len=str.size();
nex[0]=0;
for (int i=1;i<n;i++) {
for (int j=nex[i-1];j>=0;j--) {
if (str.substr(0,j)==str.substr(i-j+1,j)) {
nex[i]=j;
break;
}
}
}
}
如何证明这个复杂度呢
我们发现每次进行一次\(substr\)操作,都意味着\(\pi_i\)的值都会\(-1\)
显然有一种最坏的情况是,\(p_i\)的值先变成\(n-1\),然后再掉回\(0\)
容易看出这不会超过\(O(n)\)次,然后每次\(substr\)的复杂度为\(O(n)\)
所以总复杂度\(O(n^2)\)
优化二
我们在优化一中发现了一个性质,当\(s[i+1]==s[\pi_i]\)时,\(\pi_i=\pi_{i-1}+1\)
考虑把这个性质推下去,当\(s[i+1]!=s[\pi_i]\)时
我们发现,我们要找的转移点\(j\)是要满足\(i-1\)的前缀性质的
即满足\(s[1...j]=s[i-(j-1)...i]\)
当\(\pi_i\)不行时我们自然要去找下一个满足性质的转移点
很容易想到这东西不就是\(\pi_{\pi_i}吗\)
由于真前缀和真后缀相等,所以\(\pi_{\pi_i}\)必然满足既是真前缀的真前缀又是真后缀的真后缀
可能有点绕,举个例子\(s=\)"\(abaabaa\)"(下标从\(0\)开始)
当我们求\(\pi_6\)时,前面的\(\pi\)值为
\(\pi[0...5]=\{0,0,1,1,2,3\}\)
我们发现\(s[6]!=s[\pi_5=3]\)
那么我们就需要找到下一个转移点\(j\)满足\(s[0...j]=s[5-(j+1)...5]\)
因为\(\pi_5=3\)所以\(s[0...5]\)的满足前缀性质的真前缀(真后缀)为\(aba\)
所以对于字符串"\(aba\)"满足前缀性质的真前缀(真后缀)一定满足\(s[0...5]\)的前缀性质
所以转移点即为\(\pi_3=\pi_{\pi_5}\)
至此,求前缀函数便可以优化成\(O(n)\)了
void Getnex(std::string S) {
for (int i=2,j=0;i<S.size();i++) {
while(j && S[j+1]!=S[i]) j=nex[j];
if (S[j+1]==S[i]) j++;
nex[i]=j;
}
}
前话到此完结,接下来是真正的\(KMP\)
\(KMP\)是对于前缀函数的典型运用
举个例子,给定一个文本\(t\)和字符串\(s\),我们尝试求出\(s\)在\(t\)中的所有出现
我们记\(n,m\)为\(s\)和\(t\)的长度
我们构造一个字符串为\(s+\)'\(.\)'\(+t\),其中\(.\)为不在\(s,t\)中出现的分隔符
计算出这个字符串的前缀函数,考虑这个前缀函数除去前\(n+1\)个值意味着什么
根据定义,\(\pi_i\)为右端点为\(i\),且为一个前缀的最长真子串长度
且由于有分割符的存在,\(\pi\)不可能超过\(n\)
当\(\pi_i=n\)时,则意味着\(s\)在\(t\)中完整出现一次,其右端点为\(i\)
因此\(KMP\)可以在\(O(n+m)\)的复杂度内解决问题
void KMP(std::string S,std::string T) {
for (int i=1,j=0;i<S.size();i++) {
while(j && T[j+1]!=S[i]) j=nex[j];
if (T[j+1]==S[i]) j++;
if (j==m-1) {
std::cout<<i-m+2<<std::endl;
j=nex[j];
}
}
}
字符串的周期
定义:
对于字符串\(s\),若存在\(p\)满足\(s[i]=s[i+p](i\in[0,|s|-p-1])\),则\(p\)为\(s\)的周期
对于字符串\(s\),若存在\(r\)满足\(s\)长度为\(r\)的前缀和长度为\(r\)的后缀相等,则称\(s\)长度为\(r\)的前缀是\(s\)的\(border\)
由这两个定义不难看出\(|s|-r\)是\(s\)的周期
根据前缀函数的定义我们可以得出\(s\)所有\(border\)长度,即\(\pi_{n-1},\pi_{\pi_{n-1}},...\)
所以我们可以在\(O(n)\)的时间复杂度内求出\(s\)的所有周期
其中最小周期为\(n-\pi_{n-1}\)
统计每个前缀的出现次数
以下默认字符串下标从\(1\)开始
主要是两种问题,一个是求\(s\)的前缀在\(s\)中的出现次数,另一个是求\(s\)的前缀在另一个字符串\(t\)中的出现次数
考虑位置\(i\)的前缀函数值\(\pi_i\),根据定义,其意味着字符串\(s\)一个长度为 的前缀在位置\(i\)出现并以\(i\)为右端点,同时不存在一个更长的前缀满足前述定义。
与此同时,更短的前缀可能以该位置为右端点。
容易看出,我们遇到了在计算前缀函数时已经回答过的问题:给定一个长度为\(j\)的前缀,同时其也是一个右端点位于\(i\)的后缀,下一个更小的前缀长度\(k<j\)是多少?该长度的前缀需同时也是一个右端点为\(i\)的后缀。
因此以位置\(i\)为右端点,有长度为\(\pi_i\)的前缀,有长度为\(\pi_{\pi_i}\)的前缀,等等,直到长度变为0。
故而我们可以通过下述方式计算答案。
void Getcnt(std::string str) {
for (int i=1;i<str.size();i++) ans[nex[i]]++;
for (int i=str.size()-1;i>0;i--) ans[nex[i]]+=ans[nex[i]];
for (int i=1;i<str.size();i++) ans[i]++;
}
例题:CF432D Prefixes and Suffixes
题意:
给你一个长度为n的长字符串,“完美子串”既是它的前缀也是它的后缀,求“完美子串”的个数且统计这些子串的在长字符串中出现的次数
模板题,直接写就行
#include <ctime>
#include <cstdio>
#include <iostream>
#define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
const int maxn=1e5+5;
int n,nex[maxn],ans[maxn];
std::string str;
void chkmax(int &x,int y) {if (x<y) x=y;}
void chkmin(int &x,int y) {if (x>y) x=y;}
int read() {
int x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {if (ch=='-') f=-1;ch=getchar();}
while(ch<='9' && ch>='0') {x=(x<<3)+(x<<1)+ch-'0';ch=getchar();}
return x*f;
}
void Getnex(std::string s) {
for (int i=2,j=0;i<s.size();i++) {
while(j && s[j+1]!=s[i]) j=nex[j];
if (s[j+1]==s[i]) j++;
nex[i]=j;
}
}
void out(int i,int cnt) {
if (!i) {std::cout<<cnt<<std::endl;return ;}
out(nex[i],cnt+1);
std::cout<<i<<' '<<ans[i]<<std::endl;
}
int main() {
std::cin>>str;
str=" "+str;
Getnex(str);
// for (int i=1;i<str.size();i++) std::cout<<nex[i]<<' ';puts("");
for (int i=1;i<str.size();i++) ans[nex[i]]++;
for (int i=str.size();i>=1;i--) ans[nex[i]]+=ans[i];
for (int i=1;i<str.size();i++) ans[i]++;
out(str.size()-1,0);
return 0;
}
Z函数(扩展KMP)
定义:
对于一个字符串\(s\),\(z[i]\)表示\(s\)和\(s[i...n-1]\)的\(LCP\)(最长公共前缀)的长度,\(z\)则被称为\(s\)的\(Z\)函数
我们在计算的时候,可以通过前面已知的\(z\)来计算
对于\(i\),我们称区间\([i,i+z[i]-1]\)为\(i\)的匹配段
我们在算法过程中维护右端点最靠右的匹配段,记作\([l,r]\)
则有\([l,r]\)为\(s\)的前缀,并且在计算\(z[i]\)时我们保证\(l\leq i\)
算法流程
最开始时,\(l=r=0\)
在计算\(z[i]\)的过程中
若\(i\leq r\),则有\(s[i,r]=s[i-l,r-l]\),所以\(z[i]\ge min(z[i-l,r-i+1])\)
- 若\(z[i-l]<r-i+1\),则\(z[i]=z[i-l]\)
- 若\(z[i-l]\ge r-i+1\),我们令\(z[i]=r-i+1\),然后暴力向后枚举字符
若\(i>r\),我们直接暴力从\(s[i]\)开始比较,求出\(z[i]\)
求出\(z[i]\)后,还要更新\(l,r\)
void GetZ(std::string s) {
int l=0,r=0;
for (int i=1;i<s.size();i++) {
if (i<=r && z[i-l]<r-i+1) z[i]=z[i-l];
else {
z[i]=std::max(0,r-i+1);
while(i+z[i]<s.size() && s[z[i]]==s[i+z[i]]) z[i]++;
}
if (i+z[i]-1>r) {l=i;r=i+z[i]-1};
}
}
复杂度分析
对于内层的\(while\),每次执行都会使\(r\)向后移动至少一位,而\(r<n-1\),所以总共最多做\(n\)次
加上外层的\(for\),总复杂度\(O(n)\)
KMP&Z函数详解的更多相关文章
- Netsuite Formula > Oracle函数列表速查(PL/SQL单行函数和组函数详解).txt
PL/SQL单行函数和组函数详解 函数是一种有零个或多个参数并且有一个返回值的程序.在SQL中Oracle内建了一系列函数,这些函数都可被称为SQL或PL/SQL语句,函数主要分为两大类: 单行函数 ...
- 【C语言】printf函数详解
C语言printf函数详解 一.相关基础知识 请求printf()打印变量的指令取决于变量的类型,例如打印整数用%d符号,打印字符用%c符号,这些符号称为转换说明(conversion specifi ...
- python3 内置函数详解
内置函数详解 abs(x) 返回数字的绝对值,参数可以是整数或浮点数,如果参数是复数,则返回其大小. # 如果参数是复数,则返回其大小. >>> abs(-25) 25 >&g ...
- 【转载】3D/2D中的D3DXMatrixPerspectiveFovLH和D3DXMatrixOrthoLH投影函数详解
原文:3D/2D中的D3DXMatrixPerspectiveFovLH和D3DXMatrixOrthoLH投影函数详解 3D中z值会影响屏幕坐标系到世界坐标系之间的转换,2D中Z值不会产生影响(而只 ...
- KMP字符串模式匹配详解(zz)
刚看到位兄弟也贴了份KMP算法说明,但本人觉得说的不是很详细,当初我在看这个算法的时候也看的头晕昏昏的,我贴的这份也是网上找的.且听详细分解: KMP字符串模式匹配详解 来自CSDN A_B_ ...
- STL之map与pair与unordered_map常用函数详解
STL之map与pair与unordered_map常用函数详解 一.map的概述 map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称 ...
- python基础之函数详解
Python基础之函数详解 目录 Python基础之函数详解 一.函数的定义 二.函数的调用 三.函数返回值 四.函数的参数 4.1 位置参数 4.2 关键字参数 实参:位置实参和关键字参数的混合使用 ...
- malloc 与 free函数详解<转载>
malloc和free函数详解 本文介绍malloc和free函数的内容. 在C中,对内存的管理是相当重要.下面开始介绍这两个函数: 一.malloc()和free()的基本概念以及基本用法: 1 ...
- NSSearchPathForDirectoriesInDomains函数详解
NSSearchPathForDirectoriesInDomains函数详解 #import "NSString+FilePath.h" @implementation ...
随机推荐
- 深度学习基础-基于Numpy的感知机Perception构建和训练
1. 感知机模型 感知机Perception是一个线性的分类器,其只适用于线性可分的数据. f(x) = sign(w.x + b) 其试图在所有线性可分超平面构成的假设空间中找 ...
- HashSet集合介绍和哈希值
HashSet集合介绍 ~java.util.Set接口 extends Collection 接口~Set接口的特点: 1.不允许存储重复的元素 2.没有索引,没有带索引的方法,也不能使用普通的fo ...
- C++记录一
题目一: [描述] 比较两个整数之间的大于.小于.等于.不等于关系. [输入] 输入在一行中给出2个整数a和b. [输出] 分行输出整数a和b之间的大于.小于.等于.不等于关系. [输入示例] 5 3 ...
- 算法竞赛进阶指南0x14 Hash
组成部分: 哈希函数: 链表 AcWing137. 雪花雪花雪花 因为所需要数据量过于大,所以只能以O(n)的复杂度. 所以不可能在实现的过程中一一顺时针逆时针进行比较,所以采用一种合适的数据结构. ...
- .net core3.1 abp学习开始(一)
vs版本 2019,链接数据库使用Navicat,数据库MySql abp的官网:https://aspnetboilerplate.com/,我们去Download这里下载一个模板,需要选好Targ ...
- 如何设计一个分布式 ID 发号器?
大家好,我是树哥. 在复杂的分布式系统中,往往需要对大量的数据和消息进行唯一标识,例如:分库分表的 ID 主键.分布式追踪的请求 ID 等等.于是,设计「分布式 ID 发号器」就成为了一个非常常见的系 ...
- 3. 安装部署MGR集群 | 深入浅出MGR
GreatSQL社区原创内容未经授权不得随意使用,转载请联系小编并注明来源. 目录 1. 安装准备 2. 初始化MySQL Server 3. 初始化MGR第一个节点 4. 继续设置另外两个节点 5. ...
- 痞子衡嵌入式:聊聊i.MXRT1170双核下不同GPIO组的访问以及中断设计
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是i.MXRT1170双核下不同GPIO组的访问以及中断设计. 在双核 i.MXRT1170 下设计应用程序,有一个比较重要的考虑点就是外 ...
- 红黑树以及JAVA实现(一)
目录 前言 一. B树 1.1 概念 1.2 2-3-4树 1.3 2-3-4树的插入 节点分类 1.4 2-3-4树的删除 1.4.1 当删除节点是叶子节点 1.4.1.1 当删除节点为非2节点 1 ...
- Java 数字转汉字
阿拉伯数字转汉字 public static String number2chinese(int src) { final String num[] = {"零", "一 ...