首页 > 编程语言 >【算法】KMP 与 Z 函数

【算法】KMP 与 Z 函数

时间:2024-11-19 11:56:31浏览次数:1  
标签:匹配 函数 int KMP 算法 kmp 字符串 reg

1. KMP

1.1 算法简介

可以做到线性匹配的快速匹配字符串的算法,并可以维护字符串最长公共前后缀,扩展出计算字符串周期。

在 OI 界 KMP 算法是字符串板块中很经典的算法,可以扩展出很多巧妙的解题技巧。

1.2 算法流程

1.2.1 字符串匹配

考虑 \(O(n^2)\) 暴力的匹配,瓶颈在于每次匹配了很多重复却非法的字符导致效率很慢。

然后就是考虑如何优化,无非就是利用已经计算过的信息。

这里补充一个 最长公共前后缀 的概念:对于字符串 \(a\),最长公共前后缀的长度为满足 \(a[1,i]=a[n-i+1,n]\) 的最大的 \(i(i<n)\)。可以发现如果该定义包含本身则没有意义(长度一定为 \(n\))。所以需要保证最长公共前后缀小于本身的长度。

一个匹配下分 \(s\)(模式串)和 \(t\) (匹配串),串长分别为 \(n\) 和 \(m\)。记 \(kmp_i (1\le i\le m)\) 表示前缀 \(t[1,i]\) 的最长公共前后缀。

假设已经计算出了 \(kmp_i\)。考虑如何匹配字符串。

s: caabaaabacb
t: aaba

此时 \(kmp:[0,1,0,1]\)。

  • \(i=1,j=1\);此时两串匹配的为空串,\(i\to i+1\)
  • \(i=2,j=1\);此时两串可匹配成功,匹配了 aaba,\(i\to i+1,j=kmp_{j}=kmp_{4}=1\)
  • \(i=3,j=2\);此时两串匹配了 a,\(i\to i+1,j=kmp_{j}=kmp_{1}=0\)
  • \(i=4,j=1\);此时两串匹配的为空串,\(i\to i+1\)
  • \(i=5,j=1\);此时两串匹配了 aa,\(i\to i+1,j=kmp_{j}=kmp_{2}=1\)
  • \(i=6,j=2\);此时两串可匹配成功,匹配了 aaba,\(i\to i+1,j=kmp_{j}=kmp_{4}=1\)

如此匹配,我们便找到了所有的合法匹配位置,分别为 \(2,6\)。

考虑分析时间复杂度,可以看成 \((i,j)\) 对齐,按位匹配。所以整个流程 \(t\) 串一直在往前移动,时间复杂度 \(O(n+m)\)。

1.2.2 计算 kmp 数组

和匹配很像,相当于自己和自己匹配。所到之处的 \(j\) 记录为 \(kmp_i\) 即可。

不过更好的理解是用两个指针 \((i,j)\)。如果 \(s_i\) 与 \(s_j\) 匹配,则将 \(j\) 指针后移,否则跳 \(kmp_j\)(可以保证 \(kmp_j\) 已经算出)。然后 \(kmp_i=j\)。

这个过程一定也是 \(O(n)\) 的。分析与证明参考匹配。

1.3 算法实现

计算 \(kmp\) 数组:

for (int i = 2, j = 0; i <= n; i++) {
  while(j && t[j + 1] != t[i]) j = nx[j];
  if(t[j + 1] == t[i]) j++;
  nx[i] = j;
}

匹配:

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

然后是 P3375 【模板】KMP,就把她俩整合一下即可。

#include<bits/stdc++.h>
#define int long long
#define For(i,l,r) for(int i=l;i<=r;++i)
#define FOR(i,r,l) for(int i=r;i>=l;--i)

using namespace std;

const int N = 1e6 + 10;

int n, m, nx[N];

char s[N], t[N];

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> (s + 1) >> (t + 1);
  n = strlen(s + 1);
  m = strlen(t + 1);
  for (int i = 2, j = 0; i <= n; i++) {
    while(j && t[j + 1] != t[i]) j = nx[j];
    if(t[j + 1] == t[i]) j++;
    nx[i] = j;
  }
  for (int i = 1, j = 0; i <= n; i++) {
    while(j && t[j + 1] != s[i]) j = nx[j];
    if(t[j + 1] == s[i]) j++;
    if(j == m) {
      cout << i - m + 1 << '\n';
    }
  }
  For(i,1,m) cout << nx[i] << ' ';
  return 0;
}

1.4 扩展

1.4.1 字符串周期

UVA1328 Period

给定一个字符串 \(s\),判断其前缀 \(s[1,i]\) 是否为周期字符串,并求出其周期长度和循环节数量。

可以发现一些性质:如果对于 \(s[1,i]\),\(i \bmod (i-kmp_i) = 0\),则为周期字符串。

