字符串算法果然玄学=_=

参考资料:

OI Wiki:前缀函数与KMP算法

OI Wiki:Z函数(扩展KMP)

0. 约定

字符串的下标从 \(0\) 开始。\(|s|\) 表示字符串 \(s\) 的长度。

对于字符串 \(s\),记其每一个字符分别为 \(s_0, s_1, \cdots, s_{|s|-1}\)。

子串 \(s_l, s_{l+1}, \cdots, s_{r-1}, s_r\) 简记为 \(s[l:r]\)。特别地,若 \(l=0\),可记作 \(s[:r]\);若 \(r=|s|-1\),可记作 \(s[l:]\)。

对于字符串 \(a, b\),\(a+b\) 表示拼接操作,即将字符串 \(b\) 拼接到字符串 \(a\) 之后,构成新的字符串。

记构成的新字符串为 \(c\),则上述拼接操作记为 \(c\gets a+b\)。

其中符号 \(x\gets y\) 表示将 \(y\) 的值赋给 \(x\)。

不论是字符还是字符串,皆不加引号。

1. 前缀函数

1.1. 前缀函数的定义

对于字符串 \(s\),若存在一个非本身的子串 \(t\) 使得 \(t\) 既是 \(s\) 的前缀又是 \(s\) 的后缀,称 \(t\) 为 \(s\) 的一个 \(\text{Border}\)。

更加符号化地,对于一个长为 \(n\) 的字符串 \(s\),若存在 \(m\) 使得 \(m\neq n\) 且 \(s[:m-1]=s[n-m:]\),

称 \(s[:m-1]\) (\(s[n-m:]\) 同理) 为 \(s\) 的一个长度为 \(m\) 的 \(\text{Border}\)。

对于一个字符串 \(s\),定义 \(\text{MaxBorder}(s)\) 为 \(s\) 的 \(\text{Border}\) 中最长的。

接下来定义前缀函数:

对于一个字符串 \(s\),定义前缀函数 \(\pi(s,i)=\left|\text{MaxBorder}(s[:i])\right|\) 。

在不引起歧义的情况下,可简记为 \(\pi(i)\);特别地,定义 \(\pi(s,0)=0\)。

有时我们关心一整个字符串的前缀函数值,故此时我们也可以记 \(\pi(s)=\{\pi(s,0), \pi(s,1), \cdots, \pi(s,|s|-1)\}\)。

举一个实例:\(\pi(\texttt{abacaba})=\{0,0,1,0,1,2,3\}\)。

1.2. 前缀函数的重要性质

显然直接根据定义计算前缀函数的时间复杂度是不能接受的,于是我们需要依赖一些有用的性质来加速计算。

主要的性质有下面两个:

  1. \(\pi(i+1)\leqslant\pi(i)+1\),且仅当 \(s_{\pi(i)}=s_{i+1}\) 时有 \(\pi(i+1)=\pi(i)+1\)。
  2. 令 \(j\) 为 \(s[:i]\) 次长的 \(\text{Border}\) 的长度,则 \(j=\pi(\pi(i)-1)\)。

第一个性质是显然的。第二个性质由 \(s[:j-1]=s[i-j+1:i]=s[\pi(i)-j:\pi(i)-1]\) 自然导出。

1.3. 快速求前缀函数

这样我们就有了快速求前缀函数的算法:

我们从前向后迭代求。假设我们已经求出了 \(\pi(0), \pi(1), \cdots, \pi(i-1)\),现在要求 \(\pi(i)\)。

令 \(j=\pi(i-1)\)。如果 \(s_i=s_j\),则 \(\pi(i)=j+1\)。

否则,令 \(j\gets \pi(j-1)\),重复以上判断直到满足 \(s_i=s_j\) 或 \(j=0\)。

若 \(j=0\),则 \(\pi(i)=0\)。

如此重复直到前缀函数全部计算完成。可以证明,时间复杂度为 \(O(n)\)。

code:

void prefix()
{
for(int i=1;i<=n-1;i++)
{
int j=pi[i-1];
while(j>0&&s[i]!=s[j])j=pi[j-1];
if(s[i]==s[j])j++;
pi[i]=j;
}
}

1.4. 前缀函数的应用

1.4.1. KMP

luoguP3375

一般的方法是通过 \(\pi(i)\) 优化匹配,不过有个简单粗暴地多的方法。

假设模式串为 \(s\),文本串为 \(t\),\(s\) 的长度为 \(n\),\(t\) 的长度为 \(m\)。

