AC自动机详解

news2025/2/24 21:14:32

更好的阅读体验 \color{red}{更好的阅读体验} 更好的阅读体验


文章目录

  • 前置知识
    • 字典树 Trie
      • 支持操作
      • 建字典树
      • 实现思想
      • 代码实现
    • 例题
      • Trie字符串统计
      • 最大异或对
  • AC自动机
    • 基础概念
    • 实现思想
    • 代码实现
    • 例题
      • 搜索关键词
      • 单词


前置知识


字典树 Trie


Trie 是一种能够快速插入和查询字符串的多叉树结构。节点的编号各不相同,根节点编号为0,其他节点用来标识路径还可以标记单词插入的次数。边表示字符。


支持操作


Trie 维护字符串的集合,支持两种操作:

  • 向集合中插入一个字符串:void insert(char *s)
  • 在集合中查询一个字符串:int query(char *s)

建字典树


  • 儿子数组 ch[p][j] 存储从节点 p 沿着 j 这条边走到的子节点。
    • 边为 26 个小写字母 a ~ z 对应的映射值 0 ~ 25
    • 每个节点最多可以有 26 个分叉。
  • 计数数组 cnt[p] 存储以节点 p 结尾的单词的插入次数。
  • 节点编号 idx 用来给节点编号。

实现思想


  • Trie 仅有一个根节点,编号为 0
  • 从根结点开始插,枚举字符串的每个字符:
    • 如果有儿子,则 p 指针走到儿子;
    • 如果没儿子,则先创建儿子,p 指针再走到儿子。
  • 在单词结束点记录插入次数。
  • 例如:依次插入"cat", "car", "busy", "cate", "bus", "car"
  • 在这里插入图片描述

代码实现


const int N = 1e6 + 3;

int n;
string s;
int ch[N][27], cnt[N], idx;

void insert(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';  //字母的映射值
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
    }
    cnt[p] ++;  //插入次数
}

int query(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';
        if(!ch[p][j]) return 0;
        p = ch[p][j];
    }
    return cnt[p];
}

例题


Trie字符串统计


Original Link

维护一个字符串集合,支持两种操作:

  1. I x 向集合中插入一个字符串 x x x
  2. Q x 询问一个字符串在集合中出现了多少次。

共有 N N N 个操作,所有输入的字符串总长度不超过 1 0 5 10^5 105,字符串仅包含小写英文字母。

输入格式

第一行包含整数 N N N,表示操作数。

接下来 N N N 行,每行包含一个操作指令,指令为 I xQ x 中的一种。

输出格式

对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x x x 在集合中出现的次数。

每个结果占一行。

数据范围

1 ≤ N ≤ 2 × 1 0 4 1≤N≤2\times 10^4 1N2×104

输入样例

5
I abc
Q abc
Q ab
I ab
Q ab

输出样例

1
0
1

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 3;

int n;
string s;
int ch[N][27], cnt[N], idx;

void insert(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';  //字母的映射值
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
    }
    cnt[p] ++;  //插入次数
}

int query(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';
        if(!ch[p][j]) return 0;
        p = ch[p][j];
    }
    return cnt[p];
}

void solve(){
    int n; cin >> n;
    while(n --){
        string op; cin >> op >> s;
        if(op == "I") insert(s);
        else cout << query(s) << endl;
    }
}

int main(){
    solve();
    return 0;
}

最大异或对


Original Link

在给定的 N N N 个整数 A 1 , A 2 … … A N A_1,A_2……A_N A1A2……AN 中选出两个进行 x o r xor xor(异或)运算,得到的结果最大是多少?

输入格式

第一行输入一个整数 N N N

第二行输入 N N N 个整数 A 1 , A 2 … … A N A_1,A_2……A_N A1A2……AN

输出格式

输出一个整数表示答案。

数据范围

1 ≤ N < 1 0 5 , 0 ≤ A i ≤ 2 31 1\le N \lt 10^5, 0\le A_i\le 2^{31} 1N<105,0Ai231

输入样例

3
1 2 3

输出样例

3

思想

  • 异或运算即对数的二进制位进行运算,则先将 N N N 个整数均转化为二进制数表示。
  • 二进制是 01 构成的串,构造 Tire 树,在树枝上进行异或运算。
  • Trie 存整数,由整数的二进制位构造的 Trie,是一颗二叉树,深度为 31 层。

本质上

  • Trie 存单词,由 26 个小写字母构造的 Trie,是一颗26 叉树,深度为最长单词的长度。
  • Trie 存整数,由整数的十进制位构造的 Trie,是一颗 10 叉树,深度为 10 层。

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 3;

