首页 > 编程语言 >字符串从入门到退竞(2)——KMP 算法

字符串从入门到退竞(2)——KMP 算法

时间:2024-09-26 15:51:47浏览次数:1  
标签:前缀 int 函数 算法 vector KMP 字符串 pi 入门

约定在文字描述中字符串下标从 \(1\) 开始,代码中从 \(0\) 开始。

前缀函数

对于长 \(n\) 的字符串 \(S\),其前缀函数是一个长 \(n\) 的数组(数列)\(\pi\),定义如下:

  • 若 \(S[1..i]\) 有相等的真前缀和真后缀(称之为 border),\(\pi[i]\) 为其长度的最大值;
  • 若不存在,\(\pi[i]=0\)。

字符串的真前缀或真后缀不包括其本身。

例如 \(\mathtt{aabaaab}\) 的前缀函数是 \([0,1,0,1,2,2,3]\)。

计算前缀函数的朴素算法

容易想到对于每个位置进行暴力枚举。

vector<int> prefix_function(const string &s) {
  int n = s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; ++i)
    for (int j = i; j >= 0; --j)
      if (s.substr(0, j) == s.substr(i - j + 1, j)) {
        pi[i] = j;
        break;
      }
  return pi;
}

字符串比较的复杂度是 \(O(n)\),总时间复杂度为 \(O(n^3)\)。

第一个优化

注意到前缀函数后一个值最多比前一个值多 \(1\),我们可以对算法改进。

vector<int> prefix_function(const string &s) {
  int n = s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; ++i)
    for (int j = pi[i - 1] + 1; j >= 0; --j)
      if (s.substr(0, j) == s.substr(i - j + 1, j)) {
        pi[i] = j;
        break;
      }
  return pi;
}

这看似是常数优化,实则不然。

应用势能分析,定义势能函数为 \(\Phi(i)=\pi[i]\times n\),易得每次求新的 \(\pi[i]\) 的均摊复杂度为 \(O(n)\)。

第二个优化

延续第一个优化的思路,我们只需检查字符串的每一个 border 是否可以“扩展”。

字符串 \(S[1..i]\) 最大的 border 长度为 \(\pi[i]\),容易注意到次长的 border 即最长的 border 的 border,其长度为 \(\pi[\pi[i]]\),以此类推。

vector<int> prefix_function(string s) {
  int n = s.length();
  vector<int> pi(n);
  for (int i = 1; i < n; ++i) {
    int j = pi[i - 1];
    while (j > 0 && s[i] != s[j]) j = pi[j - 1];
    if (s[i] == s[j]) ++j;
    pi[i] = j;
  }
  return pi;
}

时间复杂度为 \(O(n)\)。

这是一个在线算法,在读取数据时立即处理。

KMP 算法

给定文本串 \(T\) 和模式串 \(S\),我们尝试找到所有 \(S\) 在 \(T\) 中的所有出现。

