KMP+Trie+AC自动机总结(字符串)

KMP简介

  • 在实际使用中,我们不可能匹配失败一次就去判断失败字符前面所有字符组成的串的最长相等的前缀和后缀,这样时间复杂度会很高,所以我们需要在匹配之前对模式串进行预处理,对每个字符如果匹配失败,要右移几位进行保存,在匹配中一旦失败,直接跳到那个位置就可以了,由此诞生了KMP算法
  • KMP算法是一种改进的字符串匹配算法,其核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的
  • 我们要解决的询问是主串a中是否包含模式串b (即模式串b是否为主串a的子串) ,具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度: O ( m + n ) O(m+n) O(m+n)

图片:

在这里插入图片描述
一些具体知识可以看 字符串的模式匹配(KMP)算法kmp的next数组值得求法 等文章来了解一下

KMP的next数组

  • next数组表示的是真前缀真后缀最大匹配长度(真即不包括字符串本身)
  • 对于一个字符串而言,其"真前缀"指自身以外的全部头部组合;"真后缀"指自身以外的全部尾部组合
  • next数组首位(对应next[0])一定是-1,第二位(对应next[1])一定是0。原因:i=0时,对于next数组首位,我们统一为next[0]=-1(其本身没有实际意义),i=1时,第一个字符不管是谁,其对应的字符串只有一位,这个只有一位的字符串最长相同真前后缀长度为0,所以next[1] = 0
  • next数组总共有1+len位,不是len位,可以理解为其首位(对应next[0])没什么实际意义

next数组求法:

void getNext(){
    int len=n;
    int i=0,j=-1;   
    nex[0]=-1;//为方便处理设为-1
    while(i<len){
        if(j==-1||a[i]==a[j])nex[++i]=++j;//若相等,都前进一步(第一次要特殊处理)              
        else j=nex[j];//匹配失败时,跳转下标
    }
}

KMP例题

例1:字符串的问题

在这里插入图片描述

思路:

先求出字符串a的next数组,之后通过 n e x [ l e n ] nex[len] nex[len] 找出前缀后缀最大匹配长度,如果为0 || 不为0但在字符串中没有出现过,就输出Just a legend

注意:在while循环中加上:
l e n = n e x [ l e n ] ; len=nex[len]; len=nex[len];
用于处理结果存在但不是前缀后缀最大匹配长度的情况,比如:
x x x y x x z x x x xxxyxxzxxx xxxyxxzxxx
代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e7+5;
char a[N],b[N];
int nex[N],cnt[N];
void getNext(char *p){
    int len=strlen(p);
    int i=0,j=-1;
    nex[i]=j;
    while(i<len){
        if(j==-1 || p[i]==p[j]) nex[++i]=++j;
        else j=nex[j];
    }
}
int main(){
    cin>>a;
    getNext(a);
    int len=strlen(a);
    for(int i=1;i<len;i++) cnt[nex[i]]++;//也可以从2开始循环
    len=nex[len];//求字符串整体的前后缀最大匹配长度
    while(len>0){
        if(cnt[len]){
            for(int i=0;i<len;i++) cout<<a[i];
            return 0;
        }
        len=nex[len];//核心:在len里面去缩小范围
    }
    cout<<"Just a legend";
    return 0;
}

备注: 本代码借鉴自: 7 Q Q Q Q Q Q Q 7QQQQQQQ 7QQQQQQQ

例2: Count the string
在这里插入图片描述

思路:

求前缀后缀公共子串出现了几次也就是求:next数组不为0出现了几次再加上自己的串长

代码:

#include<stdio.h>
#include<iostream>
using namespace std;
const int maxn=1e6+10;
const int mod=10007;
char a[maxn];
int nex[maxn],n,sum;
void getNext(){
    int len=n;
    int i=0,j=-1;   
    nex[0]=-1;
    while(i<len){
        if(j==-1||a[i]==a[j])nex[++i]=++j;              
        else j=nex[j];
    }
}
int main(){
    int t;
    scanf("%d",&t);
    while(t--){
        scanf("%d\n%s",&n,a);
        getNext();
        sum=n;
        for(int i=1;i<=n;i++){
            int k=nex[i];
            while(k){
                sum=(sum+1)%mod;
                k=nex[k];
            }
        }
        printf("%d\n",sum);
    }
    return 0;
}