int n;
int a[N];  //存数
int ch[N * 31][3], cnt[N], idx;

void insert(int x){
    int p = 0;
    for(int i = 30; i >= 0; i --){
        int j = x >> i & 1;  //取出第 i 位
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
    }
}

int query(int x){
    int p = 0, res = 0;
    for(int i = 30; i >= 0; i --){
        int j = x >> i & 1;  //取出第 i 位
        if(ch[p][!j]){
            res += 1 << i;  //累加位权
            p = ch[p][!j];
        }
        else p = ch[p][j];
    }
    return res;
}

void solve(){
    int ans = 0;
    cin >> n;
    for(int i = 0; i < n; i ++) cin >> a[i], insert(a[i]);
    for(int i = 0; i < n; i ++) ans = max(ans, query(a[i]));
    cout << ans << endl;
}

int main(){
    solve();
    return 0;
}

AC自动机


基础概念


自动机是一个对信号序列进行判定的数学模型。

AC 自动机顾名思义就是 自动AC的机器,可以帮助你将难题直接Accept掉

AC 自动机全称为 (Aho-Corasick automaton),该算法在 1975 年产生于贝尔实验室,是著名的多模匹配算法。所谓多模匹配算法,最常见的例子是给出 n 个单词,再给出一段包含 m 个字符的文章,让你找出有多少个单词在文章里出现过。

AC 自动机是 以 Trie 的结构为基础,结合 KMP 的思想 建立的。


实现思想


简单来说,建立一个 AC 自动机有两个必要结构:

  • 基础的 Trie 结构:
    • 先用 n 个模式串构造一颗 Trie
    • Trie 中的一个节点表示一个从根到当前节点的字符串,其中,根节点表示空串。
    • 如果节点是个模式串,则打个标记。
  • KMP 的思想:
    • Trie 上所有的结点构造失配指针。
    • 即在 Trie 上构建两类边:回跳边和转移边。

最后就可以利用它扫描主串进行多模式匹配。

在这里插入图片描述

节点 ⑤ ⑤ 表示 "s",节点 ⑥ ⑥ 表示 "sh",节点 ⑦ ⑦ 表示 "she"

具体地,对于构建两类边:

  • 回跳边:

    • ne[v] 存节点 v 的回跳边的终点。ne[7] = 3
    • 回跳边指向父节点的回跳边所指节点的儿子
    • 四个点(vune[u]ch[][])构成四边形。
    • 回跳边所指节点一定是当前节点的最长后缀
  • 转移边:

    • ch[u][i] 存节点 u 的树边的终点。ch[6][e] = 7

    • ch[u][i] 存节点 u 的树边的终点。ch[6][e] = 7

    • ch[u][i] 存节点 u 的转移边的终点。ch[7][r] = 4

    • 转移边指向当前节点的回跳边所指节点的儿子

    • 三个点(une[u]ch[][])构成三角形。

    • 转移边所指节点一定是当前节点的最短路

在这里插入图片描述

  • 构造 AC 自动机:

    • 初始化,把根节点的儿子们入队。
    • 只要队不空,节点 u 出队,枚举 u26 个儿子:
      • 若儿子存在,则爹帮儿子建回跳边,并把儿子入队。
      • 若儿子不存在,则爹自建转移边。

在这里插入图片描述

如图建立 AC 自动机的回跳边,转移边同理。

  • 查找单词出现次数:
    • 扫描主串,依次取出字符 s[k]
      • 指针走主串对应的节点,沿着树边或转移边走且保证不回退。
      • j 指针沿着回跳边搜索模式串,每次从当前节点走到根节点,把当前节点中的所有后缀模式串遍历完,保证不漏解。
      • 扫描完主串,返回答案。

代码实现


const int N = 5e5 + 3;

int n;
string s;
int ch[N][26], cnt[N], idx;
int ne[N];

void insert(string s){  //键 Tire 树
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
    }
    cnt[p] ++;
}

void build(){  //建 AC 自动机
    queue<int> q;
    for(int i = 0; i < 26; i ++){
        if(ch[0][i]) q.push(ch[0][i]);  //根节点儿子入队
    }
    while(q.size()){
        int u = q.front(); q.pop();
        for(int i = 0; i < 26; i ++){
            int v = ch[u][i];
            if(v) ne[v] = ch[ne[u]][i], q.push(v);  //儿子存在,爹帮儿子建回跳边,儿子入队
            else ch[u][i] = ch[ne[u]][i];  //儿子不存在,爹自建转移边
        }
    }
}