证明很简单。

image

按照这样的方式对于每一个小块染上不同的颜色。

image

这里红色段和黄色段相等。

image

这样每一个小段可以传递相等。这样就可以证明出其为周期字符串。

代码挂着
#include<bits/stdc++.h>
#define int long long
#define reg register 
#define For(i,l,r) for(reg int i=l;i<=r;++i)
#define FOR(i,r,l) for(reg int i=r;i>=l;--i)

using namespace std;

const int N = 1e6 + 10;

int n, kmp[N], id;

char s[N];

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  while(cin >> n && n != 0) {
    For(i,1,n) kmp[i] = 0;
    cin >> (s + 1);
    for (int i = 2, j = 0; i <= n; ++i) {
      while(j && s[i] != s[j + 1]) j = kmp[j];
      if(s[i] == s[j + 1]) j++;
      kmp[i] = j;
    }
    ++id;
    cout << "Test case #" << id << '\n';
    For(i,1,n) {
      if(i % (i - kmp[i]) == 0 && kmp[i] != 0) {
        cout << i << ' ' << (i / (i - kmp[i])) << '\n';
      }
    } 
    cout << '\n';
  }
  return 0;
}

2. Z 函数

2.1 算法流程

有一个这样的问题:

给定两个字符串 \(a,b\),你要求出两个数组:

  • \(b\) 的 \(z\) 函数数组 \(z\),即 \(b\) 与 \(b\) 的每一个后缀的 LCP 长度。
  • \(b\) 与 \(a\) 的每一个后缀的 LCP 长度数组 \(p\)。

对于第一个 subtask 所求的数组 \(z\),则为 Z 函数,可以用 扩展 KMP 算法(exKMP) 求得。

2.1.1 暴力求解

很好想,就拿一对指针 \((i,j)\) 去匹配,匹配记录,失配重置。

时间复杂度 \(O(n^2)\)

2.1.2 Z-box 引入

维护一个区间 \([l,r]\),\(l=i,r=i+z_i-1\),其中 \(r\) 为已知最大的合法右端点。

对于 \(i\le r\),可以分为 \(z_{i-l+1}<r-l+1\) 和 \(z_{i-l+1}\ge r-l+1\) 两种情况。

  • \(z_{i-l+1}<r-l+1\);此时 \(z_i=z_{i-l+1}\)
  • \(z_{i-l+1}\ge r-l+1\);此时 \(z_i=r-i+1\),然后暴力匹配。

其他情况暴力匹配,然后更新 \([l,r]\) 即可。

时间复杂度 \(O(n)\),这样可以做到线性了。

可以发现 \(exKMP\) 和 \(Manacher\) 的算法思想很像。

2.2 算法实现。

计算 \(Z\) 函数:

z[1] = m;
for (reg int i = 2, l, r = 0; i <= m; ++i) {
  if(i <= r) z[i] = min(z[i - l + 1], r - i + 1);
  while(i + z[i] <= m && t[1 + z[i]] == t[i + z[i]]) z[i]++;
  if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
}

计算 \(s,t\) 后缀的公共最长前缀。

把匹配 \(s\) 的数组换成 \(p\),求法和 \(Z\) 函数的求法一样。

for (reg int i = 1, l, r = 0; i <= n; ++i) {
  if(i <= r) p[i] = min(z[i - l + 1], r - i + 1);
  while(1 + p[i] <= m && i + p[i] <= n && s[i + p[i]] == t[1 + p[i]]) p[i]++;
  if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
}

然后是 【模板】扩展 KMP/exKMP(Z 函数)

#include<bits/stdc++.h>
#define int long long
#define reg register
#define For(i,l,r) for(reg int i=l;i<=r;++i)
#define FOR(i,r,l) for(reg int i=r;i>=l;--i)

using namespace std;

const int N = 2e7 + 10;

int n, m, z[N], p[N], ans1, ans2;

char s[N], t[N];

signed main() {
  ios::sync_with_stdio(0);
  cin.tie(0), cout.tie(0);
  cin >> (s + 1) >> (t + 1);
  n = strlen(s + 1), m = strlen(t + 1);
  z[1] = m;
  for (reg int i = 2, l, r = 0; i <= m; ++i) {
    if(i <= r) z[i] = min(z[i - l + 1], r - i + 1);
    while(i + z[i] <= m && t[1 + z[i]] == t[i + z[i]]) z[i]++;
    if(i + z[i] - 1 > r) l = i, r = i + z[i] - 1;
  }
  for (reg int i = 1, l, r = 0; i <= n; ++i) {
    if(i <= r) p[i] = min(z[i - l + 1], r - i + 1);
    while(1 + p[i] <= m && i + p[i] <= n && s[i + p[i]] == t[1 + p[i]]) p[i]++;
    if(i + p[i] - 1 > r) l = i, r = i + p[i] - 1;
  }
  For(i,1,m) ans1 = (ans1 ^ (i * (z[i] + 1)));
  For(i,1,n) ans2 = (ans2 ^ (i * (p[i] + 1)));
  cout << ans1 << '\n' << ans2 << '\n';
  return 0;
}

