后缀自动机学习笔记1(hiho127周)

本文深入探讨了后缀自动机(SAM)的基本概念及其在字符串处理中的应用。介绍了SAM的状态集、后缀链接和转移函数等核心组件,并通过实例展示了如何构建SAM以及如何利用SAM解决实际问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

后缀自动机(Suffix Automaton,简称SAM)。

SAM的States

字串结束集合(endpos):对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6}(这里说的是位置,不要和下面那个图所说的状态1、2、3混淆,这里说的“ab”位置是“aabbabd”,这两个位置为3,6),因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。

将所有子串的endpos都求出来。如果两个子串的endpos相等,就把这两个子串归为一类。最终这些endpos的等价类就构成的SAM的状态集合。例如对于S="aabbabd":

状态                                                                   

字符串                                                                                                      

endpos                                                                      

S

空串

{0,1,2,3,4,5,6}

1

a

{1,2,5}

2

aa

{2}

3

aab

{3}

4

aabb,abb,bb

{4}

5

b

{3,4,6}

6

aabba,abba,bba,ba

{5}

7

aabbab,abbab,bbab,bab

{6}

8

ab

{3,6}

9

aabbabd,abbabd,bbabd,babd,abd,bd,d

{7}


substrings(st)表示状态st中包含的所有子串的集合,
longest(st)表示st包含的最长的子串,
shortest(st)表示st包含的最短的子串。

有几点性质:

  1. 对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。
  2. 对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。
  3. 对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)

后缀自动机

对于一个字符串S,它对应的后缀自动机是一个最小的确定有限状态自动机(DFA),接受且只接受S的后缀。

对于字符串S="aabbabd",它的后缀自动机是

S是开始状态,9是结束状态。通过这个图可以看出状态S到状态9的蓝线所有经过的路线的字符都是aabbabd的一个后缀,例如:S-1-8-9经过的路线是abd正好是aabbabd的一个后缀数组。再比如例如字符串aabb的所有后缀数组即为从状态S到状态4经过的所有路线的字符组成的字符串,例如S-1-8-4经过的路线为abb。

这里有一个问题就是b是aabb的一个后缀数组,但是从状态S到状态4经过的路线中没有b的字符串 ,这是因为b在状态3(字符串aab)中也是它的后缀数组。所以引入了一个中间状态5,这样你会发现对于整个字符串aabbabd来说它的自没有串并没有减少,而且不会出现重复(这里从状态S出发到任意一个状态的蓝线经过的字符串都是aabbabd的一个字串)。

SAM的Suffix Links

Suffix Links实际就是上图的绿线,通过刚才的介绍发现,当前状态引入了中间状态,绿线就指向中间状态,例如上一段说的状态4的路线就指向状态5,否则指向状态S。我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。这个绿线在我们接下来的使用中有很大作用。

SAM的Transition Function

next(st):st遇到的下一个字符集,有next(st) = {S[i+1] | i ∈ endpos(st)}。例如next(S)={S[1], S[2], S[3], S[4], S[5], S[6], S[7]}={a, b, d},next(8)={S[4], S[7]}={b, d}。这里可以看成就是从状态st节点发出的线的符号。

对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) ,具体怎么转移在下一个笔记里。

性质:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6

实例

(这里用的爆搜,我的代码写的不好,但是也实现了,后面会有进步的):

问题:

输入
第一行包含一个字符串S,S长度不超过50。
第二行包含一个整数N,表示询问的数目。(1 <= N <= 10)
以下N行每行包括一个S的子串s,s不为空串。
输出
对于每一个询问s,求出包含s的状态st,输出一行依次包含shortest(st)、longest(st)和endpos(st)。其中endpos(st)由小到大输出,之间用一个空格分割。

样例输入

aabbabd  
5  
b  
abbab  
aa  
aabbab  
bb 

样例输出

b b 3 4 6  
bab aabbab 6  
aa aa 2  
bab aabbab 6  
bb aabb 4
代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace hiho127
{
    class Program
    {
        static bool equipment(List<int> l1, List<int> l2)
        {
            for (int i = 0; i < l1.Count; i++)
            {
                if (l1[i]!=l2[i])
                {
                    return false;
                }
            }
            return true;
        }
        static void Main(string[] args)
        {
            Dictionary<string, List<int>> myDic = new Dictionary<string, List<int>>();
            string s = Console.ReadLine();
            for (int i = 0; i < s.Length; i++)
            {
                for (int j = i; j >= 0; j--)
                {
                    string temp = s.Substring(j, i - j + 1);
                    if (myDic.ContainsKey(temp))
                    {
                        myDic[temp].Add(i + 1);
                    }
                    else
                    {
                        List<int> lint = new List<int>();
                        lint.Add(i + 1);
                        myDic.Add(temp, lint);
                    }
                }
            }
            int n = int.Parse(Console.ReadLine());
            for (int i = 0; i < n; i++)
            {
                string temp = Console.ReadLine();
                List<int> ltem = myDic[temp];
                for (int j = 0; j < myDic.Count; j++)
                {
                    List<int> lint = myDic.ElementAt(j).Value;
                    if (lint.Count == ltem.Count && equipment(lint,ltem))
                    {
                        Console.Write(myDic.ElementAt(j).Key);
                        break;
                    }
                }
                for (int j = myDic.Count - 1; j >= 0; j--)
                {
                    List<int> lint = myDic.ElementAt(j).Value;
                    if (lint.Count == ltem.Count && equipment(lint, ltem))
                    {
                        Console.Write(" " + myDic.ElementAt(j).Key);
                        break;
                    }
                }
                foreach (int item in ltem)
                {
                    Console.Write(" " + item.ToString());
                }


                Console.WriteLine();
            }
        }
    }
}
注意:
一开始一直是过90%点,注意试试这组数据

dbddba
3
db
dba
b

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值