int query(string s){  //扫描主串查询
    int ans = 0;
    for(int k = 0, i = 0; k < s.size(); k ++){
        i = ch[i][s[k] - 'a']; 
        for(int j = i; j && ~ cnt[j]; j = ne[j]){
            ans += cnt[j], cnt[j] = -1;  //找到后退出,加速查询
        }
    }
    return ans;
}

例题


搜索关键词


Original Link

给定 n n n 个长度不超过 50 50 50 的由小写英文字母组成的单词,以及一篇长为 m m m 的文章。

请问,其中有多少个单词在文章中出现了。

注意:每个单词不论在文章中出现多少次,仅累计 1 次。

输入格式

第一行包含整数 T T T,表示共有 T T T 组测试数据。

对于每组数据,第一行一个整数 n n n,接下去 n n n 行表示 n n n 个单词,最后一行输入一个字符串,表示文章。

输出格式

对于每组数据,输出一个占一行的整数,表示有多少个单词在文章中出现。

数据范围

1 ≤ n ≤ 1 0 4 , 1 ≤ m ≤ 1 0 6 1\le n \le 10^4, 1\le m \le 10^6 1n104,1m106

输入样例

1
5
she
he
say
shr
her
yasherhs

输出样例

3

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 5e5 + 3;

int n;
string s;
int ch[N][26], cnt[N], idx;
int ne[N];

void insert(string s){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
    }
    cnt[p] ++;
}

void build(){  //建 AC 自动机
    queue<int> q;
    for(int i = 0; i < 26; i ++){
        if(ch[0][i]) q.push(ch[0][i]);
    }
    while(q.size()){
        int u = q.front(); q.pop();
        for(int i = 0; i < 26; i ++){
            int v = ch[u][i];
            if(v) ne[v] = ch[ne[u]][i], q.push(v);
            else ch[u][i] = ch[ne[u]][i];
        }
    }
}

int query(string s){
    int ans = 0;
    for(int k = 0, i = 0; k < s.size(); k ++){
        i = ch[i][s[k] - 'a'];
        for(int j = i; j && ~ cnt[j]; j = ne[j]){
            ans += cnt[j], cnt[j] = -1;
        }
    }
    return ans;
}

void solve(){

    idx = 0;
    memset(ch, 0, sizeof ch);
    memset(cnt, 0, sizeof cnt);
    memset(ne, 0, sizeof ne);

    cin >> n;
    for(int i = 0; i < n; i ++){
        cin >> s; insert(s);
    }
    build();
    cin >> s;
    cout << query(s) << endl;
}

int main(){
    int _; cin >> _;
    while(_ --) solve();
    return 0;
}

单词


Original Link

某人读论文,一篇论文是由许多单词组成的。

但他发现一个单词会在论文中出现很多次,现在他想知道每个单词分别在论文中出现多少次。

输入格式

第一行一个整数 N N N,表示有多少个单词。

接下来 N N N 行每行一个单词,单词中只包含小写字母。

输出格式

输出 N N N 个整数,每个整数占一行,第 i i i 行的数字表示第 i i i 个单词在文章中出现了多少次。

数据范围

1 ≤ N ≤ 200 1≤N≤200 1N200
所有单词长度的总和不超过 1 0 6 10^6 106

输入样例

3
a
aa
aaa

输出样例

6
3
1

思想

  • 求每个单词在全文中出现的次数,即该单词在其他单词中出现次数的总和。
  • 故该单词在其他单词中的前缀的后缀即为该单词出现次数的总和。
  • 在建 AC 自动机时利用 BFS 从第 0 层搜索到 n 层,需要保留堆的信息进行递推计算,且递推计算出现的次数时必须逆序。

代码

#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 3;

int n;
string s;
int ch[N][26], cnt[N], idx;
int ne[N], num[N];

int id[N];  //记录


void insert(int x){
    int p = 0;
    for(int i = 0; i < s.size(); i ++){
        int j = s[i] - 'a';
        if(!ch[p][j]) ch[p][j] = ++ idx;
        p = ch[p][j];
        cnt[p] ++;  //统计前缀的次数,每一个结束的位置都代表一个字符串
    }
    id[x] = p;  //记录截止位置
}

void build(){  //建 AC 自动机
    //由于后续递推求前缀出现的次数,故需要保留堆的信息
    int hh = 0, tt = -1;
    for(int i = 0; i < 26; i ++){
        if(ch[0][i]) num[++ tt] = ch[0][i];
    }
    while(hh <= tt){
        int u = num[hh ++];
        for(int i = 0; i < 26; i ++){
            int v = ch[u][i];
            if(v) ne[v] = ch[ne[u]][i], num[++ tt] = v;
            else ch[u][i] = ch[ne[u]][i];
        }
    }
}

