首页 > 其他分享 >KMP 和 AC 自动机

KMP 和 AC 自动机

时间:2022-10-10 14:44:20浏览次数:75  
标签:nxt AC 匹配 int trie maxn KMP 自动机 sim

引入:字符串匹配

给定字符串 \(S\) 和 \(T\),查询 \(T\) 在 \(S\) 中所有出现的位置。(其中 \(S\) 称为文本串,\(T\) 称为模式串)显然暴力匹配的最坏时间复杂度是 \(O(|S||T|)\) 的。然而在题目中我们需要一种最坏情况 \(O(|S|+|T|)\) 左右的算法。

KMP 模式匹配(Knuth-Morris-Pratt)

字符串相关的某个思路即是某个字符串不可行时考虑其某个后缀是否可以。

考虑 \(S_1\sim S_i\) 与 \(T\) 最多匹配到了 \(P_i\) 位,也就是 \(P_i=\max\limits_{d=1}^i [S_{i-d+1}\sim S_i=T_1\sim T_d]\,d\)。显然在考虑 \(S_1\sim S_{i+1}\) 与 \(T\) 匹配时,有 \(P_{i+1}\le P_i+1\)。而若 \(S_{i+1}\ne T_{P_i+1}\) 时,考虑有哪些长度 \(l\) 满足 \(S_{i-l+1}\sim S_i=T_1\sim T_l\) 且 \(T_{l+1}=S_{i+1}\),此时最大的 \(l\) 即为所求的 \(P_{i+1}-1\)。也就是说我们需要找到所有的满足 \(T_1\sim T_l=S_{i-l+1}\sim S_i=T_{P_i-l+1}\sim T_{P_i}\) 的 \(l\),也就是满足 \(T_1\sim T_{P_i}\) 的长为 \(l\) 的 真前缀(非 \(T\) 本身 的前缀)就是长为 \(l\) 的 真后缀(非 \(T\) 本身 的后缀)的 \(l\)(其中每个 \(i\) 对应的最长的 \(l\) 组成的数组称为 \(T\) 的 前缀函数,此处用 \(nxt\) 表示。特别地,\(nxt_0=nxt_1=0\))。根据定义,最大的 \(l\) 是 \(nxt_{P_i}\),考虑之后的 \(l\) 应该如何选择。

引理:仅次于 \(nxt_{P_i}\) 的 \(l\) 一定是 \(nxt_{nxt_{P_i}}\) 而不会是 \((nxt_{nxt_{P_i}},nxt_{P_i})\) 之间的任意一个值。

证明:若 \(\exists\,l\in(nxt_{nxt_{P_i}},nxt_{P_i})\),则 \(T_1\sim T_l=T_{P_i-l+1}\sim T_{P_i}=T_{nxt_{P_i}-l+1}\sim T_{nxt_{P_i}}\),而 \(nxt_{nxt_{P_i}}=\max\limits_{d=1}^{nxt_{P_i}}[T_{nxt_{P_i}-d+1}\sim T_{nxt_{P_i}}=T_1\sim T_d]\,d\),此时 \(l\) 大于 \(nxt_{nxt_{P_i}}\) 且满足 \(nxt_{nxt_{P_i}}\) 满足的条件,与 \(nxt\) 的定义矛盾。故而仅次于 \(nxt_{P_i}\) 的 \(l\) 一定是 \(nxt_{nxt_{P_i}}\)。

此时可以令 \(p=P_i\),若 \(S_{i+1}\ne T_{p+1}\),则 \(p\leftarrow nxt_p\),不断进行此操作直到 \(p=0\) 或 \(S_{i+1}=T_{p+1}\)。若 \(p=0\) 时仍然 \(S_{i+1}\ne T_{p+1}\),则 \(P_{i+1}\) 为 \(0\),否则 \(P_{i+1}\) 为 \(p+1\)。由于 \(\forall i,0\le nxt_i<i\) 且 \(P_{i+1}\le P_i+1\),故最后计算 \(P\) 的时间复杂度为 \(O(|S|)\) 的。

考虑 \(nxt\) 的求法。我们发现 \(nxt\) 就是 \(T\) 和自己进行上述过程(此时 \(P\) 数组即为 \(nxt\) 数组),但是强制从第 \(2\) 位进行匹配。同样进行上述操作即可。故而总的时间复杂度是 \(O(|S|+|T|)\) 的。

模板代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=1000010;
char s1[maxn],s2[maxn];
int i,j,nxt[maxn];
int main(){
	scanf("%s%s",s1+1,s2+1);
	const int siz1=strlen(s1+1);
	const int siz2=strlen(s2+1);
	for(i=2;i<=siz2;++i){
		while(j&&s2[i]!=s2[j+1]) j=nxt[j];
		if(s2[i]==s2[j+1]) ++j;nxt[i]=j;
	}
	j=0;
	for(i=1;i<=siz1;++i){
		while(j&&(s1[i]!=s2[j+1]||j==siz2)) j=nxt[j];
		if(s1[i]==s2[j+1]) ++j;
		if(j==siz2){printf("%d\n",i-siz2+1);j=nxt[j];}
	}
	for(i=1;i<=siz2;++i) printf("%d ",nxt[i]);
	return 0;
}

