Z算法
Z算法
Z算法是一种用于字符串匹配的算法。此算法的核心在于\(z\)数组以及它的求法。
(以下约定字符串下标从\(1\)开始)
\(z\)数组和Z-box
定义\(z\)数组:\(z_{a,i}\)表示从字符串\(a\)的第\(i\)位开始,往后能与\(a\)的前缀匹配的最长长度。显然,\(z_{a,1}=|a|\)恒成立。
一个Z-box是一个区间。给定一个字符串\(a\),那么\(a\)上存在一个Z-box\([l,r]\)当且仅当满足以下全部条件:
- \(l\ne1\);
- \(z_{a,l}\ne0\);
- \(r=l+z_{a,l}-1\)。
通俗来说,若从\(a\)的第\(i\)位开始能与\(a\)的前缀匹配至少\(1\)位,那么能匹配的最长的串覆盖过的区间就是一个Z-box。(\(l\ne1\)是因为位置\(1\)很特殊,本身就是前缀,单独考虑)
例如若\(a=\texttt{acactaac}\),那么\(z_{a}=[8,0,2,0,0,1,2,0]\),Z-box有\([3,4],[6,6],[7,8]\)。
\(z\)数组的求法
给定字符串\(a\),现在我们需要求出\(z_{a}\)。
由于\(z_{a,1}\)的值不用求,而且位置\(1\)比较特殊,就是前缀,所以我们单独处理。
假设我们现在已经知道了\(z_{a,2\sim i-1}\)和使得\(zr\)最大的Z-box\([zl,zr]\),要求出\(z_{a,i}\)并更新\(zl,zr\),那么分\(2\)种情况:
- \(zr<i\)。此时我们直接暴力地从第\(i\)位向后匹配求出\(z_{a,i}\)。如果\(z_{a,i}\ne0\),则令\(zl=i,zr=i+z_{a,i}-1\);
- \(zr\ge i\)。设\(i-zl+1=i'\),即\(i'\)是把跨越\(i\)的Z-box\([zl,zr]\)平移至\(a\)的前缀处后\(i\)的位置。此时又分\(2\)种情况:
- \(i+z_{a,i'}\le zr\)。显然\(\left[i,i+z_{a,i'}\right]\subsetneq[zl,zr]\)。根据Z-box的定义,\(\forall j\in\left[i,i+z_{a,i'}\right],a_j=a_{j-zl+1}\)。那么从\(a\)的第\(i\)位开始与\(a\)的前缀匹配的情况和从第\(i'\)位开始是一样的,直接令\(z_{a,i}=z_{a,i'}\),\(zl,zr\)不变;
- \(i+z_{a,i'}>zr\)。同理,\(\forall j\in[i,zr],a_j=a_{j-zl+1}\)。那么\(a\)的第\(i\sim zr\)位与\(a\)的前缀匹配的情况和第\(i'\sim zr-zl+1\)位是一样的,显然\(z_{a,i}\)至少有\(zr-i+1\)这么多,于是直接从第\(zr+1\)位开始暴力向后匹配求出\(z_{a,i}\),并令\(zl=i,zr=i+z_{a,i}-1\)(因为\(z_{a,i}\)不可能为\(0\))。
这样先令\(z_1=|a|\),然后按上述方法从\(i=2\)递推到\(i=|a|\),便可求出\(z_a\)数组。
下面是求\(z\)数组的代码:
//|a|=n
void z_init(){//求z数组
z[1]=n;//特殊处理z[1]
int zl=0,zr=0;//右端点最大的Z-box
for(int i=2;i<=n;i++)//从i=2递推到i=n
if(zr<i){//第1种情况
z[i]=0;
while(i+z[i]<=n&&a[i+z[i]]==a[1+z[i]])z[i]++;//直接向后暴力匹配
if(z[i])zl=i,zr=i+z[i]-1;//更新右端点最大的Z-box
}
else if(i+z[i-zl+1]<=zr)z[i]=z[i-zl+1];//第2种情况的第1种情况
else{//第2种情况的第2种情况
z[i]=zr-i+1;//z[i]至少有zr-i+1这么多
while(i+z[i]<=n&&a[i+z[i]]==a[1+z[i]])z[i]++;//后面再暴力匹配
zl=i;zr=i+z[i]-1;//更新右端点最大的Z-box
}
}
时间复杂度
按上述方法求\(z\)数组的时间复杂度是线性的\(\mathrm{O}(|a|)\)。
证明(感性):观察上述方法可发现,只有当\(i>zr\)时,才可能将这个位置的字符与前缀匹配,而匹配结束后会把\(zr\)更新至最后一个匹配成功的位置,所以每个字符最多会和前缀成功匹配\(1\)次,所以匹配成功的总次数为\(\mathrm{O}(|a|)\);算\(z_{a,i}\)时,如果往后暴力匹配(即遇到的不是第\(2\)种情况的第\(1\)种情况),那么第\(1\)次匹配失败就会停下来,所以匹配失败的总次数也为\(\mathrm{O}(|a|)\)。因此总时间就是匹配所花的时间\(\mathrm{O}(|a|)+\mathrm{O}(|a|)=\mathrm O(|a|)\)再加上一些赋值、更新\(zl,zr\)等一些\(1\)次只要\(\mathrm O(1)\)的操作,就还是\(\mathrm O(|a|)\)了。得证。
应用
Z算法和ExKMP算法是完全等价的,因为它们求的数组的意思是一样的。但是哈希、KMP能求的东西却有Z算法力所不及的。
Z算法最常用的用法就是字符串模式匹配(这个哈希和KMP也可以做到线性复杂度)。考虑把模式串\(b\)隔一个不常用字符接到文本串\(a\)前面,即令\(c=b+\texttt{!}+a\)。然后求出\(z_c\),从\(i=|b|+2\)到\(i=|c|\)扫一遍,如果\(z_i=|b|\),那么在该位置匹配成功。注意:所谓不常用字符一定不能在串中出现,不然会出bug。如果要用模式串\(c\)去匹配两个文本串\(a,b\),可以令\(d=c+\texttt{!}+a+\texttt @+b\),这时两个分隔符不能相同,不然也会出bug。
为什么Z算法在字符串模式匹配上花的时间和哈希相同呢?Z算法算出了从每一位开始能与前缀匹配的最长长度,但是字符串模式匹配只需要知道能否与前缀\(c_{1\sim|b|}\)匹配,并未完全使用\(z\)数组的价值。如果你就是想知道某一位开始能与前缀匹配的最长长度,哈希可就要二分的帮助了,复杂度是带\(\log\)的,不如用Z算法预处理一下。具体的可以参考下面\(3\)道例题。
不仅如此,Z算法的常数比哈希小(因为为了使哈希不被卡、不在CodeForces上FST,一般要写双重哈希),正确率也比哈希高(Z算法正确率当然是\(100\%\)啦)。
例题
CodeForces 526D - Om Nom and Necklace
CodeForces 427D - Match & Catch
CodeForces 955D - Scissors
Z算法的更多相关文章
- 【算法】字符串匹配之Z算法
求文本与单模式串匹配,通常会使用KMP算法.后来接触到了Z算法,感觉Z算法也相当精妙.在以前的博文中也有过用Z算法来解决字符串匹配的题目. 下面介绍一下Z算法. 先一句话讲清楚Z算法能求什么东西. 输 ...
- Codeforces 126B Password(Z算法)
题意 给定一个字符串 \(s\) ,求一个子串 \(t\) 满足 \(t\) 是 \(s\) 的前缀.后缀且在除前缀后缀之外的地方出现过. \(1 \leq |s| \leq 10^6\) 思路 \( ...
- CodeForces - 1051E :Vasya and Big Integers(Z算法 & DP )
题意:给定字符串S,A,B.现在让你对S进行切割,使得每个切割出来的部分在[A,B]范围内,问方案数. 思路:有方程,dp[i]=Σ dp[j] (S[j+1,i]在合法范围内). 假设M和 ...
- Z算法板子
给定一个串$s$, $Z$算法可以$O(n)$时间求出一个$z$数组 $z_i$表示$s[i...n]$与$s$的前缀匹配的最长长度, 下标从$0$开始 void init(char *s, int ...
- Zbar和Z*算法对比
博客转载自:https://blog.csdn.net/qishandaxue/article/details/45481387 移植zbar和zxing源码到linux平台,zbar移植的是C源码, ...
- [转] Manacher算法详解
转载自: http://blog.csdn.net/dyx404514/article/details/42061017 Manacher算法 算法总结第三弹 manacher算法,前面讲了两个字符串 ...
- 计算字符串的最长回文子串 :Manacher算法介绍
转自: http://www.open-open.com/lib/view/open1419150233417.html Manacher算法 在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简 ...
- CF #93 div1 B. Password KMP/Z
题目链接:http://codeforces.com/problemset/problem/126/B 大意:给一个字符串,问最长的既是前缀又是后缀又是中缀(这里指在内部出现)的子串. 我自己的做法是 ...
- 最长回文子串问题-Manacher算法
转:http://blog.csdn.net/dyx404514/article/details/42061017 Manacher算法 算法总结第三弹 manacher算法,前面讲了两个字符串相算法 ...
随机推荐
- C语言学习书籍推荐《C Primer Plus(中文版)(第5版)》下载
普拉塔 (Prata S.) (作者), 云巅工作室 (译者) <C Primer Plus(中文版)(第5版)>共17章,介绍了C语言的基础知识,包括数据类型.格式化输入输出.运算符.表 ...
- Java并发之线程封闭
读者们好! 在这篇博客中,我们将探讨线程封闭是什么意思,以及我们如何实现它. 所以,让我们直接开始吧. 1. 线程封闭 大多数的并发问题仅发生在我们想要在线程之间共享可变变量或可变状态时.如果在多个线 ...
- C#使用sqlite-net搭建简易的ORM
SQLite简易版ORM 首先打开项目,使用nuget搜索sqlite-net,如下图: 下载完成后,我们会多出两个文件,SQLite.cs和SQLiteAsync.cs. 我们新建一个文件夹SQLi ...
- python的@修饰符
‘@’引用已有的函数,对下面的函数进行修饰.引用函数必须放在修饰函数的上面,引用函数的返回值,返回给被修饰的函数 一个简单的栗子: def funA(fn): print('A') # 输出A fn( ...
- 手动部署EJB于WebLogic
转载自http://blog.sina.com.cn/s/blog_678530f60100hy6c.html 说是转载,其实是我个人几年前在新浪博客上发表的一篇文章 上一篇说道如何使用Eclipse ...
- 20190101.DDD笔记
建立领域模型步骤 根据提供的信息完善主要业务场景和业务流程: 根据业务流程识别领域事件并按照时序排列: 针对领域事件进行命令识别: 针对领域事件和命令进行聚合和子域的初步识别: 在识别的subdoma ...
- KVM :vnc 远程控制kvm创建虚拟机
一.vnc远程控制服务器 前期准备: 1.编辑/etc/hosts vi /etc/hosts 10.1.16.32 kvm 2.关闭防火墙 service iptables stop 3.关闭sel ...
- 批量替换git目录的远程仓库URL地址脚本
需求: 1. 输入work-dir 工作目录 2. 扫描工作目录中的子目录 3. 对每一个子目录, 判断是否是git repo 4. 确认是git repo, 获取git origin remote- ...
- Python解释器安装教程以及环境变量配置 以及 pycharm的安装与激活
计算机的组成 主板:人的骨架,用于拓展设备 CPU:人的大脑,用于计算和逻辑处理 硬盘:存储数据(永久储存),比如电脑上的 C盘,D盘 内存:临时记忆(断电即消失) 操作系统:XP win7 win1 ...
- SpringBoot Jpa入门案例
版权声明:署名,允许他人基于本文进行创作,且必须基于与原先许可协议相同的许可协议分发本文 (Creative Commons) 我们先来了解一下是什么是springboot jpa,springboo ...