我们构造字符串 \(S+\mathtt{\#}+T\),其中 \(\mathtt{\#}\) 是既不出现在 \(S\) 也不出现在 \(T\) 中的分割符。计算该字符串的前缀函数,显然不会出现 \(\pi[i]>|S|\);若 \(\pi[i]=|S|\) 成立,意味着 \(S\) 在这里完整出现了一次。

vector<int> find_occurrences(string text, string pattern) {
  string cur = pattern + '#' + text;
  int sz1 = text.size(), sz2 = pattern.size();
  vector<int> v;
  vector<int> lps = prefix_function(cur);
  for (int i = sz2 + 1; i <= sz1 + sz2; i++) {
    if (lps[i] == sz2) v.push_back(i - 2 * sz2);
  }
  return v;
}

字符串的周期

若字符串 \(S\) 有长 \(r\) 的 border,则 \(|S|-r\) 显然是 \(S\) 的周期。求前缀函数后枚举 border 即可。

统计每个前缀的出现次数

给定长 \(n\) 的字符串 \(S\),考虑位置 \(i\) 的前缀函数值 \(\pi[i]\),这意味字符串 \(S\) 长 \(\pi[i]\) 的前缀在 \(i\) 出现并以 \(i\) 作为右端点,并且不存在更长的前缀。同时,更短的前缀可能在这里作为右端点,因此还需统计 \(\pi[\pi[i]],\pi[\pi[\pi[i]]]\),等等。

vector<int> ans(n + 1);
for (int i = 0; i < n; ++i) ++ans[pi[i]];
for (int i = n - 1; i > 0; --i) ans[pi[i - 1]] += ans[i];
for (int i = 0; i <= n; ++i) ++ans[i];

前缀自动机

在 KMP 算法中,前缀函数不会超过模式串 \(S\) 的长度,因此 \(T\) 部分每个字符的前缀函数只与模式串的前缀函数和前一个字符的前缀函数有关。我们将当前状态定义为前一个字符的前缀函数,可以求出自动机的转移表 \((\pi_\text{old},c)\to \pi_{\text{new}}\):

void compute_automaton(string s, vector<vector<int>>& aut) {
  s += '#';
  int n = s.size();
  vector<int> pi = prefix_function(s);
  aut.assign(n, vector<int>(26));
  for (int i = 0; i < n; ++i) {
    for (int c = 0; c < 26; ++c) {
      if (i > 0 && 'a' + c != s[i])
        aut[i][c] = aut[pi[i - 1]][c];
      else
        aut[i][c] = i + ('a' + c == s[i]);
    }
  }
}

时间复杂度可以做到 \(O(|\Sigma|n)\)。

标签:前缀,int,函数,算法,vector,KMP,字符串,pi,入门
From: https://www.cnblogs.com/weily09/p/18430829/strKMP

相关文章

  • BladeX开发入门(记录)
    BladeX物联网平台是一款高度集成的物联网解决方案,涵盖设备管理、数据采集、实时监控、数据分析以及开放API服务等核心功能。平台经过精心设计与开发,提供了全面的品类、产品和设备支持。设备注册成功后,能够轻松桥接至其他物联网云平台,实现设备的无缝集成。同时提供服务端订阅功......
  • RabbitMq 入门应用 提升性能 : 算法多阶段并行 (Python)
    大问题:我们有一个算法,它可以被分为多个阶段进行(顺序不可颠倒),每个阶段的性能和资源要求不同(且不均衡程度比较高);假设我们现在可以堆资源(较多的CPU和内存),如何将算法各个步骤拆分并进行性能均衡和实现,使得算法性能最大化以满足生产要求?多进程:由于算法有严格的顺序要求,如果是......
  • 老鼠检测、厨房老鼠检测、老鼠识别算法
    老鼠检测算法主要用于家庭、餐饮行业、仓储物流、医疗设施等领域的鼠患监控,通过图像识别技术来检测和识别视频或图像中的老鼠。这种技术可以帮助管理者实时监控老鼠活动,及时采取措施,防止鼠患带来的健康和经济损失。一、技术实现老鼠检测算法通常依赖于计算机视觉和深度学习技术,通......
  • 本科学历能找到人工智能算法岗位的工作吗?好就业吗?
    随着科技的发展,人工智能技术在各行各业的应用日益广泛,催生了大量专注于人工智能的企业,这些企业在招聘网站上发布了众多相关岗位,并且这些岗位的薪资普遍高于其他行业岗位,因此越来越多求职者渴望进入这一行业。对于同样有这一愿景的本科生来说,他们常常会问:“我本科学历能找到人工智能......
  • MyBatis-Plus的使用基础入门案例
    目录文章目录目录简介特性框架结构第一个案例准备工作初始化工程添加依赖完整的pom配置编写实体类编写Mapper修改启动类--扫描Mapper测试运行简介MyBatis-Plus(简称MP)是一个MyBatis的增强工具,在MyBatis的基础上只做增强不做改变,为简化开发、提高效率而生......
  • 【图计算算法‌】基于路径依赖的图计算算法‌
    目录一、基于路径依赖的图计算算法‌概述二、基于路径依赖的图计算算法‌主要特点三、基于路径依赖的图计算算法‌优缺点和改进2.1  基于路径依赖的图计算算法‌优点3.2  基于路径依赖的图计算算法‌缺点3.3  基于路径依赖的图计算算法‌改进四、 基于路径依赖的......
  • go语言学习入门
    packagemain是什么packagemain是Go语言程序的包声明,表示该文件属于主包。主包是Go程序的入口点,包含一个名为main的函数。这个函数是程序启动时首先执行的代码。每个可执行的Go程序都需要有一个main包和main函数。packagemainimport"fmt"funcmain(){fmt.P......
  • Spring Boot入门到精通:网上购物商城系统
    第3章系统分析3.1可行性分析在系统开发之初要进行系统可行分析,这样做的目的就是使用最小成本解决最大问题,一旦程序开发满足用户需要,带来的好处也是很多的。下面我们将从技术上、操作上、经济上等方面来考虑这个系统到底值不值得开发。3.1.1技术可行性本基于SpringBo......
  • docker部署jumpserver及入门
    一、环境及要求环境:CentOSLinux7.9jumpserverv2.28.6要求LinuxKernel:>=4.0 MySQL:>=5.7#官方使用MariaDB10.6对照MySQL8.0Redis:>=5.0#不支持cluster模式官方使用Redis6.2SoftRequirement:wgetcurltargettextiptablespythone二、依赖安装1.MySQL......
  • RabbitMQ(兔子队列入门/消息队列)
    介绍(本笔记不涉及RabbitMQ的环境搭建,主要用于了解和上手使用RabbitMQ)RabbitMQ是一种消息队列,什么是消息队列?消息(Message):是指在应用之间传送的数据,消息可以非常简单,比如只包含文本字符串,也可以更复杂,可能包含嵌入对象。**队列:**可以说是一个数据结构,可以存储数据,如下图,我们从右侧(队......