后缀树(Suffix Tree)简介

为避免混乱,本篇介绍不涉及后缀数组/KMP/AC自动机/SAM/广义SAM等相关知识,但需要Trie(字典树)作为前置知识。

后缀树的由来

把原串所有后缀塞入一个Trie,则形成一个后缀Trie。后缀Trie从根到任意节点x的路径与子串一一对应。对应于原串后缀的节点x称为后缀节点(叶节点必然是后缀节点)。

把所有后缀硬塞进Trie,复杂度为O(n^2),与暴力没有区别,于是考虑把后缀Trie上的非根非叶&非后缀节点&儿子不超过1的节点构成的链缩成一个点,则是后缀树(Suffix Tree),此时树边不再是字符而是字符串。

更进一步地,把非根非叶&儿子不超过1的后缀节点也缩掉,则是隐式后缀树(Implicit Suffix Tree)

隐式后缀树的性质

下面几个性质从理解后缀树到构建后缀树到引用后缀树都非常有用,这里不证明。

性质1:记原串为S,记S(x)为从根到任意节点的路径,则节点x所代表的字符串集合为{ S(fa(x))+x入边的任意非空前缀 },记为set(x)。

性质2:每个属于任意set(x)的元素,均为原串的一个子串,且一一对应。

性质3:若s是S的一个后缀,s≠任意S(叶节点),则s是隐式后缀,且所有长度小于s的后缀也都是隐式后缀。

性质4:若x是非根非叶节点,则一定存在非叶节点y,使得S(x)去掉首字符是S(y),记为link(x)=y。

性质5:S(叶节点)与显式后缀一一对应。

性质6:若在原串后加一个从未出现过的字符,则构建出的隐式后缀树没有隐式后缀。

隐式后缀树的构建

ukkonen算法可以在线的以线性复杂度构造隐式后缀树。

我们用len[u]表示节点u的入边字符串长度,st[u]表示节点u的入边首字符在S上的序号。

我们将叶节点的len值赋为inf,这样当插入新的字符时,显式后缀会完成自动更新(显式后缀与叶节点一一对应)。

tran[u][c]≠0表示节点u有开头为c的出边到节点tran[u][c]。0同时表示根节点和空节点,令len[0]=inf可以减少一些判断。

隐式后缀,必然是某个显式后缀的前缀。

设本轮插入的字符为c,now为当前匹配节点,S(now)是当前待插入新后缀的前缀,还剩下rem长度没有匹配。

开始这轮插入时,rem++,n++,显式后缀自动更新,来看如何更新隐式后缀。

首先如果len[tran[now][S[n-rem+1]]]>rem,就跨越这条出边,直到len[tran[now][S[n-rem+1]]] ≤ rem,因为当前待插入新后缀去掉尾字符就是已插入的隐式后缀,这条边肯定能走通。

令v=tran[now][s[n-rem+1]],接下来分为3种情况:

①rem==1且v==0,此时S(now)已经是待插入后缀的最大真前缀,只需新建一个节点u,令st[u]=n,len[u]=inf,tran[now][c]=u。

②非情况①且S[st[v]+rem-1]==c,那么说明待插入后缀已经在树中了,其他更短的待插入后缀也都在了,直接退出这轮插入。

③非情况①且S[st[v]+rem-1]≠c,此时需要分裂v。新建一个z,使st[z]=n,len[z]=inf,再新建一个u,使st[u]=st[v],len[u]=rem-1,tran[now][S[n-rem+1]]=u,tran[u][S[st[v]+rem-1]]=v,tran[u][c]=z。对于原来的v,使st[v]+=rem-1,len[v]-=rem-1。

需要明确的一点是,now≠0时必然是非叶节点,因为now的入边是可以跨越的,而叶节点的len值为inf不可能跨越。当我们在①③情况下成功完成一次插入时,下一个待插入后缀就是当前插入后缀去掉首字符,于是当now≠0时,根据性质4可以直接令now=link[now],否则rem--继续从根开始匹配。

每当开始一轮插入时,令last=0。

对于①②,我们维护link[last]=now,last=now。