标签:匹配,函数,int,KMP,算法,kmp,字符串,reg
From: https://www.cnblogs.com/Daniel-yao/p/18554369

相关文章

  • Oracle数据库安全扫描1158/3938端口出现弱SSL加密算法解决方法之一
    问题复述某国企项目现场反应安全扫描出部署某历史项目的Windows服务器上的1158及3938两个端口出现了弱SSL加密算法漏洞,要求整改。经过核实,该Windows服务器上部署了tomcat与Oracle11g数据库,其中1158和3938两个端口均为Oracle数据库所使用。处理思路确认1158和3938作用:如果没......
  • new 函数(一)name 参数
    在UVM(UniversalVerificationMethodology)中,new函数是构造对象的关键函数之一,它用于创建和初始化UVM对象或组件。UVM中的new函数不仅用于创建对象,还通常涉及到对象的初始化和设置属性(如名字)。new函数的使用方式有一定的规范,特别是在UVM的对象和组件体系中。1.new......
  • new 函数 (二) parent 参数
    在UVM(UniversalVerificationMethodology)中,new函数通常用于创建对象或组件,并进行初始化。对于大多数uvm_object和uvm_component类,new函数会使用stringname参数来指定对象的名称。parent参数主要与uvm_component类有关,用于指定组件的父级组件。这个参数对于UVM......
  • 记录个Java/Groovy的小问题:空字符串调用split函数返回非空数组
    问题复现最近写了一个groovy替换程序增量流水线脚本(会Java也能看懂),示意脚本如下://获取文件列表方法deflistFiles(folder){defoutput=sh(script:"ls${folder}",returnStdout:true).trim()returnoutput.split('\n')asList}//调用以上方法获取lib目录下......
  • 单变量微积分学习笔记:反函数求导法则(12)【6,9,11】
    常用公式\(\arcsin(x)=\frac{1}{\sqrt{1-x^2}}\)\(\arccos(x)=-\frac{1}{\sqrt{1-x^2}}\)\(\arctan(x)=\frac{1}{1+x^2}\)证明\(y=\arcsin(x)\)\(\sin(y)=x\)\(\cos(y)y'=1\)\(y'=\frac{1}{\cos(y)}\)\(y'=\......
  • R语言caretEnsemble包的caretList函数一次性构建多个机器学习模型、caret包的resample
    R语言caretEnsemble包的caretList函数一次性构建多个机器学习模型、caret包的resamples函数比较在同一数据集上多个机器学习模型的比较结果目录R语言使用caretEnsemble包的caretList函数一次性构建多个机器学习模型、并使用caret包的resamples函数比较在同一数据集上多个机......
  • R语言riskRegression包的FGR函数构建生存资料的竞争风险回归模型、pec包的cindex函数
    R语言riskRegression包的FGR函数构建生存资料的竞争风险回归模型、pec包的cindex函数计算化多时间竞争风险生存资料的C-index目录R语言使用riskRegression包的FGR函数构建生存资料的竞争风险回归模型、使用pec包的cindex函数计算化多时间竞争风险生存资料的C-index#什么......
  • 基于 Levenberg - Marquardt 法的 BP 网络学习改进算法详解
    基于Levenberg-Marquardt法的BP网络学习改进算法详解一、引言BP(BackPropagation)神经网络在众多领域有着广泛应用,但传统BP算法存在收敛速度慢、易陷入局部最优等问题。Levenberg-Marquardt(LM)算法作为一种有效的优化算法,被应用于改进BP网络学习,能够显著提高训......
  • 基于共轭梯度法的 BP 网络学习改进算法详解
    基于共轭梯度法的BP网络学习改进算法详解一、引言BP(BackPropagation)神经网络是一种强大的机器学习工具,广泛应用于模式识别、函数逼近、数据分类等领域。然而,传统的BP算法在训练过程中存在一些问题,例如收敛速度慢、容易陷入局部最优解等。共轭梯度法作为一种高效的优......
  • 从0开始机器学习--11.关联规则挖掘基础(概念-频繁项集、关联规则、支持度置信度提升度,
    写在前面“关联规则挖掘”是数据挖掘的一个重要方向。在本专栏之前的所有文章中,我们已经了解了机器学习和神经网络的基本模型、数据分析方面的应用。那这篇文章所介绍的就是在数据分析方面的另一种“关联规则”的挖掘。本博文是我个人根据ppt的学习记录稍加整理和理解,若有疑问......