• 后缀自动机


    后缀自动机是什么?

    作为一个初学者,你跟我说如何构建自动机有什么用!?

    我们来举一个例子:aabbabd。(借用一下网上的图,侵删)

    观察这幅图,我们可以得到看到后缀自动机的性质:

    • 仅看实线边,这是一幅有向无环图。
    • 每个点有且仅有一个蓝色边,连向的点不妨叫做“父亲结点(parent)”。特别的,我们让根结点(图中标记为S)的父亲结点为NULL(空)。
    • 对于一个点来说,连出去的边没有相同的两个字母。
    • 对于原串aabbabd任意的连续子串(下面说的子串都是指连续子串),从源点开始走,一定不会无路可走。如果一个串不是原串的子串,那么走了一段路之后一定走不动了。
    • 每一个点有一个到它的最长距离(length),图中用红色数字表示,我们不妨把这条路径表示字符串叫做这个点的代表的最长字符串
    • ……

    最关键的:对于一个点u来说,它所代表的字符串(不止一个)是从源点走到它的所有路径,不妨设这些字符串组成集合S(u),一定满足:

    • 定义(len(s))为字符串s的长度。
    • 对于任意(s, t in S(u)),如果(len(s) < len(t)),那么s一定是t的后缀。
    • 设该点的length为right,该点父亲的length为left,那么对于任意整数(x in (left, right]),一定有(s in S(u))使得(len(s) = x)。并且S(u)的大小为(right-left)

    再设T(u)表示u点代表的所有后缀,设u的父亲为v,那么定义(T(u) = T(v) + S(u)),特别地,(T(root)={""})。几乎和上面相同,我们有:

    • 对于任意(s, t in T(u)),如果(len(s) < len(t)),那么s一定是t的后缀。
    • (x in [0, length]),一定有(s in T(u))使得(len(s) = x)。并且T(u)的大小为(length + 1)。这里的length就是u的length。

    需要满足的条件好像很复杂,那么如何构建?

    不严谨地说,我们可以利用递归的思想:假设对于字符串s的自动机已经构造好,如何构造s+c(c是一个字符)的自动机。只要做到这个,其他都不是问题。

    我们找到当前自动机length最长的那个点,记为last,注意该点只有一个,且T(last)恰好就是s的所有后缀组成的集合。

    首先新建一个点u,我们只要让T(u)恰好是s+c的所有后缀组成的集合就可以继续保持上面一系列的性质。

    定义(T(u) + c = { a + c | a in T(u) }。)

    最基本的想法就是沿last的父亲一直找下去,然后向点u连一条字符为c的边,正确性是因为(T(u) = T(last) + c)

    current = last
    while current != NULL
      current->child[c] = u
      current = current->parent
    u->parent = root
    

    然而,事情没有那么简单。问题出在第3行,因为对于一个点来说,连出去的边没有相同的两个字母。所以如果当前current已经有一个标号为c的边了,直接这样连接,就会破坏一些性质。

    如果发现current->child[c]已经有了,那么我们分两种情况处理:

    情况一

    current->child[c]->length等于current->length + 1

    current->child[c]为点v。
    这个简单。我们不是要通过往我们的T'(u)加入一些字符串,使得T'(u)最后成为T(u)吗?此时我们的T'(u)缺少的恰恰就是T(v)!就是说(T'(u)+ ( T(current) +c) = T(u))(如果集合A+集合B,表示的结果是(Acup B),且说明(A cap B = emptyset)),而此时(T(current) + c = T(v))

    这样的话,我们把u->parent设为current->child[c]就可以啦。

    current = last
    while current != NULL
      v = current->child[c]
      if v != NULL
        if v->length = current->length + 1
          u->parent = v
          return 
        else
          ...
      else 
        current->child[c] = u
      current = current->parent
    u->parent = root
    

    情况二

    那当然就是:current->child[c]->length等于current->length + 1

    此时我们所需要的(T(current) + c = T(v) - P),也就是说(T(v))多了一点我们不需要的字符串,不妨设它们组成集合P。如果简单地将u->parent设为current->child[c],将违背前面所说的T的性质。

    我们要做的就是把T(v)分成两半,即(T(v) -P)和P。因为(S(current)subseteq S(v)),更进一步说,就是将S(v)分成两半,即(S(v) -P)(不妨设为Q)和P。

    (par(u, x))表示u->parent->parent->...->parent,x个parent。

    容易发现:
    (Q = (S(par(current, 0)) +c)+(S(par(current, 0)) +1) +... +(S(par(current, k)) +c)),k的大小为使得(par(current,k) +csubseteq S(v))的最大值,所以这里任意par(current,i)(0 leq i leq k)都有一条连向v的c字符的边。

    那么我们可以新建一个点w,表示Q,原来的v表示P。首先,显然的,将w的parent改为v的parent,v的parent改为w(注意这两个是有前后顺序的)。然后我们将par(current, 0)..par(current, k)这些点的连向v的c字符的边,全部改为连向w。

    这样我们就完成了“将S(v)分成两半”这一工作了。

    然后同情况一,将把u->parent设为current->child[c] (此时为点w)就可以啦。

    current = last
    while current != NULL
      v = current->child[c]
      if v != NULL
        if v->length = current->length + 1
          u->parent = v
          return 
        else
          w = v
          v->parent = w
          
          current2 = current
          while current2 != NULL and current2->child[c] = v
            current2->child[c] = w
            current2 = current2->parent
          if current 
            assert current2->child[c] = w->parent
          u->parent = w
          return
      else 
        current->child[c] = u
      current = current->parent
    u->parent = root
    

    空间复杂度?

    显然,点数为O(n),因为每次插入最多添加两个点。

    边数我们可以这样算。对于一条u连向v的边,如果u->length+1= v->length,那么我们把这样的边分为第一类,其余的为第二类。

    对于第一类,对于每个点来说,连向它的第一类边只能有一条,所以第一类边的数量为O(n)。

    对于第二类,这里有一个性质:所有的没有出度的结点u,必然存在i,使得(par(last, i)=u),也就是说,所有的(S(u))的并集就是(T(last))

    对于一条u连向v的边,我们通过下面的路径,让它对应一个字符串(一条路径代表一个字符串):

    • 从root到u的最长路
    • 从u到v
    • 从v到最近的没有出度的点

    这样,第二类的每一条都对应一个字符串,且有这样的性质:

    • 每个字符串都不相同
    • 每个字符串都是原串的后缀(看前面的那个性质)

    由于原串只有O(n)个后缀,所以第二类边的边数也是O(n)的。我们可以开(2n)的点数和(3n)的边数即可,如果字符集比较大的话。

    时间复杂度?

    除了下面这个操作,其他都与添加边有关。因为空间复杂度是O(n),所以不算下面这个操作的时间复杂度也是O(n)。

    然后我们将par(current, 0)..par(current, k)这些点的连向v的c字符的边,全部改为连向w。

    我们观察这样一个k值((par(last,k)=root)),每一次插入一个字符c,k的值都加一。每次我们执行上述操作,假设“连向v的c字符的边”数量为x,那么k就要减少x。因为减少的一定小于增加的,所以这个时间复杂都也是O(n)的。

  • 相关阅读:
    灾难 BZOJ 2815
    消耗战 BZOJ 2286
    征途 BZOJ 4518
    纸箱堆叠 BZOJ 2253
    Gate Of Babylon BZOJ 1272
    std::string::npos mean
    [转]整理索引碎片,提升SQL Server速度
    笔记本win7制作wifi
    关闭linux下的使用的端口
    linux多线程
  • 原文地址:https://www.cnblogs.com/wangck/p/4799092.html
Copyright © 2020-2023  润新知