首页 > 其他分享 >【笔记/模板】KMP 与 Z 函数

【笔记/模板】KMP 与 Z 函数

时间:2024-11-03 15:01:46浏览次数:1  
标签:dots 函数 笔记 KMP 字符串 border 模板 前缀

前缀函数

前缀函数通常称为 border,一个字符串 \(S\) 的 border 定义为它的一个前缀子串 \(t(t \ne S)\),满足 \(t\) 既是 \(S\) 的前缀,也是 \(S\) 的后缀。下文的 border 均为 \(S\) 的最长 border 长度。

简单来说,对于一个字符串 \(S = \texttt{abcabcd}\)(下标从 \(1\) 开始),它的前缀函数为 \([0, 1, 0, 1, 2, 2, 3]\)。

计算方法

单纯暴力

从左向右遍历一遍,每次循环一次长度,每次检查遍历一遍子串,复杂度为 \(O(n^3)\)。

边界优化

从左向右遍历的过程中,子串的大小每次只会增加 \(1\),而原先的前缀不会改变,因此 \(border_i\) 的长度最多只会比 \(border_{i - 1}\) 大 \(1\),在这种情况下:

\[S[i] = S[border_{i - 1} + 1] \Rightarrow S[1 \dots border_{i - 1}] = S[i - 1 - border_{i - 1} + 1 \dots i - 1] \\ \Rightarrow S[1 \dots border_{i - 1} + 1] = S[i - 1 - border_{i - 1} + 1 \dots i] \\ \Rightarrow border_i = border_{i - 1} + 1 \]

可见这种优化下,字符串比较次数与 border 有关,border 越长,字符串越长,而比较次数越短,根据势能分析可知,在这种情况下,字符串比较的时间复杂度为 \(O(n)\),总共的时间复杂度为 \(O(n^2)\)。

遍历优化

不难发现,每一次失配之后,我们都会将 \(border_i\) 减 \(1\),从而造成了大量的浪费,考虑从这方面入手。

目标就是找到一个下标 \(j\),满足 \(S[1 \dots j] = S[i - j + 1 \dots i]\),我们可以惊奇的发现,在之前的子串 \(S[1 \dots border_{i - 1}]\) 之中也存在一个 \(k = border_{border_{i - 1}}\),满足:

\[S[1 \dots k] = S[i - 1 - k + 1 \dots i - 1] \]

这个 \(k\) 值一定是除了 \(border_{i - 1}\) 之外最大的 \(j\),因为他本身是子串 \(S[1 \dots border_{i - 1}]\) 的最大的 border,到这里操作就十分简单了,每次失配后让 \(j \leftarrow border_{j}\) ,直到 \(S[i] = S[j + 1]\) 或者 \(j = 0\) 即可。

实现代码

for (int i = 2, j = 0; i <= m; i ++)	// i == 1 时 t 不是 s 的真子串
{
    while (j && b[i] != b[j + 1]) j = nxt[j];
    if (b[i] == b[j + 1]) j ++;
    nxt[i] = j;
}

KMP 算法

P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

KMP 算法是前缀函数的典型应用。

算法思路

我们定义 \(S\) 为文本串,\(T\) 为模式串,要求找到 \(S\) 中所有 \(T\) 的位置。

这个问题可以简单地进行转化:定义 \(f_i\) 表示满足 \(T[1 \dots f_i] = S[i - f_i + 1 \dots i]\) 的最大值,不难发现,如果 \(f_i = |T|\),那么 \(S\) 中的位置 \(i - |T| + 1\) 便是 \(T\) 的一个位置。

思路十分简单,先求出 \(T\) 的前缀函数,在用 \(T\) 的前缀函数 \(nxt[]\) 用来更新 \(f_i\)。

代码实现

// a:文本串 b:模式串 n:|a| m:|b| 下标为 1
for (int i = 2, j = 0; i <= m; i ++)
{
    while (j && b[i] != b[j + 1]) j = kmp[j];
    if (b[i] == b[j + 1]) j ++;
    kmp[i] = j;
}

for (int i = 1, j = 0; i <= n; i ++)
{
    while (j && a[i] != b[j + 1]) j = kmp[j];
    if (a[i] == b[j + 1]) j ++;
    if (j == m) cout << i - m + 1 << '\n';
}

Z 函数 / exKMP

P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Z 函数与前缀函数的定义相似,关于字符串 \(S\) 的 Z 函数中 \(z_i\) 表示满足 \(S[1 \dots z_i] = S[i \dots i + z_i - 1]\) 的最大值。

计算方法

由于不同的性质,Z 函数的求法与前缀函数完全不同,采取的是类似 Manacher 的双指针递推方法。