void solve(){
    cin >> n;
    for(int i = 0; i < n; i ++){
        cin >> s; insert(i);  //i 为当前单词对应编号
    }
    build();
    //递推更新 cnt, trie 中节点编号为 0 ~ idx,一共 idx + 1 个点,0 既代表根节点又代表空节点
    for(int i = idx; i >= 0; i --) cnt[ne[num[i]]] += cnt[num[i]];  
    for(int i = 0; i < n; i ++) cout << cnt[id[i]] << endl;
}

int main(){
    solve();
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/197799.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

成功解决yum安装的php版本过低的问题

文章目录前言一. 问题复现二. 问题分析三. 问题解决&#xff1a;四. 重要补充1. yum-config-manager介绍2. yum-uitls介绍3. remi资源库总结前言 大家好&#xff0c;我是沐风晓月&#xff0c;日常学习过程经常会遇到一些奇奇怪怪的问题&#xff0c;而解决问题就成了常态&#…

鸿蒙开发学习|HarmonyOS工程介绍

系列文章目录 第一章 HarmonyOS是什么 第二章 基础环境和开发工具 文章目录系列文章目录前言一、HarmonyOS工程介绍二、工程目录结构三、工程目录介绍1.entry2.Ability3.库文件4.资源文件5.配置文件6.pack.info7.HAR总结前言 本文将会给大家梳理 HarmonyOS 源码目录结构&…

关于《利用LexYacc进行词法分析和语法分析并生成语法树》

利用Lex&Yacc进行词法分析和语法分析 写在前面 利用Lex进行词法分析的流程在前面已经讲过&#xff0c;接下来是利用Lex&Yacc进行语法分析&#xff0c;最后可视化生成语法树。具体的操作视频&#xff1a;https://www.bilibili.com/video/BV1wY411q7aH/ 语法分析流程 …

【MySQL】MySQL 8.0 新特性之 - 窗口函数(Window Functions)

窗口函数 - Window Functions1. 定义1.1 窗口函数1.2 语法格式2. 分类2.1 序号函数2.1.1 row_number()2.1.2 rank()2.1.3 dense_rank()2.2.4 示例2.2 分布函数2.2.1 percent_rank()2.2.2 cume_dist()2.3 前后函数2.3.1 lag(expr, n, default)2.3.2 lead(expr, n, default)2.3.3…

致跟我一样苦恼的你们

2023年2月1日&#xff0c;我决定结束实习&#xff0c;回去准备春招和毕设。我把这个决定跟家人和朋友说时&#xff0c;他们似乎是有点不太赞同&#xff0c;他们觉得&#xff1a; “现在工作不好找&#xff0c;你可以先找好下一家公司后再选择离职” “毕设得事情&#xff0c;…

VBA提高篇_17 区域合纵连横,单元格精准定位

文章目录Application.Union方法:Application.Intersect方法:Range.CurrentRegion属性:Range.Resize(3,2)Range.Offset 单元格偏移属性Application.Union方法: 把多个Range联合在一起,作为一个新的Range对象返回 Sub RangeUnionDemo()Dim a&, r1 As Range, r2 As Range, r3 …

魔兽世界服务端AzerothCore+Centos系统+docker编译教程

魔兽世界服务端AzerothCoreCentos系统docker编译教程1.1 准备工作1.1.1 准备1.1.2 安装软件1.1.3 下载源码1.1.4 地图文件1.2 修改配置文件1.2.1 修改环境变量文件1.2.2 修改文件执行权限1.2.3 修改配置文件1.3 编译及启动1.3.1 编译项目1.3.2 启动容器1.3.3 无法启动1. 网络问…

【Java多线程】线程的安全问题

根据上篇文章买票问题举例&#xff0c;还可能出现的问题&#xff1a; 代码如下&#xff1a; class Window1 implements Runnable{private static int ticket 100;Overridepublic void run() {while (true){if (ticket > 0){try {Thread.sleep(100);} catch (InterruptedExc…

网站优化与seo的方法(seo的优化基础)

SEO优化的常规思路&#xff0c;别全以转化为目标 SEO优化作为现在公司推广营销的基础&#xff0c;几乎每个公司都在做这件事。这种优化既可以提升品牌知名度&#xff0c;又能直接给公司带来流量&#xff0c;确实让不少公司感觉很有用。但是在持续的过程中&#xff0c;又会觉得…

ESP32 Arduino 学习篇(五)TFT_eSPI库

前期准备&#xff1a;1.TFT_eSPI库的安装首先在Libraries里面搜索安装TFT_eSPI库到你的工程文件里面。2.TFT_eSPI库的配置文件配置该库有User_Setup.h和 User_Setup_Select.h两个配置文件&#xff0c;支持 ①自定义参数或 ②使用已有配置 驱动TFT屏幕。User_Setup.h — 由自己定…

PCB阻焊层介绍与设计经验总结

&#x1f3e1;《总目录》 目录1&#xff0c;什么是阻焊层2&#xff0c;阻焊层的用途,3&#xff0c;阻焊层的工艺流程4&#xff0c;阻焊设计的注意事项1&#xff0c;什么是阻焊层 阻焊层是顶层或底层布线层表面的顶层保护层&#xff0c;就是PCB表层的绿油层&#xff0c;在阻焊层…

【坤坤讲师--图】Dinic

Dinic是个很神奇的网络流算法。它是一个基于“层次图”的时间效率优先的最大流算法。层次图是什么东西呢?层次,其实就是从源点走到那个点的最短路径长度。于是乎,我们得到一个定理:从源点开始,在层次图中沿着边不管怎么走,经过的路径一定是终点在剩余图中的最短路。(摘自…

疫情时代的宠儿:抗生素行业,今后何去何从

本文由前嗅数据研究院出品 自2020年COVID-19流行开始&#xff0c;已经过去了3年&#xff0c;医药行业发生巨大的变化&#xff0c;各种大中小企业实现了一系列调整。疫情将近结束的时候&#xff0c;让我们回顾分析一下近年来医药领域抗生素行业相关发展。 本研究将从行业现状、…

SpringBoot+Vue核酸预约系统

简介&#xff1a;本项目采用了基本的springbootvue设计的核酸预约系统。详情请看截图。经测试&#xff0c;本项目正常运行。本项目适用于Java毕业设计、课程设计学习参考等用途。 项目描述 项目名称SpringBootVue核酸预约系统源码作者LHL项目类型Java EE项目 &#xff08;前后…

计算机网络基础知识总结

计算机网络基础知识总结 如果说计算机把我们从工业时代带到了信息时代&#xff0c;那么计算机网络就可以说把我们带到了网络时代。随着使用计算机人数的不断增加&#xff0c;计算机也经历了一系列的发展&#xff0c;从大型通用计算机 -> 超级计算机 -> 小型机 -> 个人…

Windows驱动环境配置

windows驱动开发视频教程(2023最新版)_哔哩哔哩_bilibili 以前的 WDK 版本和其他下载 - Windows drivers | Microsoft Learn 确认本机操作系统版本 安装操作系统版本对应的Visual Studio 我的机器是1904可以安装vs2019&#xff0c;但是实际上我装的是vs2017也是没有问题的 安…

泼辣修图2023最新网页版MAC电脑手机修图软件功能介绍

泼辣修图5.11.4最新版为用户带来更多新版的修改工具&#xff0c;进一步优化相关的设备&#xff0c;可以更舒畅的使用去修改图片&#xff0c;还有很多贴纸&#xff0c;文字等等小工具使用&#xff0c;丰富照片情景。 适用于Windows的泼辣修图摄影爱好者都在用泼辣处理照片 泼辣…

并发编程学习案例-ReentrantReadWriteLock非公平的情况下读锁插队和写锁插队场景复现

文章目录一、前言二、源码三、 代码案例&#xff08;一&#xff09;复现写的时候插队场景参考执行结果&#xff08;二&#xff09;复现读的时候插队参考执行结果参考资料一、前言 Java ReentrantReadWriteLock 是 ReadWriteLock 的实现类&#xff0c;可以分出2把锁&#xff0c;…

OpenCV 图像形态学处理

本文是OpenCV图像视觉入门之路的第11篇文章&#xff0c;本文详细的在图像形态学进行了图像处理&#xff0c;例如&#xff1a;腐蚀操作、膨胀操作、开闭运算、梯度运算、Top Hat Black Hat运算等操作。 OpenCV 图像形态学处理目录 1 腐蚀操作 2 膨胀操作 3 开闭运算 4 梯度运…

57.Isaac教程--定位监视器

定位监视器 ISAAC教程合集地址: https://blog.csdn.net/kunhe0512/category_12163211.html 检测异常系统状态并采取纠正措施有助于确保稳定的系统性能和与预期行为的最小偏差。 为此&#xff0c;Isaac SDK 提供了一个监控框架&#xff0c;可以搭载多种系统观察组件。 该框架目…