例3: Oulipo

题意:

一共有n组数据,每组数据中包含两个字符串s和t,找出字符串s在字符串t中出现的次数

思路:

带入模板即可

#include<stdio.h>
#include<string.h>
using namespace std;
const int maxn1= 1e4+5;
const int maxn2 =1e6+5;
char s1[maxn1],s2[maxn2];
int next[maxn1];
void GetNext(char *p){
    int len=strlen(p);
    int i=0,j=-1;
    next[0]=-1;
    while(i<len){
        if(j==-1 || p[i]==p[j])next[++i]=++j;
        else j=next[j];
    }
}
int main(){
    int n;
    scanf("%d",&n);
    while(n--){
        scanf("%s%s",s1,s2);
        int len1=strlen(s1),len2=strlen(s2);
        GetNext(s1);
        if(len1>len2){printf("0\n");continue;}
        int i=0,j=0,cnt=0;//一定注意j不能初始化为-1
        while(i<len2){
            if(j==-1||s1[j]==s2[i])++i,++j;
            else j=next[j];
            if(j==len1)j=next[j],cnt++;//完成一次匹配         
        }
        printf("%d\n",cnt);
    }
    return 0;
}

Trie介绍

字典树又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。

在这里插入图片描述

Trie板子

变量的定义:

int son[N][26];
数组的值是:子节点对应的idx
第一维意义:父节点对应的idx
第二维意义:其直接子节点('a'-'0')的值

int cnt[N];  
用字符串最后一个字符对应的idx作为cnt数组的下标,数组的值是该idx对应的个数

int idx;  
在tire树中,以下标来记录每一个字符的位置,方便之后的插入和查找