假设我们处理完了前 \(i - 1\) 个,令 \(r\) 表示前 \(i - 1\) 满足 Z 函数定义的最右边界,\(l\) 则是与 \(r\) 相对应的左边界(也就是满足 \(r\) 的下标)。对于第 \(i\) 个子串,有如下几种情况:

  • \(i \gt r\) :说明前面的处理与第 \(i\) 个无关,直接暴力遍历即可。
  • \(i \le r\):根据定义,\(S[1 \dots z_l = S[l \dots r]\),然而 \(i \le r\),那么推出 \(S[i - l + 1 \dots z_l = S[i \dots r]\),既然如此,我们让 \(z_i \leftarrow \min (z_{i - l + 1}, r - i + 1)\),取 \(\min\) 的原因是无法保证 \(r\) 的右边字符串是否仍然相等。

和 Manacher 算法一样,指针 \(r\) 只会向右移动,每次更新也会更新 \(r\),时间复杂度为 \(O(n)\)。

代码实现

void Z_Function()	// 下标为 1
{
	z[1] = m;	// i == 1 时子串是其本身
	for (int i = 2, l = 0, r = 0; i <= m; i ++)
	{
		int k = i > r ? 0 : min(z[i - l + 1], r - i + 1);	// 分类讨论
		while (i + k <= m && b[i + k] == b[k + 1]) k ++;
		z[i] = k;
		if (i + k - 1 > r) l = i, r = i + k - 1;	// 更新 l 和 r
	}
}

exKMP

求出字符串 \(S\) 与字符串 \(T\) 中每一个后缀的 LCP(Longest Common Prefix)。

采取与 KMP 算法类似的思想,先预处理出 \(T\) 与其本身的 Z 函数,再用这个 Z 函数更新 \(S\) 串即可。

代码实现

// a:S b:T n:|S| m:|T|
void exKMP()
{
	for (int i = 1, l = 0, r = 0; i <= n ; i ++)
	{
		int k = i > r ? 0 : min(z[i - l + 1], r - i + 1);
		while (i + k <= n && a[i + k] == b[k + 1]) k ++;
		p[i] = k, ansp ^= (p[i] + 1) * i;
		if (i + k - 1 > r) l = i, r = i + k - 1;
	}
}

Reference

前缀函数与 KMP 算法 - OI Wiki (oi-wiki.org)

P3375 【模板】KMP - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

Z 函数(扩展 KMP) - OI Wiki (oi-wiki.org)

P5410 【模板】扩展 KMP/exKMP(Z 函数) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

标签:dots,函数,笔记,KMP,字符串,border,模板,前缀
From: https://www.cnblogs.com/ThySecret/p/18523441

相关文章

  • 【模板】图的存储与遍历
    图的存储与遍历直接存边法做法建立三个数组u[N],v[N],w[N],对于每一次读入的起点,出点和权值存储即可。初始化structEdge{intu,v,w;};vector<Edge>graph;存储voidadd(inta,intb,intc){ graph.push_back({a,b,c});}遍历voiddfs(intver){ if(vis[v......
  • 【笔记】动态规划
    前言动态规划(DynamicProgramming)是c++算法学习当中十分重要而变化灵活的一部分分支,这种算法是通过递推的方式从而达到求出最优解的目的。动态规划基本原理能用动态规划解决的问题,需要满足三个条件:最优子结构,无后效性和子问题重叠。最优子结构:每个子问题的解是其本身的最优......
  • 【笔记/模板】A*算法
    A*算法定义A*搜索算法(\(\text{A*searchalgorithm}\))是一种在图形平面上,对于有多个节点的路径求出最低通过成本的算法。它属于图遍历(英文:\(\text{Graphtraversal}\))和最佳优先搜索算法(英文:\(\text{Best-firstsearch}\)),亦是BFS的优化,用到了启发式搜索的思维。启发式搜索(......
  • 【笔记/模板】Bellman-Ford
    Bellman-Ford求最短路和负环时间复杂度:\(O(nm)\)【模板/笔记】Johnson算法boolBellman_Ford(){memset(dist,0x3f,sizeofdist);for(intk=1;k<n;k++)for(intver=1;ver<=n;ver++)for(inti=h[ver];~i;i=ne[i])......
  • 【模板】缺省源
    Debuginlinevoiddebug(){cerr<<'\n';}template<typenameType,typename...Other>inlinevoiddebug(constType&x,constOther&...y){cerr<<x<<'';debug(y...);}#defineDEBUG(a...)cerr<......
  • 【模板】手写基本数据结构
    栈STL模板STL中的stack容器提供了一众成员函数以供调用,常见的包括但不限于如下:创建一个栈:stack<int>stk;修改元素:stk.push(x);将传入的参数插入到栈顶。stk.pop();将栈顶的元素弹出。查询:stk.top();返回栈顶的元素。stk.empty();返回栈是否为空。stk.size......
  • 【模板】素数筛
    模板原题1.寻找因数筛法时间复杂度:\(O(n\sqrtn)\)核心模板代码如下:boolisprime(intn){ if(n<2)returnfalse; //0和1都不是 for(inti=2;i*i<=n;++i) if(n%i==0)returnfalse; //有1和它本身以外的其他因子就不是素数了 returntrue;}2.埃......
  • C++学习笔记
    一、从C转向C++1.1、使用const和inline而不#define使用constconstintm=10;intn=m;上述代码在C中,编译器会先到m的内存中取出数据,再赋值给n;但在C++中,会直接将10赋值给n。C++的常量更类似于#define,是一个替换过程。#define经常不被认为是语言的一部分。define本......
  • floyed算法模板
    #include<bits/stdc++.h>#include<vector>usingnamespacestd;intlj[1010][1010];//邻接矩阵//可以换成链式前向星之类的巴拉巴拉,这里用邻接矩阵演示比较清楚intn,m;intflyd[1001][1001];intmain(){ cin>>n>>m; for(inti=1;i<=m;i++){ intu,v,w; cin>>u>>......
  • C++模板元编程 实测
    本文记录在各平台(g++、msvc)中实测《C++模板元编程实战:一个深度学习框架的初步实现》中代码的过程。1.3.2节,作者给出了这一段代码:`templatestructWrapper{templatestructFun_{constexprstaticsize_tvalue=0;};template<>structFun_<int>{constexprst......