• 后缀自动机总结


    一些定义

    endpos("") 表示一个 S 的子串在 S 中出现的结束位置集合。每个子串都有一个 endpos 集合,可能有一些子串的 endpos 集合相同,就合并,构成后缀自动机节点。也即一个节点代表一个 endpos 集合的等价类,里边包含一些串,容易知道这些串是连续的,且互为包含关系。一个点的 endpos 集合大小即为当前点所有串在 S 中出现的次数。

    每个点有一些转移边,表示在当前节点所能表示的所有串后面接一个字符,转移到一些新的等价类。容易知道这样的转移关系构成一个拓扑图。

    每个点有一个 parent,也即后缀链接,表示把当前串前面删去一些字符,第一次使得 endpos 集合发生改变的转移边。容易知道这样的转移关系构成一棵树。

    每个点有一个 len,表示该点表示的串中最大的长度。那么该点表示的所有串的长度介于 ((par.len,len])

    一条根到一个节点的路径唯一构成了 S 中一个子串,不同路径所构成的串一定不同。根据这点可以在拓扑图中 dp 出,以某串为前缀的串的个数有多少种,可以查询字典序第 k 大的串是哪个。

    板子

    void ins(int c){
        int tmp=la,X=la=++tot;
        endpos[X]=1,t[X].len=t[tmp].len+1;
        for(;tmp&&!t[tmp].w[c];tmp=t[tmp].par) t[tmp].w[c]=X;
        if(!tmp) t[X].par=1;
        else{
            int to=t[tmp].w[c];
            if(t[to].len==t[tmp].len+1) t[X].par=to;
            else{
                int Nto=++tot;
                t[Nto]=t[to],t[Nto].len=t[tmp].len+1;
                t[to].par=t[X].par=Nto;
                for(;tmp&&t[tmp].w[c]==to;tmp=t[tmp].par) t[tmp].w[c]=Nto;
            }
        }
    }
    

    不同子串个数

    给你一个长为N的字符串,求本质不同的子串的个数。

    SOL: 拓扑图上 dp,(S_{u}=sum S_v+1)

    【模板】后缀自动机

    求出 (S) 的所有出现次数不为 (1) 的子串的出现次数乘上该子串长度的最大值。

    SOL: 出现次数为 endpos 集合大小,可以先在树上子树求和。长度即为该点的 len。

    [SDOI2016]生成魔咒

    共进行 (n) 次操作,每次操作是在 (S) 的结尾加入一个数。每次操作后都需要求出,当前的串 (S) 共有多少种本质不同的串。

    SOL: 字符集是 1e9,考虑用 map。根据增量构造,S 的后缀自动机会有两种加点方式,第一种是新建的 X,另一种是 Nto。而容易知道 Nto 是没有贡献的,因为它只是将原有节点拆开了。贡献只来自 X,为 (len-par.len)

    LCS - Longest Common Substring

    求两个串 S 和 T 的最长公共子串。

    SOL: 对 S 建后缀自动机,用 T 在自动机上跑匹配。如果有转移边,就转移,并使匹配长度加一,否则跳后缀链接(类似 AC 自动机的性质,后缀链接一定指向的是一个已经匹配了的后缀,而且是长度最长且 endpos 不同的串),直到有转移边为止,然后领匹配长度为当前节点的最长串长度。最后对过程中所有匹配的长度取最大值。

    int now=1,l=0,ans=0;
    for(int i=0;i<n;i++){
        int c=s[i]-'a';
        if(!t[now].w[c]){
            while(now!=1&&!t[now].w[c]) now=t[now].par;
            l=t[now].len;
        }
        if(t[now].w[c]) now=t[now].w[c],l++;
        ans=max(ans,l);
    }
    

    LCS2 - Longest Common Substring II

    求多个串的最长公共子串。

    SOL: 对除第一个串的每个串重复建后缀自动机。然后用第一个串在上面跑匹配,定义 (p_i) 表示第一个串以第 (i) 位结尾的后缀能匹配到的最大长度。匹配的时候对所有 (p_i) 取最小值来更新,最后答案是所有 (p_i) 的最大值。

    清空:

    void Clear(){
        for(int i=1;i<=tot;i++){
            t[i].par=t[i].len=0;
            for(int j=0;j<26;j++) t[i].w[j]=0;
        }
        tot=la=1;
    }
    

    [TJOI2015]弦论

    求字典序第 (k) 小的子串,分不同位置算一个和不同位置算多个。

    SOL: 先统计出从当前节点能都到多少字符串。然后从小到大枚举转移边,如果 (k) 大于能走到的串个数 (num),就减去 (num),然后枚举下一个转移边。直到 (k) 不够,然后输出当前转移,继续递归。同时在进入函数时减去 endpos 集合大小,表示判断答案是不是就是当前串,或者后面还要接字符。如果只算本质不同的串就把 endpos 设成 1。

    void print(int u,long long k){
        if(k<=right[u]) return;
        k-=right[u];
        for(int i=0;i<26;i++)
            if(nd[u].c[i]){
            int v=nd[u].c[i];
            if(k>sum[v]){
                k-=sum[v];
                continue;
            }
            printf("%c",i+'a');
            print(v,k);
            return;
        } 
    } 
    

    [BJOI2020] 封印

    给出只包含小写字母 (a,b) 的两个字符串 (s,t,q) 次询问,每次询问 (s[l dots r])(t) 的最长公共子串长度。

    SOL: 求出上述的 (p_i) 后(对于这一类问题似乎都可以考虑这样做?),实际上就是要算 (maxlimits_{lleq i leq r}{min{p_i,i-l+1}})。然后有一个很神奇的二分做法。二分能不能取到 (mid),显然必然只考虑 (igeq l+mid-1) 的,因为如果 (i) 小了,那么一定取不到。也就是在 ([l+mid-1,r]) 查询有没有大于 (mid)(p_i)。可以用 ST 表。那么就 (O(nlog n)) 了。

    Cyclical Quest

    给定一个主串 (S)(n) 个询问串,求每个询问串的所有循环同构在主串中出现的次数总和。

    SOL: 如果不考虑循环同构,那么直接在 (S) 的后缀自动机上匹配。循环同构实际上是先在前面删去一个字符,然后在当前串后接一个字符。删字符其实就对应了跳后缀链接。要注意如果上一个同构都没有匹配那么根本不需要删。加字符直接走转移边即可。然后注意相同的节点是不能重复贡献的,考虑走的时候打上 tag。

    [AHOI2013]差异

    给定一个长度为 (n) 的字符串 (S),令 (Ti)​ 表示它从第 (i) 个字符开始的后缀。求

    [sum_{1leq i < j leq n} |T_i|+|T_j|-2|lcp(T_i,T_j)| ]

    SOL: 前面两项很简单,关键是求公共前缀长度。我们知道 SAM 可以求出两个串最长后缀的长度,考虑把串反过来,原串的前缀就是反串的后缀,所以考虑构建反串的 SAM。利用后缀链接性质可知,两个串的公共后缀长度就是两个对应节点的 LCA 的长度。那么只需要统计有多少对 ((i,j)) 的 LCA 是当前节点,树形 dp 即可。

    [APIO2014]回文串

    给定串 S。一个串得存在值为其在 S 中的出现次数乘上其长度。求所有回文串的存在值的最大值。

    SOL: 不考虑回文串的话就是板子。首先需要知道那些是回文串,想到跑 manacher。那么就有一个暴力的做法,每次扩展到一个新的回文串就在 SAM 上查询,由于本质不同的回文串共有 (O(n)) 个,那么复杂度就是 (O(n^2))。这个算法慢在我们每次需要暴力重新匹配。再次观察,发现是自身和自身的匹配,想到优化。记串 (S(l,r)) 为回文串,S 的前缀串 (T_r) 对应的节点为 (p)。那么 (S(l,r)) 对应的节点一定在 (p) 的根缀上。我们要求的是长度最小的包含该回文串的节点,而 SAM 上一条根缀上的节点的 len 是有单调性的,所以想到倍增。复杂度 (O(n log n))

    CF427D Match & Catch

    给定两个字符串 S 和 T,求最短的满足各只出现一次的连续公共字串

    SOL: 对 S 建 SAM,用 T 在 SAM 上跑匹配,求出 (p_i) 表示以 (i) 结尾的串的最长匹配长度。那么对应节点的根缀都是能匹配的串,考虑打上差分标记。最后 dfs 子树求和,记为 s,那么必须 s 和 (|endpos|) 都为一才能算进答案,长度更新为当前节点能表示的最短的长度。

    [HAOI2016]找相同字符

    给定两个字符串,求出在两个字符串中各取出一个子串使得这两个子串相同的方案数。两个方案不同当且仅当这两个子串中有一个位置不同。

    SOL: 和上一题差不多,也是树上求和,但是有一个特殊情况要考虑。假设当前匹配到节点 (p),然而可能并不完全匹配,只是匹配了该节点所表示的部分节点。所以算答案的时候要单独提出来算。

    Match:
    ans+=1ll*(l-t[t[p].par].len)*R[p];
    S[t[p].par]++;
    
    dfs: if(u!=1) ans+=1ll*(t[u].len-t[t[u].par].len)*S[u]*R[u];
    

    CF1037H Security

    给定串 (S),每次给出串 (T),询问串子串 (S(l,r)) 中字典序最小的且严格大于串 (T) 的子串。输出串,没有就输出 -1。

    SOL: 先考虑对全局串询问。有一个贪心的做法,每次在后面补最小的能补的字符一定是最优的。那么最先匹配到的一定是一个前缀,如果可以完全匹配那么再在后面补一个字符就是最小的。但是可能并不能完全匹配,假设当前匹配到第 (p) 个字符,但是没有 (S[p+1]) 的转移边,那么选择一个大于 (S[p+1]) 的转移就得到了答案。如果不存在大于等于 (S[p+1]) 的转移,那么只能回溯。复杂度 (O(26 imessum|T|))

    现在考虑加入区间限制,也就是说在 SAM 上有些转移边不能走了,因为对应节点的 endpos 里不存在元素属于这个区间。那其实我们只需要知道 endpos 与该区间的交是不是空的就行了。考虑线段树合并,维护每个点的 endpos 集合,直接查询即可。注意合并时,儿子节点不能销毁,而要新建一个节点。因为是在树上维护有哪些节点,所以复杂度有保证。还要注意,endpos 只是结束位置,我们只保证了结束为止存在于区间内,而没有保证左端点也在区间内。解决这个问题,只需要查询最大的属于该区间的结束点,然后判断其左端点是不是在区间内。修改一下 query 即可。

    // 'l' start from 0
    bool Dfs(int l,int u){
        int c=s[l]-'a';
        if(l==m){
            for(int i=0;i<26;i++)
                if(t[u].w[i]&&query(rt[t[u].w[i]])-l>=L)
                    return sta[++top]=i,1;
        }else if(t[u].w[c]&&query(rt[t[u].w[c]])-l>=L&&Dfs(l+1,t[u].w[c]))
            return sta[++top]=c,1;
        else{
            for(int i=c+1;i<26;i++)
                if(t[u].w[i]&&query(rt[t[u].w[i]])-l>=L)
                    return sta[++top]=i,1;
        }
        return 0;
    }
    

    [HEOI2016/TJOI2016]字符串

    (Q) 次询问。每次询问 (S(l,r)) 的所有子串和串 (S(l',r')) 的最长公共前缀。

    SOL: 显然答案有单调性,可以二分答案,然后转换为 (S(l',mid)) 在不在 (S(l,r)) 出现过。可以先从 (S) 的前缀串 (T_mid) 对应的节点向上跳 parent,跳到 (S(l',r')) 对应节点,然后判断该点的 endpos 集合是否存在元素出现在区间 ([l+|S(l',mid)|-1,r]) 以判定串是否严格属于该区间。然后 endpos 集合可以沿用上题做法用线段树合并求出。

    [NOI2018] 你的名字

    给定串 (S)(Q) 次询问,每次给出串 (T) 和整数 (l,r),问串 (T) 的所有本质不同的子串中有多少个串不是 (S(l,r)) 的子串。

    SOL: 容易想到对 (S) 建 SAM,并用线段树合并预处理出每个点的 endpos 集合。然后用 (T) 在 SAM 上匹配,求出后缀匹配长度。注意线段树判断一下转移边能不能走。如果失配了,不能直接跳 parent 边,因为我们只是提取了一个区间的 SAM,并不满足一个节点所表示的串的 endpos 都一样,所以每次只能将匹配长度减一。这样的复杂度显然是正确的,因为匹配至多使长度加一,所以均摊下来能接受。最后对 (T) 建 SAM,本质不同的串的个数就是每个节点的 (len-par.len) 求和。再减去每个节点对应后缀串的最大匹配长度就是不能匹配的长度,即答案。

  • 相关阅读:
    当你不知道今天星期几,不妨在编辑器写下这段代码
    自定义注解!绝对是程序员装逼的利器!!
    什么是可串行化MVCC
    Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
    连接真机开发安卓(Android)移动app MUI框架 添加购物车等——混合式开发(四)
    从前世今生聊一聊,大厂为啥亲睐时序数据库
    工作五年,面试官说我只会CRUD!竟然只给我10K!
    bootstrap知识总结
    数据处理的两个基本问题05 零基础入门学习汇编语言42
    转移指令的原理02 零基础入门学习汇编语言44
  • 原文地址:https://www.cnblogs.com/wwlwQWQ/p/14782294.html
Copyright © 2020-2023  润新知