首页 > 其他分享 >Z 函数(扩展KMP)

Z 函数(扩展KMP)

时间:2024-07-18 19:56:25浏览次数:8  
标签:子串 函数 复杂度 扩展 算法 计算 KMP 字符串

author: LeoJacob, Marcythm, minghu6

约定:字符串下标以 \(0\) 为起点。

定义

对于一个长度为 \(n\) 的字符串 \(s\),定义函数 \(z[i]\) 表示 \(s\) 和 \(s[i,n-1]\)(即以 \(s[i]\) 开头的后缀)的最长公共前缀(LCP)的长度,则 \(z\) 被称为 \(s\) 的 Z 函数。特别地,\(z[0] = 0\)。

国外一般将计算该数组的算法称为 Z Algorithm,而国内则称其为 扩展 KMP

这篇文章介绍在 \(O(n)\) 时间复杂度内计算 Z 函数的算法以及其各种应用。

解释

下面若干样例展示了对于不同字符串的 Z 函数:

  • \(z(\mathtt{aaaaa}) = [0, 4, 3, 2, 1]\)
  • \(z(\mathtt{aaabaab}) = [0, 2, 1, 0, 2, 1, 0]\)
  • \(z(\mathtt{abacaba}) = [0, 0, 1, 0, 3, 0, 1]\)

朴素算法

Z 函数的朴素算法复杂度为 \(O(n^2)\):
C++

vector<int> z_function_trivial(string s) {
	int n = (int)s.length();
	vector<int> z(n);
	for (int i = 1; i < n; ++i)
		while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
	return z;
}

Python

def z_function_trivial(s):
n = len(s)
z = [0] * n
for i in range(1, n):
	while i + z[i] < n and s[z[i]] == s[i + z[i]]:
		z[i] += 1
		return z

线性算法

如同大多数字符串主题所介绍的算法,其关键在于,运用自动机的思想寻找限制条件下的状态转移函数,使得可以借助之前的状态来加速计算新的状态。

在该算法中,我们从 \(1\) 到 \(n-1\) 顺次计算 \(z[i]\) 的值(\(z[0]=0\))。在计算 \(z[i]\) 的过程中,我们会利用已经计算好的 \(z[0],\ldots,z[i-1]\)。

对于 \(i\),我们称区间 \([i,i+z[i]-1]\) 是 \(i\) 的 匹配段,也可以叫 Z-box。

算法的过程中我们维护右端点最靠右的匹配段。为了方便,记作 \([l,r]\)。根据定义,\(s[l,r]\) 是 \(s\) 的前缀。在计算 \(z[i]\) 时我们保证 \(l\le i\)。初始时 \(l=r=0\)。