循环字符串

相关定理 1

对长为 \(n\) 的字符串 \(s\) 跑一遍 KMP 之后,若 \(n\bmod (n-nxt_n)=0\),则 \(s\) 的最小循环节长度为 \(n-nxt_n\)。

证明 1

若 \(s\) 的最小循环节长度为 \(a\),则必有其长度为 \(n-a\) 的前缀和后缀完全相同,也就是 \(nxt_n\) 至少为 \(n-a\)。

如果此时 \(nxt_n\) 小于 \(n-a\),(令 \(b=n-nxt_n\))令 \(t\) 为 \(s\) 的长为 \(a\) 的前缀(只有此处下标从 \(0\) 开始),有 \(t_bt_{b+1}\cdots t_{a-1}t_0t_1\cdots t_{b-1}=t_0t_1\cdots t_{a-1}\),也就是 \(\forall i\in[0,a-1],t_i=t_{(i+b)\bmod a}\);由裴蜀定理可得 \(\exists p>0\) 对 \(\forall i\in[1,n]\),都有 \(t_i=t_{(i+pb)\bmod a}=t_{(i+\gcd(a,b))\bmod a}\);故 \(t\) 和 \(s\) 的最小循环节长度均为 \(\gcd(a,b)\),与之前矛盾。

证毕。

相关定理 2

长为 \(n\) 的任意字符串 \(s\) 的所有循环节长度一定都是最小循环节长度的整数倍。

证明 2

思路与 证明 1 相似。

令 \(s\) 的最小循环节长度为 \(a\);若 \(s\) 存在长度为 \(b\) 的循环节,且 \(b\bmod a\ne 0\),则 \(n\ge \rm{lcm}(a,b)\);由裴蜀定理得 \(\exists j\ge 0,\forall i\in[0,a-1],s_i=s_{i+aj}=s_{(i+aj)\bmod b}=s_{i+\gcd(a,b)}\),且 \(j<\frac{\rm{lcm}(a,b)}a\)。所以 \(s\) 由长度为 \(\gcd(a,b)\) 的循环节,而 \(\gcd(a,b)<a\);与之前矛盾。

证毕。

AC 自动机(Alfred-Corasick Automaton)

原理

AC 自动机结合了 Trie 树能整理多个字符串的特性和 KMP 算法中的前缀函数的性质,常用于对多个字符串的信息的整合(例如多模式串匹配)。

构造和理解

AC 自动机上除了需要对于多个串建出普通的 Trie 之外,每个节点还需要一个 nxt 指针,表示这个节点对应的字符串在 Trie 存在的最长后缀的对应节点。

nxt 指针的求法、原理和原理证明与 KMP 算法中的 nxt 相似。在计算某个节点 \(u\) 的 nxt 时,令其父节点为 \(p\),父节点连向这个点的转移边为字符 \(c\)(令 \(trie_{p,c}=u\)),若 \(trie_{nxt_p,c}\) 存在,则令 \(nxt_u\leftarrow trie_{nxt_p,c}\);否则 \(p\leftarrow nxt_p\),再看 \(trie_{nxt_p,c}\) 是否存在;直到 \(p\) 为根节点且 \(trie_{p,c}\) 不存在,再将 \(nxt_u\) 赋值为根节点。

显然 \(\forall i\),\(nxt_i\) 对应的字符串长度小于 \(i\),故而可以在 Trie 树上 BFS,可以保证在求每个点的 nxt 时其父节点的 nxt、这个 nxt 对应的 nxt …… 这样即可顺利求这个点的 nxt。不难发现 nxt 组成了一棵内向树。(p.s. 这种树被称为 失配树。)

考虑另外一种情况:在模式串为 she、he、her,对文本串 sher 进行匹配。在匹配完 she 之后需要标记 she、he(某个模式串得以完整匹配时,其所有后缀也能完整匹配),同时需要从 Trie 树上的 she 串的节点和其 nxt 上同时走,才能匹配出模式串 her。如果真的在目前经过的节点的所有 nxt 一起根据字符跳转的话,这样会是很低效的。

如果需要更方便地进行跳转,Trie 树边是不够的,我们需要在求出的 nxt 的基础上多连一些转移边。此时需要的就是 \(\forall u,c\),若不存在 \(trie_{u,c}\),则增加从 \(u\) 到 \(trie_{nxt_u,c}\) 的 \(c\) 转移边。这样等效于在从 \(u\) 走到 \(trie_{u,c}\) 时,若这条转移边非树边,则相当于同时在 \(nxt_u\)、\(nxt_{nxt_u}\)(因为 \(nxt_u\) 也进行了这样的过程)等节点均进行了转移,在后面补上 \(c\)。

