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. jdk源码--LinkedList

    本文基于jdk1.8_171 LinkedList介绍 之前看了ArrayList,内部是一个数组.这次看了LinkedList,作用和ArrayList一样,但是内部是链表形式.链表结构如下图: 数 ...

  2. ~~函数基础(一):函数&参数~~

    进击のpython 函数基础(一):参数 ==说明一点奥,如果你前置文章没有看懂,那么不建议你直接看这个== ==因为看了也是浪费时间,没有意义== 好,接下来就是Python的灵魂部分了--函数 在 ...

  3. python如何将一个多位数数值转换为列表类型

    现在:a = 10,由于暂时没找到更好的方法,且使用下面的方法进行转换. 目标:转化为['10'] 以下为错误尝试: 1.直接转换,提示整型对象不可迭代. 2.先转换为字符串,再转换为列表,发现被分成 ...

  4. 码云及Git的使用

    什么是码云 码云就是相当一个远程仓库,在以后的工作中,你和同事负责工作的不同部分,齐头并进,最后上传到码云,类似于一个汇总的作用. 同一个绳上的不同分支 码云网址链接:https://gitee.co ...

  5. 数据结构-哈夫曼树(python实现)

    好,前面我们介绍了一般二叉树.完全二叉树.满二叉树,这篇文章呢,我们要介绍的是哈夫曼树. 哈夫曼树也叫最优二叉树,与哈夫曼树相关的概念还有哈夫曼编码,这两者其实是相同的.哈夫曼编码是哈夫曼在1952年 ...

  6. [leetcode] 67. Add Binary (easy)

    原题链接 思路: 用一个数保存进制,从后往前不断pop出两个数字和进制数相加,放入返回值中. var addBinary = function(a, b) { var arrA = a.split(' ...

  7. 2019年7月16日 abp(net core)+easyui+efcore实现仓储管理系统——多语言(十)

    abp(net core)+easyui+efcore实现仓储管理系统目录 abp(net core)+easyui+efcore实现仓储管理系统——ABP总体介绍(一) abp(net core)+ ...

  8. 201803-1跳一跳 CCF (C语言)

    问题描述 近来,跳一跳这款小游戏风靡全国,受到不少玩家的喜爱. 简化后的跳一跳规则如下:玩家每次从当前方块跳到下一个方块,如果没有跳到下一个方块上则游戏结束. 如果跳到了方块上,但没有跳到方块的中心则 ...

  9. python课堂整理17---文件操作(上)

    1.在同一目录下新建文本文件 “爱了” 2.在该文件下写入内容,同时留意pycharm右下角的编码格式为 utf- 8 3.下面程序中的read函数会索引系统默认的编码格式,winx下是gbk ,所以 ...

  10. python课堂整理13---函数的作用域及匿名函数

    name = 'alex' def foo(): name = 'jinling' def bar(): print(name) return bar a = foo() print(a) 阅读上述代 ...