构造字符串 \(a\gets s+\#+t\),其中 \(\#\) 是 \(s,t\) 中均不会出现的字符。

然后求 \(a\) 的前缀函数,若\(\pi(a,i)=n\),则 \(s\) 在 \(t\) 的 \(i-2n\) 处出现。

总时间复杂度 \(O(n+m)\)。代码略。

优化:注意到前缀函数是可以一个一个字符在线处理的,所以空间复杂度可优化到 \(O(n)\)。

1.4.2 字符串的周期

luoguP4391

对于字符串 \(s\),若 \(\exist p,\ 0<p\leqslant |s|\) 使 \(\forall i\in [0,|s|-p-1],\ s_i=s_{i+p}\),则称 \(p\) 是 \(s\) 的周期。

运用 \(\text{Border}\) 理论,可以发现,若 \(s\) 存在一个长为 \(l\) 的 \(\text{Border}\),则 \(|s|-l\) 是 \(s\) 的周期。

我们应用之前的结果,可知 \(|s|-\pi(|s|-1),\ |s|-\pi(\pi(|s|-1)-1),\cdots\) 为 \(s\) 的周期,其中最小的周期为 \(|s|-\pi(|s|-1)\)。

1.4.3 统计每个前缀的出现次数

根据前缀函数的定义及其性质,以 \(i\) 为右端点有长度为 \(\pi(i),\ \pi(\pi(i)-1),\cdots\) 的前缀。

我们不能按照端点来统计,因为在极端情况下,如 \(\texttt{aaaaaa}\),时间复杂度达到了平方级别。

但是注意到每一个长为 \(i\) 的前缀的出现中也一定包含长为 \(\pi(i-1)\) 的前缀的出现,因此我们可以考虑用较长的前缀的值来更新较短的前缀的值。(具体见代码)

最后别忘了加上每个前缀在初始位置出现的一次。

code:

for(int i=1;i<n;i++)ans[pi[i]]++;
for(int i=n-1;i>0;i--)ans[pi[i-1]]+=ans[i];
for(int i=1;i<=n;i++)ans[i]++;

2. Z函数

2.1. Z函数的定义

定义 \(\text{lcp}(a,b)\) 为字符串 \(a, b\) 的最长公共前缀。

定义Z函数:

对于字符串 \(s\),定义Z函数 \(z(s,i)\) 为 \(\text{lcp}(s,s[i:])\) 的长度。

在不引起歧义的情况下,可简记为 \(z(i)\);特别地,定义 \(z(s,0)=0\)。

根据以上定义,我们有 \(s[:z(i)-1]=s[i:i+z(i)-1]\);对此我们称 \(\text{Z-box}(i)=s[i:i+z(i)-1]\)。

有时我们关心一整个字符串的Z函数值,故此时我们也可以记 \(z(s)=\{z(s,0), z(s,1), \cdots, z(s,|s|-1)\}\)。

举一个实例:\(z(\texttt{abacaba})=\{0,0,1,0,3,0,1\}\)。

2.2. 快速求Z函数

想要快速求出Z函数的值,我们需要充分发掘Z函数的性质以使信息得到高效利用。

考虑若\(l \leqslant i \leqslant l+z(l)-1\),即 \(i\) 在 \(\text{Z-box}(l)\)之内:

方便起见,设 \(r=l+z(l)-1\)。

根据Z函数的定义有 \(s[i-l:r-l]=s[i:r]\),则 \(s[i-l:i-l+z(i)-1]=\text{Z-box}(i)=s[:z(i)-1]\)。

很明显,若 \(z(i-l)<r-i+1\),则直接有 \(z(i)=z(i-l)\);否则,我们能得到 \(z(i) \geqslant r-i+1\),想要得出具体数值还需向后枚举。

为了更好地利用上述性质,我们可以在计算过程中维护最大的 \(r\)。具体操作如下:

初始时令 \(l=r=0\)。

从前到后开始计算。若 \(i\leqslant r\),利用上述性质进行计算。否则,暴力枚举。

如果新的 \(r\) 比之前的更大,即 \(i+z(i)-1>r\),更新 \(l=i,\ r=i+z(i)-1\)。

code:

z[0]=l=r=0;
for(int i=1;i<n;i++)
{
if(r>i)z[i]=min(z[i-l],r-i+1);//简便起见,我们可以直接让z[i]取为z[i-l]和r-i+1中较小的一个
for(;b[i+z[i]]==b[z[i]];z[i]++);//暴力枚举
if(i+z[i]-1>r){l=i;r=i+z[i]-1;}//更新l,r
}

可以看到每个字符只会被暴力匹配一次,因此复杂度为 \(O(n)\)。

这个网站有对该算法的演示。

luoguP5410

对于这道题,我们还要求 \(b\) 与 \(a\) 的每一个后缀的 \(\text{lcp}\)。可以对之前的算法进行修改,也可以直接把 \(a\) 接到 \(b\) 的后面来做。

前缀函数与Z函数介绍的更多相关文章

  1. linux 文件名称前后缀操作函数----取目录函数dir、取文件名称函数notdir、取后缀函数suffix、取前缀basename、加后缀函数addsuffix、加前缀addprefix、连接函数join

    1.1       文件名操作函数 下面我们要介绍的函数主要是处理文件名的.每个函数的参数字符串都会被当做一个或是一系列的文件名来对待. 1.1.1        取目录函数dir $(dir < ...

  2. ORACLE常用数值函数、转换函数、字符串函数介绍

    ORACLE常用数值函数.转换函数.字符串函数介绍. 数值函数: abs(m) m的绝对值 mod(m,n) m被n除后的余数 power(m,n) m的n次方 round(m[,n]) m四舍五入至 ...

  3. 浅谈 es6 箭头函数, reduce函数介绍

    今天来谈一下箭头函数, es6的新特性 首先我们来看下箭头函数长什么样子, let result = (param1, param2) => param1+param2; 上述代码 按照以前书写 ...

  4. exkmp(Z函数) 笔记

    exkmp 用于求解这样的问题: 求文本串 \(T\) 的每一个后缀与模式串 \(M\) 的匹配长度(即最长公共前缀长度).特别的,取 \(M=T\),得到的这个长度被称为 \(Z\) 函数.&quo ...

  5. Atcoder Regular Contest 058 D - 文字列大好きいろはちゃん / Iroha Loves Strings(单调栈+Z 函数)

    洛谷题面传送门 & Atcoder 题面传送门 神仙题. mol 一发现场(bushi)独立切掉此题的 ycx %%%%%%% 首先咱们可以想到一个非常 naive 的 DP,\(dp_{i, ...

  6. KMP&Z函数详解

    KMP 一些简单的定义: 真前缀:不是整个字符串的前缀 真后缀:不是整个字符串的后缀 当然不可能这么简单的,来个重要的定义 前缀函数: 给定一个长度为\(n\)的字符串\(s\),其 \(前缀函数\) ...

  7. swift1.2语言函数和闭包函数介绍

    swift1.2语言函数和闭包函数介绍 在编程中,随着处理问题的越来越复杂,代码量飞速增加.其中,大量的代码往往相互重复或者近似重复.如果不采有效方式加以解决,代码将很难维护. swift1.2语言函 ...

  8. [SQL SERVER系列]之常用函数和开窗函数介绍及实例

    本文主要介绍SQL SERVER数据库中一些常用的系统函数及其SQL SERVER 2005以上支持的开窗函数. 1.常用函数 --从字符串右边截取指定字符数 select RIGHT('HELLO' ...

  9. NSIS文字及字符串函数与头文件介绍

    原文 NSIS文字及字符串函数与头文件介绍 文字函数,顾名思义就是处理字符串的函数.使用这些字符串函数前,必须先包含头文件WordFunc.nsh.该头文件目前包含如下一些函数:WordFind.Wo ...

随机推荐

  1. 【LeetCode】85. Maximal Rectangle 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/maximal- ...

  2. 【LeetCode】526. Beautiful Arrangement 解题报告(Python & C++)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  3. 【Java笔记】applet和html注意

    1.首先记得extends Applet 泪目 2.如果不分包,HTML可以写 <applet code="HelloWorldApplet.class" width=150 ...

  4. html5调用摄像头并拍照

    随着flash被禁用,flash上传附件的方式已成为过去,现在开始用html5上传了.本片文章就是介绍如何使用html5拍照,其实挺简单的原理: 调用摄像头采集视频流,利用canvas的特性生成bas ...

  5. [opencv]GeneralProcessing_Template_Function

    // // Created by leoxae on 2019-05-08. // #ifndef OPENCVDEMO_UTILS_H #define OPENCVDEMO_UTILS_H #inc ...

  6. 网络划分和各层协议以及webservice 浅谈

    最近在公司做一些和其他外部系统接口调用的工作,遇到一些网络传输的问题,趁周末的时间记录.整理一下. 提到网络我们不得不提网络的分层架构: 我们通常听到 网络七层架构/五层架构/四层架构,但是不了解很容 ...

  7. centos6.5-Apache优化

    Apache的网页压缩功能 一.配置网页压缩功能 在配置压缩功能以前访问网页的响应头部 Response Headers view source Accept-Ranges:bytes Connect ...

  8. 前端如何低门槛开发iOS、Android、小程序多端应用

    现如今跨平台开发技术已不是什么新鲜话题了,在市面上也有一些开源的框架可供选择,然而技术成熟.产品服务健全的平台并不多,其中也不乏推陈出新的框架值得关注. 比如最近使用的AVM,由APICloud迭代推 ...

  9. 『无为则无心』Python函数 — 34、lambda表达式

    目录 1.lambda的应用场景 2.lambda语法 3.快速入门 4.示例:计算a + b 5.lambda的参数形式 6.lambda的应用 lambda表达式的主要作用就是化简代码. 匿名函数 ...

  10. PowerShell【变量篇】

    PS C:\Users\Administrator> $str='这是一个变量' PS C:\Users\Administrator> $str 这是一个变量 PS C:\Users\Ad ...