在计算 \(z[i]\) 的过程中:

  • 如果 \(i\le r\),那么根据 \([l,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\),然后暴力枚举下一个字符扩展 \(z[i]\) 直到不能扩展为止。
  • 如果 \(i>r\),那么我们直接按照朴素算法,从 \(s[i]\) 开始比较,暴力求出 \(z[i]\)。
  • 在求出 \(z[i]\) 后,如果 \(i+z[i]-1>r\),我们就需要更新 \([l,r]\),即令 \(l=i, r=i+z[i]-1\)。

可以访问 这个网站 来看 Z 函数的模拟过程。
C++

	vector<int> z_function(string s) {
		int n = (int)s.length();
		vector<int> z(n);
		for (int i = 1, l = 0, r = 0; i < n; ++i) {
			if (i <= r && z[i - l] < r - i + 1) {
				z[i] = z[i - l];
			} else {
				z[i] = max(0, r - i + 1);
				while (i + z[i] < n && s[z[i]] == s[i + z[i]]) ++z[i];
			}
			if (i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
		}
		return z;
	}

Python

def z_function(s):
n = len(s)
z = [0] * n
l, r = 0, 0
for i in range(1, n):
	if i <= r and z[i - l] < r - i + 1:
		z[i] = z[i - l]
		else:
			z[i] = max(0, r - i + 1)
			while i + z[i] < n and s[z[i]] == s[i + z[i]]:
				z[i] += 1
				if i + z[i] - 1 > r:
					l = i
					r = i + z[i] - 1
					return z

复杂度分析

对于内层 while 循环,每次执行都会使得 \(r\) 向后移至少 \(1\) 位,而 \(r< n-1\),所以总共只会执行 \(n\) 次。

对于外层循环,只有一遍线性遍历。

总复杂度为 \(O(n)\)。

应用

我们现在来考虑在若干具体情况下 Z 函数的应用。

这些应用在很大程度上同 前缀函数 的应用类似。

匹配所有子串

为了避免混淆,我们将 \(t\) 称作 文本,将 \(p\) 称作 模式。所给出的问题是:寻找在文本 \(t\) 中模式 \(p\) 的所有出现(occurrence)。

为了解决该问题,我们构造一个新的字符串 \(s = p + \diamond + t\),也即我们将 \(p\) 和 \(t\) 连接在一起,但是在中间放置了一个分割字符 \(\diamond\)(我们将如此选取 \(\diamond\) 使得其必定不出现在 \(p\) 和 \(t\) 中)。

首先计算 \(s\) 的 Z 函数。接下来,对于在区间 \([0,|t| - 1]\) 中的任意 \(i\),我们考虑以 \(t[i]\) 为开头的后缀在 \(s\) 中的 Z 函数值 \(k = z[i + |p| + 1]\)。如果 \(k = |p|\),那么我们知道有一个 \(p\) 的出现位于 \(t\) 的第 \(i\) 个位置,否则没有 \(p\) 的出现位于 \(t\) 的第 \(i\) 个位置。

其时间复杂度(同时也是其空间复杂度)为 \(O(|t| + |p|)\)。

本质不同子串数

给定一个长度为 \(n\) 的字符串 \(s\),计算 \(s\) 的本质不同子串的数目。

考虑计算增量,即在知道当前 \(s\) 的本质不同子串数的情况下,计算出在 \(s\) 末尾添加一个字符后的本质不同子串数。

令 \(k\) 为当前 \(s\) 的本质不同子串数。我们添加一个新的字符 \(c\) 至 \(s\) 的末尾。显然,会出现一些以 \(c\) 结尾的新的子串(以 \(c\) 结尾且之前未出现过的子串)。

设串 \(t\) 是 \(s + c\) 的反串(反串指将原字符串的字符倒序排列形成的字符串)。我们的任务是计算有多少 \(t\) 的前缀未在 \(t\) 的其他地方出现。考虑计算 \(t\) 的 Z 函数并找到其最大值 \(z_{\max}\)。则 \(t\) 的长度小于等于 \(z_{\max}\) 的前缀的反串在 \(s\) 中是已经出现过的以 \(c\) 结尾的子串。

所以,将字符 \(c\) 添加至 \(s\) 后新出现的子串数目为 \(|t| - z_{\max}\)。

算法时间复杂度为 \(O(n^2)\)。

值得注意的是,我们可以用同样的方法在 \(O(n)\) 时间内,重新计算在端点处添加一个字符或者删除一个字符(从尾或者头)后的本质不同子串数目。

字符串整周期

给定一个长度为 \(n\) 的字符串 \(s\),找到其最短的整周期,即寻找一个最短的字符串 \(t\),使得 \(s\) 可以被若干个 \(t\) 拼接而成的字符串表示。

考虑计算 \(s\) 的 Z 函数,则其整周期的长度为最小的 \(n\) 的因数 \(i\),满足 \(i+z[i]=n\)。

该事实的证明同应用 前缀函数 的证明一样。

练习题目


本页面主要译自博文 Z-функция строки и её вычисление 与其英文翻译版 Z-function and its calculation。其中俄文版版权协议为 Public Domain + Leave a Link;英文版版权协议为 CC-BY-SA 4.0。

标签:子串,函数,复杂度,扩展,算法,计算,KMP,字符串
From: https://www.cnblogs.com/gutongxing/p/18310328

相关文章

  • 嵌入式学习——C语言字符数组及其函数
    目录一、字符数组    1、定义    2、初始化                    3、引用字符数组元素二、字符串和字符串结束的标志三、字符数组的输入输出        1、字符串的输入:scanf    2、注意事项四、字符串处理函数......
  • 第八章函数及常用的内置函数
    函数的定义和调用1、内置函数:输出函数print()输入函数input()列表定义的函数list()2、自定义函数def函数名称(参数列表):函数体[return返回值列表]3、函数调用函数名(参数列表)不带返回值的函数直接调用,带返回值的函数调用后要将结果保存到变量中点击查看......
  • 扩展 KMP/exKMP(Z 函数)学习笔记
    声明本文章转载自shangruolin的博客,已经过作者(存疑)同意,帮TA宣传一下。扩展KMP/exKMP(Z函数)学习笔记兼P10479匹配统计题解。LCP:最长公共前缀。Z函数,又称扩展KMP(exKMP),能够在\(O(n)\)的时间内求出一个字符串与其所有后缀的LCP的长度。定义\(z_i\)为字符串\(s\)......
  • 【算法】JZ30 包含min函数的栈
    1.概述描述定义栈的数据结构,请在该类型中实现一个能够得到栈中所含最小元素的min函数,输入操作时保证pop、top和min函数操作时,栈中一定有元素。此栈包含的方法有:push(value):将value压入栈中pop():弹出栈顶元素top():获取栈顶元素min():获取栈中最小元素......
  • [php命令执行函数]详解各种php命令执行函数
    如下几种命令执行函数:目录systemexcpassthrushell_exec反引号``popensystemsystem函数简介:用于执行命令语法形式:system(string$command,int$return_var=?)command:必选参数,字符类型,被system函数执行的命令,如lsreturn_var:可选参数,整数类型,如果提供此参数,则com......
  • 虚函数
    多态(polymorphism)是面向对象编程语言的一大特点,而虚函数是实现多态的机制。其核心理念就是通过基类访问派生类定义的函数。多态性使得程序调用的函数是在运行时动态确定的,而不是在编译时静态确定的。使用一个基类类型的指针或者引用,来指向子类对象,进而调用由子类复写的个性化的虚......
  • FlowUs的愿景远不止于简单的信息存储和阅读,而是致力于构建一个平台,让用户可以自由创作
    FlowUs在2024年以全新的姿态和强大的功能,为个人和团队带来了更高效、便捷和创新的工作体验。作为一款综合性的工作平台,FlowUs融合了笔记、项目管理、文档协作等多种功能,旨在满足用户在不同场景下的需求,成为提升工作效率和创造力的得力助手。......
  • 超长上下文扩展:LongLoRA & LongQLoRA
    学习链接https://blog.csdn.net/v_JULY_v/article/details/135375799目录从LongLoRA到LongQLoRA(含源码剖析):超长上下文大模型的高效微调方法第一部分LongLora:超长上下文大模型的高效微调方法1.1从PI、LoRA到LongLora1.1.1面对长文本:PI和LoRA在各自角度上的不足1.1.2LongLor......
  • 三角函数
    三角函数学习笔记三角形标准记号三角函数的定义正弦$$\sin(α)=\frac{对边}{斜边}=\frac{a}{c}$$余弦$$\cos(α)=\frac{邻边}{斜边}=\frac{b}{c}$$正切$$\tan(α)=\frac{对边}{邻边}=\frac{a}{b}$$余切$$\cot(α)=\frac{邻边}{对边}=\frac{b}{a}$$正......
  • 母函数与高斯和
    前置知识:单位根(为了偷懒,基本将所有的\(\omega\)都写成了\(w\))CP1我们很经常遇到的一个问题是\(xy~mod~n\)的求解,母函数在处理这样的一些问题时会有效(更优先的是寻求\(xy\)的性质或者利用带余除法)例1设\(p>2\)是素数,\(p\nmidabcd\),满足\(\{\frac{ra}p\}+\{\frac......