最后进行多模式匹配的过程就是将文本串在 AC 自动机上匹配的过程。在文本串进行匹配时,标记每个点被经过的次数。理论上某个点被经过时我们需要标记其 nxt 树上的所有祖先(但是这样会使得时间复杂度升为 \(O(n^2)\),考虑模式串刚好为 \(aa\cdots aa\) 的情况),但是可以在跑完文本串后再进行标记。时间复杂度显然为 \(O(n|\Sigma|)\) 的,其中 \(|\Sigma|\) 为字符集大小。

AC 自动机模板代码

#include <bits/stdc++.h>
using namespace std;
const int maxn=2000010;
const int maxl=2000010;
int i,j,n,k,t,*p,hd,tl,siz,tot,ans,all;
int trie[maxn][26],q[maxn],ed[maxn],nxt[maxn];
int To[maxn],In[maxn],cnt[maxn];
char s[maxn],d[maxl];
int main(){
	scanf("%d",&n);
	for(i=1;i<=n;++i){
		scanf("%s",s+1);
		siz=strlen(s+1);
		p=&k;
		for(j=1;j<=siz;++j){
			p=&trie[*p][s[j]-'a'];
			if(!(*p)) *p=++tot;
		}
		To[i]=*p;
	}
	for(i=0;i<26;++i) if(trie[0][i]) q[++tl]=trie[0][i];
	while(hd!=tl){
		i=q[++hd];
		for(j=0;j<26;++j){
			p=&trie[i][j];
			if(!(*p)) *p=trie[nxt[i]][j];
			else{nxt[*p]=trie[nxt[i]][j];q[++tl]=*p;}
		}
	}
	hd=tl=j=0;
	for(i=1;i<=tot;++i) ++In[nxt[i]];
	for(i=1;i<=tot;++i) if(!In[i]) q[++tl]=i;
	scanf("%s",d+1);
	siz=strlen(d+1);
	for(i=1;i<=siz;++i){
		j=trie[j][d[i]-'a'];
		++cnt[j];
	}
	while(hd!=tl){
		i=q[++hd];j=nxt[i];
		cnt[j]+=cnt[i];
		if(!(--In[j])) q[++tl]=j;
	}
	for(i=1;i<=n;++i) printf("%d\n",cnt[To[i]]);
	return 0;
}

标签:nxt,AC,匹配,int,trie,maxn,KMP,自动机,sim
From: https://www.cnblogs.com/FrancaisDrakeCwoi/p/16775644.html

相关文章

  • 成品直播源码,react+ts实现分页全部功能
    成品直播源码,react+ts实现分页全部功能index.tsx代码 importReact,{Component}from'react';importHeaderfrom'../../component/header'importFooterfrom'.......
  • reactor.core.CorePublisher
    java.lang.NoClassDefFoundError:reactor.core.CorePublisher升级<dependency><groupId>io.projectreactor</groupId><artifactId>reactor-core</artifactId><ver......
  • 曾经最强性能的人脸检测算法(Wider Face Dataset)
    计算机视觉研究院专栏作者:Edison_G今天分享的内容,在其他各平台估计都有陆续分享,今天我们“计算机视觉研究院”从我们自己的角度来分析下YOLOF框架,看看他值不值得被CVPR2021......
  • Mac效率软件:Multitouch for Mac(TouchOven触控板手势增强软件)
    Mac哪款触控板手势增强软件好用呢?Multitouchmac是Mac平台上一款mac触控板手势增强软件,Multitouchmac版添加了各种不同的触控板手势,手指轻点、轻扫等就能快速进行操作,使用......
  • React生命周期深度完全解读
    在React中,对于每一次由状态改变导致页面视图的改变,都会经历两个阶段:render阶段、commit阶段。只有class组件才有生命周期,因为class组件会创建对应的实例,而函数组......
  • React核心技术浅析
    1.JSX与虚拟DOM我们从React官方文档开头最基本的一段HelloWorld代码入手:ReactDOM.render(<h1>Hello,world!</h1>,document.getElementById('root'));这段......
  • 英伟达 | 推出适用于AI和高性能计算的NVIDIA GRACE CPU
    计算机视觉研究院专栏作者:Edison_GNVIDIA发布其首款基于Arm架构的数据中心CPU处理器,在最复杂的AI和高性能计算工作负载下,可实现10倍于当今最快服务器的超高性能。4月12日晚,......
  • 1、Centos7下安装Oracle11gR2及多实例
    实验环境:系统:2核8G内存60G硬盘,centos7.4;优化操作:已经关闭了防火墙、selinux,/etc/hosts文件中以添加"172.16.1.92slave-node2"的主机解析记录;设置u......
  • 洛谷 P5194 [USACO05DEC]Scales S 折半搜索
    题目https://www.luogu.com.cn/problem/P5194思路\(n\leq1000\)的范围很吓人,但是按照【每个砝码的质量至少等于前面两个砝码的质量的和】的规则,打表可知n在50时总重量......
  • 28、python3.7(windows)将ORACLE11gR2中的数据取出写入excel表
    28.1、下载python的离线扩展模块:1、windows下python的离线扩展模块下载地址为:​​https://www.lfd.uci.edu/~gohlke/pythonlibs/​​提示:可以通过python官方的pypi仓库下载l......