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\)种情况:

  1. \(zr<i\)。此时我们直接暴力地从第\(i\)位向后匹配求出\(z_{a,i}\)。如果\(z_{a,i}\ne0\),则令\(zl=i,zr=i+z_{a,i}-1\);
  2. \(zr\ge i\)。设\(i-zl+1=i'\),即\(i'\)是把跨越\(i\)的Z-box\([zl,zr]\)平移至\(a\)的前缀处后\(i\)的位置。此时又分\(2\)种情况:
    1. \(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\)不变;
    2. \(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算法的更多相关文章

  1. 【算法】字符串匹配之Z算法

    求文本与单模式串匹配,通常会使用KMP算法.后来接触到了Z算法,感觉Z算法也相当精妙.在以前的博文中也有过用Z算法来解决字符串匹配的题目. 下面介绍一下Z算法. 先一句话讲清楚Z算法能求什么东西. 输 ...

  2. Codeforces 126B Password(Z算法)

    题意 给定一个字符串 \(s\) ,求一个子串 \(t\) 满足 \(t\) 是 \(s\) 的前缀.后缀且在除前缀后缀之外的地方出现过. \(1 \leq |s| \leq 10^6\) 思路 \( ...

  3. CodeForces - 1051E :Vasya and Big Integers(Z算法 & DP )

    题意:给定字符串S,A,B.现在让你对S进行切割,使得每个切割出来的部分在[A,B]范围内,问方案数. 思路:有方程,dp[i]=Σ dp[j]   (S[j+1,i]在合法范围内).    假设M和 ...

  4. Z算法板子

    给定一个串$s$, $Z$算法可以$O(n)$时间求出一个$z$数组 $z_i$表示$s[i...n]$与$s$的前缀匹配的最长长度, 下标从$0$开始 void init(char *s, int ...

  5. Zbar和Z*算法对比

    博客转载自:https://blog.csdn.net/qishandaxue/article/details/45481387 移植zbar和zxing源码到linux平台,zbar移植的是C源码, ...

  6. [转] Manacher算法详解

    转载自: http://blog.csdn.net/dyx404514/article/details/42061017 Manacher算法 算法总结第三弹 manacher算法,前面讲了两个字符串 ...

  7. 计算字符串的最长回文子串 :Manacher算法介绍

    转自: http://www.open-open.com/lib/view/open1419150233417.html Manacher算法 在介绍算法之前,首先介绍一下什么是回文串,所谓回文串,简 ...

  8. CF #93 div1 B. Password KMP/Z

    题目链接:http://codeforces.com/problemset/problem/126/B 大意:给一个字符串,问最长的既是前缀又是后缀又是中缀(这里指在内部出现)的子串. 我自己的做法是 ...

  9. 最长回文子串问题-Manacher算法

    转:http://blog.csdn.net/dyx404514/article/details/42061017 Manacher算法 算法总结第三弹 manacher算法,前面讲了两个字符串相算法 ...

随机推荐

  1. ~~函数基础(三):嵌套函数&匿名函数~~

    进击のpython 嵌套函数&匿名函数 讲完作用域之后 对变量的作用范围有大致的了解了吗? 讲个稍微小进阶的东西吧 能够帮助你更加的理解全局和局部变量 嵌套函数 玩过俄罗斯套娃不? 没玩过听过 ...

  2. Android开发-实现第三方APP跳转

    自己创建一个按钮: <Button android:id="@+id/btn_button" android:layout_width="fill_parent&q ...

  3. TCP/IP协议-网络编程

    本文转载自公众号“呆呆熊一点通”,作者:呆呆 开篇语 前两年, 就买了<TCP/IP网络编程>这本书, 由于自身基础薄弱, 只是走马观花翻阅了几张. 后来工作了这些年, 越来越感到瓶颈期已 ...

  4. 学习springboot整合mybatis并编写测试类

    报名立减200元.暑假直降6888. 邀请链接:http://www.jnshu.com/login/1/20535344 邀请码:20535344 遇到的问题: 1.原因是在启动类上只有一个@Map ...

  5. vue--组件性别选择器和仿百度搜索栏

    目录 实现原理 性别选择器 仿百度搜索栏 实现原理 主要参考vue官网上的自定义事件,父组件v-bind给子组件传参数,子组件利用props来接受父组件那边传过来的参数.我们还会遇到一个问题,怎么实时 ...

  6. Java编程思想:嵌套类

    public class Test { public static void main(String[] args) { // Parcell11.test(); // ClassInterface. ...

  7. koa2服务端使用jwt进行鉴权及路由权限分发

    大体思路 后端书写REST api时,有一些api是非常敏感的,比如获取用户个人信息,查看所有用户列表,修改密码等.如果不对这些api进行保护,那么别人就可以很容易地获取并调用这些 api 进行操作. ...

  8. Excel催化剂开源第47波-Excel与PowerBIDeskTop互通互联之第一篇

    当国外都在追求软件开源,并且在GitHub等平台上产生了大量优质的开源代码时,但在国内却在刮着一股收割小白智商税的知识付费热潮,实在可悲. 互联网的精神乃是分享,让分享带来更多人的受益. 在Power ...

  9. 【转】Spring事务详解

    1.事务的基本原理 Spring事务的本质其实就是数据库对事务的支持,使用JDBC的事务管理机制,就是利用java.sql.Connection对象完成对事务的提交,那在没有Spring帮我们管理事务 ...

  10. 利用DOMNodeInserted监听标签内容变化

    var exeFlag = 0;//控制执行业务次数标记$('#list1').bind('DOMNodeInserted', function () { if(!/img/.test($(" ...