char str[N];
待插入或询问的字符串
void insert(char *str){
    int p = 0;//p起初指向根节点
    for (int i = 0; str[i]; i ++ ){
        int u = str[i] - 'a';//字符映射为数字 
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;//在当前节点P上标记它是"一个"字符串的末尾
}
int query(char *str){
    int p = 0;
    for (int i = 0; str[i]; i ++ ){
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];//若被标记了,则存在,否则不存在
}

Trie例题

Trie字符串统计

#include <iostream>
using namespace std;
const int N = 100010;
int son[N][26];
int cnt[N];
int idx;
char str[N];
void insert(char *str){
    int p = 0;//p起初指向根节点
    for (int i = 0; str[i]; i ++ ){
        int u = str[i] - 'a';//字符映射为数字 
        if (!son[p][u]) son[p][u] = ++ idx;
        p = son[p][u];
    }
    cnt[p] ++ ;//在当前节点P上标记它是"一个"字符串的末尾
}
int query(char *str){
    int p = 0;
    for (int i = 0; str[i]; i ++ ){
        int u = str[i] - 'a';
        if (!son[p][u]) return 0;
        p = son[p][u];
    }
    return cnt[p];//若被标记了,则存在,否则不存在
}
int main(){
    int n;
    scanf("%d", &n);
    while (n -- ){
        char op[2];//%s 读进去的,需要多一位,存一个'\0'
        scanf("%s%s", op, str);
        //scanf读入单个字符(%c)的时候不会自动过滤回车和空格,所以%s方便一些
        if (*op == 'I') insert(str);
        else printf("%d\n", query(str));
    }
    return 0;
}

AC自动机简介

  • 一个常见的例子就是给出n个单词,再给出一段包含m个字符的文章,让你找出有多少个单词在文章里出现过。
  • 要搞懂AC自动机,先得有模式树(字典树)Trie和KMP模式匹配算法的基础知识,AC自动机算法分为3步:构造一棵Trie树,构造失败指针和模式匹配过程。
  • 如果你对KMP算法了解的话,应该知道KMP算法中的next函数(shift函数或者fail函数)是干什么用的。KMP中我们用两个指针 i i i j j j 分别表示, A [ i − j + 1.. i ] A[i-j+ 1..i] A[ij+1..i] B [ 1.. j ] B[1..j] B[1..j]完全相等。也就是说, i i i 是不断增加的,随着 i i i 的增加 j j j 相应地变化,且 j j j 满足以 A [ i ] A[i] A[i]结尾的长度为j的字符串正好匹配B串的前 j j j 个字符,当 A [ i + 1 ] ≠ B [ j + 1 ] A[i+1]≠B[j+1] A[i+1]=B[j+1],KMP的策略是调整j的位置(减小 j j j 值)使得 A [ i − j + 1... i ] A[i-j+1...i] A[ij+1...i] B [ 1... j ] B[1...j] B[1...j]保持匹配且新的 B [ j + 1 ] B[j+1] B[j+1]恰好与 A [ i + 1 ] A[i+1] A[i+1]匹配,而next函数恰恰记录了这个 j j j 应该调整到的位置。同样AC自动机的失败指针具有同样的功能,也就是说当我们的模式串在Trie上进行匹配时,如果与当前节点的关键字不能继续匹配,就应该去当前节点的失败指针所指向的节点继续进行匹配。

在这里插入图片描述

AC自动机板子

板子借鉴自: AC自动机总结(超详细注释)(略有修改)

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
#include <queue>
#include <set>
#include <stdlib.h>
using namespace std;
const int maxn = 500010;
struct Trie{
    //next存放的是字典树,next[i][j]中i是父亲节点,j是孩子节点,fail存放失配边的信息,end标记的是每个模式串末尾节点
    int next[maxn][26],fail[maxn],end[maxn];
    int root,L;//root是根节点,L是节点编号   
    int newnode(){//newnode()创建一个新节点,并将其所有孩子节点初始化为-1,返回这个新建节点的编号
        for(int i = 0;i < 26;i++)next[L][i] = -1;
        end[L++] = 0;
        return L-1;
    }   
    void init(){//初始化,给根节点初始化为0号节点
        L = 0;
        root = newnode();
    }
    void insert(char *str){//插入一个字符串
        int len = strlen(str),p = root;//令一个指针p起初指向根节点
        for(int i = 0;i <len;i++){
            int ch = str[i]-'a';
            if(next[p][ch] == -1)next[p][ch] = newnode();//如果现在这个节点下面没有这个节点,就建立新的节点
            p = next[p][ch];//切换到下一个节点
        }
        end[p]++;//标记末尾节点
    }
    //建立失配边,用bfs的方式建立
    void build(){
        queue<int> Q;
        fail[root] = root;
        for(int i = 0;i < 26;i++){
            if(next[root][i] == -1)next[root][i] = root;
            else{
                fail[next[root][i]] = root;//如果没有前缀,就指向root就行了
                Q.push(next[root][i]);
            }
        }
        while(!Q.empty()){
            int p = Q.front();Q.pop();
            for(int i = 0;i < 26;i++){
                if(next[p][i] == -1)next[p][i] = next[fail[p]][i];
                else{
                    fail[next[p][i]] = next[fail[p]][i];//失配边指向相同前缀
                    Q.push(next[p][i]);
                }
            }
        }
    }
    //目标串与字典树进行匹配
    int query(char str[]){
        int len = strlen(str);
        int p = root;
        int res = 0;
        for(int i = 0;i < len;i++){
            p = next[p][str[i]-'a'];
            int temp = p;
            //找目标串中与模式串相同的串
            while(temp != root){
                res += end[temp];
                end[temp] = 0;//这里置零是防止重复计数
                temp = fail[temp];
            }
        }
        return res;//返回匹配成功的个数
    }    
    void dubug(){}//debug函数,不做多解释了
};
char str[1000010];
Trie ac;
int main()
{
    int t,n;
    scanf("%d",&t);
    while(t--){
        scanf("%d",&n);
        ac.init();//初始化
        for(int i = 0;i < n;i++){scanf("%s",str); ac.insert(str);}
        ac.build();
        scanf("%s",str);
        printf("%d\n",ac.query(str));
    }
    return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值