首页 > 其他分享 >广义后缀自动机

广义后缀自动机

时间:2022-12-20 16:36:24浏览次数:36  
标签:include last SAM 后缀 len trie int 广义 自动机

广义 SAM。这玩意和 SAM 的关系就类似 AC 自动机和 kmp 的关系,也就是可以处理多个串之间的问题。就像 AC 自动机是 kmp 在 trie 上的拓展,广义 SAM 也是 SAM 在 trie 上的拓展。

首先你必须保证你完全理解了 SAM 是个什么东西。要不然以下的东西大概一定会看不懂。

oiwiki 上提到两种常见的假写法:

  1. 把每个串连起来,中间加分隔符,建 SAM ,然后通过某些奇技淫巧手段(比如根据分隔符判断)处理。
  2. 每次插入一个串前,last 指针清空,然后插入。

显然第一种大多数情况下没什么意义。第二种据说可以卡。下面来讲正确的方法。

离线构建

离线构建,就是先把 trie 建出来然后在 trie 的基础上建立广义 SAM。

SAM 的构建可以看作不断插入 \(\text{len}\) 严格递增的值,且差值为 \(1\)。所以我们可以把 trie 进行拓扑排序,按照这个顺序插入 SAM,得到广义 SAM。

区别在于,普通的 SAM 的 last 就是上一个节点,但是广义 SAM 不太一样,是字典树上的父亲。因此我们只需要在普通 SAM 的基础上加一个拓扑排序,再加一个数组保存 trie 上的每个节点对应 SAM 的哪个节点,就可以建立广义 SAM,复杂度显然为线性。

洛谷板子:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
char s[1000010];
int n;
long long ans;
struct Trie{
    int cnt,trie[1000010][26],fa[1000010],col[1000010];
    Trie(){cnt=1;}
    void ins(char s[]){
        int p=1,len=strlen(s+1);
        for(int i=1;i<=len;i++){
            if(!trie[p][s[i]-'a'])trie[p][s[i]-'a']=++cnt,col[cnt]=s[i]-'a',fa[cnt]=p;
            p=trie[p][s[i]-'a'];
        }
    }
}T;
struct Sam{
    int cnt,pos[2000010],fa[2000010],len[2000010],trie[2000010][26];
    //pos是trie上的每个节点对应SAM的哪个节点
    Sam(){cnt=1;}
    int ins(int ch,int last){
        int p=last;last=++cnt;
        len[last]=len[p]+1;
        while(p&&!trie[p][ch])trie[p][ch]=cnt,p=fa[p];
        if(!p){
            fa[last]=1;return last;
        }
        int q=trie[p][ch];
        if(len[p]+1==len[q]){
            fa[last]=q;return last;
        }
        len[++cnt]=len[p]+1;
        for(int j=0;j<26;j++)trie[cnt][j]=trie[q][j];
        fa[cnt]=fa[q];fa[q]=cnt;fa[last]=cnt;
        while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
        return last;
    }
    void build(){
        queue<int>q;
        for(int i=0;i<26;i++)if(T.trie[1][i])q.push(T.trie[1][i]);
        pos[1]=1;
        while(!q.empty()){
            int x=q.front();q.pop();
            pos[x]=ins(T.col[x],pos[T.fa[x]]);
            for(int i=0;i<26;i++)if(T.trie[x][i])q.push(T.trie[x][i]);
        }
    }
}SAM;
signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",s+1);
        T.ins(s);
    }
    SAM.build();
    for(int i=2;i<=SAM.cnt;i++)ans+=SAM.len[i]-SAM.len[SAM.fa[i]];
    printf("%lld\n",ans);
    return 0;
}

注意写 dfs 的话复杂度是不对的,是 trie 上所有节点深度之和,平方级别。

在线构建

在线构建就是和 SAM 一样,每读入一个字符就插入一个字符。当然我们每次插入前需要初始化last指针。