对于③,我们维护link[last]=u,last=u。

举例来说,假设当前最长隐式后缀为'abbc',现在插入新字符'c'。假设'abbcc'在之前没有出现过于是作为显式后缀被插入,那么S(last)='abbc',然后需要继续插入'bbcc'。假设'bbcc'在之前没出现过需要作为显式后缀被插入,那么对于情况①,S(now)='bbc',维护link[last]=now;对于情况③,S(u)='bbc',维护link[last]=u。如果'bbcc'在之前已经出现过,那么就是情况②,而'bbcc'在第一次出现时必然是作为显式后缀被插入,即'bbc'这个非根非叶节点一定存在,now也一定会走到这个节点即S(now)='bbc',维护link[last]=now。

PS:情况①③之后结束循环只有一种情况即now=0&rem=1,此时只是在增加了一个连接根节点的叶节点,无需维护link。

复杂度分析

可以看出,每完成一轮插入,rem的实际意义就是当前最长隐式后缀长度。每当执行一次①或③,则完成这轮插入时rem一定减1,而每次开始一轮插入时rem++,因此复杂度是O(n)的。

代码

用python撸了一个在文本T中统计模式串s的出现次数的方法,比较好理解。

具体是在T后面加一个从未出现过的字符,这样叶节点和T的后缀一一对应,而s的出现次数等于其在后缀中作为前缀出现的次数,于是只需统计s所在节点的子树的叶节点个数即可。

from math import inf

class ukkonen():
    def __init__(self):
        self.num=27
        self.tot=0
        self.s=[]
        self.n=0
        self.st=[inf]
        self.L=[inf]
        self.tran=[[0]*self.num]
        self.link=[0]
        self.now=0
        self.rem=0

    def new_node(self,st,L):
        self.tot+=1
        self.st.append(st)
        self.L.append(L)
        self.tran.append([0]*self.num)
        self.link.append(0)
        return self.tot

    def extend_c(self,c):
        self.s.append(c)
        self.n+=1
        rem=self.rem+1
        n,now,tran,link,st,L,s=self.n,self.now,self.tran,self.link,self.st,self.L,self.s
        last=0
        while rem:
            while rem>L[tran[now][s[n-rem]]]:
                now=tran[now][s[n-rem]]
                rem-=L[now]
            v=tran[now][s[n-rem]]
            if rem==1 and not v:
                link[last]=now
                last=now
                tran[now][c]=self.new_node(n-1,inf)
            elif s[st[v]+rem-1]==c:
                link[last]=now
                last=now
                break
            else:
                u=self.new_node(st[v],rem-1)
                tran[now][s[n-rem]]=u
                tran[u][s[st[v]+rem-1]]=v
                tran[u][c]=self.new_node(n-1,inf)
                st[v]+=rem-1
                L[v]-=rem-1
                link[last]=u
                last=u
            if now:
                now=link[now]
            else:
                rem-=1
        self.now,self.rem=now,rem

    def extend_s(self,s):
        for c in s:
            self.extend_c(ord(c)-97)

    def match(self,s):
        q=[ord(c)-97 for c in s]
        m=len(s)
        now,rem=0,m
        n,tran,L,st=self.n,self.tran,self.L,self.st
        while rem:
            if not self.tran[now][q[m-rem]]:
                return -1
                break
            else:
                v=self.tran[now][q[m-rem]]
                k=min(L[v],rem)
                if q[m-rem:m-rem+k]==self.s[st[v]:st[v]+k]:
                    now=v
                    rem-=k
                else:
                    return -1
                    break
        return now

    def dfs(self,u,nleaf):
        if self.L[u]==inf:
            return 1
        for c in range(self.num):
            if self.tran[u][c]:
                nleaf+=self.dfs(self.tran[u][c],0)
        return nleaf

    def count(self,s):
        node=self.match(s)
        if node==-1:
            return 0
        else:
            return self.dfs(node,0)


T='abvaabbvvababaaabbb'
tree=ukkonen()
tree.extend_s(T+'{')
s='ab'
print(tree.match(s),tree.count(s))

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值