实际上我们只需要在 SAM 的插入前面特判就行。首先我先把代码放上来。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
char s[1000010];
int n;
long long ans;
struct Sam{
    int cnt,fa[2000010],len[2000010],trie[2000010][26];
    Sam(){cnt=1;}
    int ins(int ch,int last){
        if(trie[last][ch]){
            int p=last,q=trie[p][ch];
            if(len[p]+1==len[q])return q;
            else{
                len[++cnt]=len[p]+1;
                for(int i=0;i<26;i++)trie[cnt][i]=trie[q][i];
                while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
                fa[cnt]=fa[q];fa[q]=cnt;
                return cnt;
            }
        }
        int p=last;last=++cnt;
        len[last]=len[p]+1;
        while(p&&!trie[p][ch])trie[p][ch]=cnt,p=fa[p];
        if(!p){
            fa[last]=1;return last;
        }
        int q=trie[p][ch];
        if(len[p]+1==len[q]){
            fa[last]=q;return last;
        }
        len[++cnt]=len[p]+1;
        for(int j=0;j<26;j++)trie[cnt][j]=trie[q][j];
        fa[cnt]=fa[q];fa[q]=cnt;fa[last]=cnt;
        while(trie[p][ch]==q)trie[p][ch]=cnt,p=fa[p];
        return last;
    }
}SAM;
signed main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%s",s+1);int last=1,len=strlen(s+1);
        for(int j=1;j<=len;j++)last=SAM.ins(s[j]-'a',last);
    }
    for(int i=2;i<=SAM.cnt;i++)ans+=SAM.len[i]-SAM.len[SAM.fa[i]];
    printf("%lld\n",ans);
    return 0;
}

特判是用来处理已经存在这个转移的情况,需要分转移是否连续来讨论。

第一个特判:前面已经插入过相同的转移且转移连续,直接不管。

第二个特判:转移不连续,类似 SAM 的处理方式,我们同样需要把一个状态拆开变成两个。那么把原来的状态复制一份,把除了 \(\text{len}\) 以外的信息复制过去,重新标定 \(\text{len}\) 并且重定向转移边,和 SAM 是一样的。

根据论文,它的复杂度也是 trie 上所有节点深度之和级别的。所以有时候遇到题目给你一棵 trie 的题会爆炸。但是平时跑的飞快,比离线快一车。

应用继续咕。

标签:include,last,SAM,后缀,len,trie,int,广义,自动机
From: https://www.cnblogs.com/gtm1514/p/16994524.html

相关文章

  • 后缀自动机,SAM
    后缀自动机,SAM。这玩意可以解决一群字符串问题,但是它本身的原理相当复杂,因此理解这玩意比较困难(10级考点)。以下基本没有证明。定义SAM可以在线性的空间和时间复杂度内......
  • 后缀自动机
    \(\text{SAM}\)大部分是贺OI-Wiki的。目录\(\text{SAM}\)性质概念\(\text{endpos}\)-子串在母串中的结束位置\(\text{link}\)-转移结束位置的后缀链接总结构造流程......
  • KMP 自动机
    相当于\(\pi\)函数(KMP中的next数组)的扩展。\(\delta(i,c)\)表示在KMP匹配过程中当模式串指针在\(i\),接受字符\(c\)后模式串指针所处的位置。则KMP过程中模......
  • 如何申请@MSN.Com后缀的邮箱?
    最近辞职在家无事,想申请个@MSN.Com后缀的信箱,在网上搜索了一下,原来只要从下面的地址进入注册即可!注册抵制:​​https://accountservices.passport.net/reg.srf?ns=msn.......
  • R语言广义线性模型(GLM)、全子集回归模型选择、检验分析全国风向气候数据
    全文链接:http://tecdat.cn/?p=30914原文出处:拓端数据部落公众号我们正和一位朋友讨论如何在R软件中用GLM模型处理全国的气候数据。本文获取了全国的2021年全国的气候数据......
  • 回文自动机,PAM
    回文自动机。或者叫回文树。这坑我放了一百年没填了。结构回文自动机的每个节点都代表一个回文子串。它有两个起始状态:奇根和偶根。它们是奇回文串和偶回文串的起点,不代......
  • Go语言获取路径的文件名、后缀
    packagemainimport( "fmt" "path" "path/filepath")funcmain(){ filePath:="D:/DDPS/log/log.txt" paths,fileName:=filepath.Split(filePath) fm......
  • 【学习笔记】UKK线性求后缀树
    (话说是不是可以直接SAM线性构造啊QAQ)构造过程直观图入门......
  • R语言用Rshiny探索lme4广义线性混合模型(GLMM)和线性混合模型(LMM)|附代码数据
    全文链接:http://tecdat.cn/?p=3138随着软件包的进步,使用广义线性混合模型(GLMM)和线性混合模型(LMM)变得越来越容易最近我们被客户要求撰写关于广义线性混合模型的研究报告,包......
  • 数据分享|R语言用lme4多层次(混合效应)广义线性模型(GLM),逻辑回归分析教育留级调查数据
    最近我们被客户要求撰写关于混合效应广义线性模型的研究报告,包括一些图形和统计输出。本教程为读者提供了使用频率学派的广义线性模型(GLM)的基本介绍。具体来说,本教程重点介......