参考:

Ukkonen算法讲解

Ukkonen算法动画

Ukkonen算法,以字符串abcabxabcd为例,先介绍一下运算过程,最后讨论一些我自己的理解。

需要维护以下三个变量:

  • 当前扫描位置#
  • 三元组活动节点(AN),活动边(AE),活动长度(AL)
  • 剩余后缀数:表示还有多少个潜在后缀应该被插入还没有插入

每多扫描一个后缀,其实是增加了一个新的后缀,从#=0-2的过程可以看出。

举个例子:

  • ab的后缀有abb,可以表示成[0,],[1,]
  • abc的后缀有abc,bcc,可以表示成[0,],[1,][2,]

增加了c之后,前两个后缀事实上可以使用相同的表示法,这样只有一个新的c后缀需要被增加

#=0, char='a'

  • 增加前:(root, "", 0), remainder = 1
  • 为根节点增加一条边:[0,],由于确实增加了条边,所以remainder会减少
  • 增加后:(root, "", 0), remainder = 0

#=1, char='b'

  • 增加前:(root, "", 0), remainder = 1
  • 为根节点增加一条边:[1,],由于确实增加了条边,所以remainder会减少
  • 增加后:(root, "", 0), remainder = 0

#=2, char='c'

  • 增加前:(root, "", 0), remainder = 1
  • 为根节点增加一条边:[2,],由于确实增加了条边,所以remainder会减少
  • 增加后:(root, "", 0), remainder = 0

#=3, char='a'

  • 增加前:(root, "", 0), remainder = 1
  • 由于新增加的后缀a已经在根结点(活动节点)处伸了一个边出去了,所以这回不增加,只是修改三元组

    现在我们知道了,三元组(AN,AE,AL)的意思是,活动边为从AN伸出的以AE开头的边的第AL个字符后。

    并且由于这条边表示为[i,],所以它的总长度应为#-i+1,分界线前最后一个字符为str[i+AL-1],分界线后第一个字符为i+AL

  • 增加后:(root,"a"/[0,], 1), remainder = 1

#=4, char='b'

  • 增加前:(root,"a"/[0,], 1), remainder = 2
  • 发生的事情跟上一步一样
  • 增加后:(root,"a"/[0,], 2), remainder = 2

#=5, char='x'

  • 增加前:(root,"a"/[0,], 2), remainder = 3

  • 这一步发生了复杂的事情,现在有待增加的后缀已经有3个了,分别是abxbxx

    可以看出,有待增加的后缀为str[#-remainder+1:#]中的remainder个后缀

    由于cx不同,之前的偷懒现在要偿还了:

    • 第一个添加abx,它必然在三元组指示的地方分裂,分裂一个新节点ab【其实也可以用下标, 后面再说】,原来的叶子边修改为[2,]【显然是因为[i+AL,]】,新增加的叶子结点一定为[5,]

      注意这里分裂的新结点是非叶结点!!! 因为相当于竖线的地方有个隐藏节点, 把它变成真实节点了

      从另一个方面想, 为什么增加非叶结点呢? 是因为要保留原来1后面的子树

      由于插入了一条边,remainder减少为2

    • 然后执行向插入bx。注意,原来三元组为(root, "a"/[0,], 2),匹配的是abx,那么显然,从root开始匹配bx的话,会从[1,]开始匹配,AL也减少一位。所以,插入b的时候三元组变成(root,"b"/[1, ], 1)(如上图)

      插入x的时候新增中间节点"b",原来的[1,]变成[2,],新增叶子节点[5,]

      这里有一个新的规则:

      #相等的同一次扫描里,分裂的结点之间要有链接,由先分裂的指向后分裂的,如图

    • 现在remainder减少为1,因为cx不匹配,从root开始匹配x是找不到合适的边的,所以没办法将三元组变成(root,"c"/[2, ], 0),而既然活动长度为0了,三元组还是回到初始的(root,"", 0)状态,在这个状态下插入x,将直接插入新边[5,]

  • 增加后:(root,"", 0), remainder = 0

#=6, char='a'

  • 增加前:(root,"", 0), remainder = 1
  • 发生的事情与#=3时相似
  • 增加后:(root,"a"/"ab", 1), remainder = 1

#=7, char='b'

  • 增加前:(root,"a"/"ab", 1), remainder = 2
  • 发生的事情与#=4时相似,不过这个时候我们发现中间结点"ab"已经遍历完成了,emmm那就要发生活动节点的转移:

    (4,"", 0), remainder = 2
  • 增加后:(4,"", 0), remainder = 2

#=8, char='c'

  • 增加前:(4,"", 0), remainder = 3
  • 推迟后缀的插入
  • 增加后:(4,"c"/[2,], 1), remainder = 3

#=9, char='d'

  • 增加前:(4,"c"/[2,], 1), remainder = 4
  • 这里发生的事情也很复杂:
    • 首先按照之前说的,分裂活动节点4,得到中间节点"c"和两个叶子节点[3,][9,]

    • 按照我们之前学习的规则,本来是要把状态改成:(4,"", 0), remainder = 3然后再向活动节点4增加新后缀bcd的。

      但是4不是根节点,所以它适用于一条新规则:

      (AN,AE,AL)添加完后缀,还要再添加新后缀时,如果节点不是根节点,则要将活动节点转移到链接的下一个节点(设为AN’,如果没有下一个节点,那就转移到root),并保持AE和AL不变。

      这是因为下一个节点和这个节点有相同的边,所以AE和AL都不需要改变! 后面我们详细讨论这个链接

      所以状态变成(6,"c"/[2,], 1), remainder = 3,然后再分裂,得到如图状态:



      (root,"c"/[2,], 1), remainder = 2,同时,由于发生了连续分裂,需要记录链接

    • 接下来的事情不用说了:



      (root,"", 0), remainder = 1,记录链接

    • 最后再添加后缀d

  • 增加后:(root,"", 0), remainder = 0

#=10, char='$'

最后还要假设添加一个虚拟结尾$,这是因为假如结束的时候remainer不为0,有一些后缀被隐藏在了活动节点里,这样得到的后缀树是隐式后缀树,不好用,我们需要保证后缀一定结束在叶子节点。

  • 增加前:(root,"", 0), remainder = 1
  • root加了一个新边
  • 增加后:(root,"", 0), remainder = 0

总结

而另一方面,竖线的位置又可以通过AL来计算,前面我们说过了,是i+AL

此外,由于相等关系,还有一个等式:

str[i+j]==str[#−AL+j],∀0≤j&lt;ALstr[i+j]==str[\#-AL+j], \forall 0\le j&lt; ALstr[i+j]==str[#−AL+j],∀0≤j<AL

总之结论就是,在中间节点通过下标来记录的时候,这些相等关系可以减少我们需要保持的变量,实际使用的过程中根据自己代码的不同考虑清楚它们之间的关系即可。

链接的意义

从上面的图示明显可以看出,链接到一起的节点都伸出了相同的边,而分裂都发生在这些边上。

这是因为,某个位置#处发生的分裂都一定产生一条[#,]的边,所以链接到一起的节点一定有相同的边。

所以链接是为了快速地找到下一个分裂的节点,而不需要再从头开始匹配。

链接的最后一定是root,因为root一定存过所有的边

整理代码

状态变量:(AN,AL)

AE也不需要存,用#AL返推出AE的首字母再从AN里查就可以了

树结构需求
  1. 叶子结点:begin

  2. 非叶子节点:

    • begin
    • end
    • 边列表,按首字母存
    • 链接
  3. 根结点:

    • 边列表,按首字母存

综上,树节点可以定义为:

class SuffixNode(object):
def __init__(index, end=None, suffix=None):
self.begin = index
self.end = end
self.edge = {}
self.suffix = suffix

广义后缀树建立代码

写了一个,还没用题目测试过

class SuffixTree(object):
class SuffixTreeNode(object):
def __init__(self, index, end=None, suffix=None, isleaf = True):
self.begin = index
self.end = end
self.edge = {}
self.suffix = suffix def __init__(self):
self.al = 0
self.s = ""
self.now = self.SuffixTreeNode(-1)
self.root = self.now
self.words = [] def add(self, string):
beg = len(self.s)
self.s += string+'$'
self.words.append(len(self.s))
end = len(self.s)
for ptr in xrange(beg,len(self.s)):
char = self.s[ptr]
self.al += 1 # 多一个等待存的后缀
last = None
while self.al:
ae = self.s[ptr - self.al+1]
if ae in self.now.edge:
# 有边, 开始匹配字符
sc = self.s[self.now.edge[ae].begin + self.al-1]
if sc == char:
# 如果匹配, 不增加边
# 如果有前驱节点, 存链接
if last!=None:
last.suffix = self.now
last = self.now
# 匹配满的时候转移活动节点
if self.al >= self.now.edge[ae].end-self.now.edge[ae].begin:
self.now = self.now.edge[ae]
self.al -= (self.now.end-self.now.begin)
break # 如果隐含了, 就不再分裂, 直接往下一个位置走
else:
# 如果不匹配, 开始分裂
new = self.SuffixTreeNode(self.now.edge[ae].begin, self.now.edge[ae].begin + self.al -1, self.root, False)
self.now.edge[ae].begin = self.now.edge[ae].begin + self.al -1
new.edge[char] = self.SuffixTreeNode(ptr, end)
new.edge[sc] = self.now.edge[ae]
self.now.edge[ae] = new
# 如果有前驱节点, 存链接
if last != None:
last.suffix = new # 因为new添加了新叶子节点
last = new
else:
# 没边, 添加一个新边
self.now.edge[ae] = self.SuffixTreeNode(ptr, end)
# 如果有前驱节点, 存链接
if last != None:
last.suffix = self.now # 因为new添加了新叶子节点
last = self.now
if self.now.begin != -1:
self.now = self.now.suffix
# 活动长度不用变
else:
self.al -= 1

后缀树的建立-Ukkonen算法的更多相关文章

  1. 广义后缀树(GST)算法的简介

    导言 最近软件安全课上,讲病毒特征码的提取时,老师讲了一下GST算法.这里就做个小总结. 简介 基本信息  广义后缀树的英文为Generalized Suffix Tree,简称GST. 算法目的   ...

  2. [算法]从Trie树(字典树)谈到后缀树

    我是好文章的搬运工,原文来自博客园,博主July_,地址:http://www.cnblogs.com/v-July-v/archive/2011/10/22/2316412.html 从Trie树( ...

  3. 从Trie树(字典树)谈到后缀树

    转:http://blog.csdn.net/v_july_v/article/details/6897097 引言 常关注本blog的读者朋友想必看过此篇文章:从B树.B+树.B*树谈到R 树,这次 ...

  4. 后缀树 & 后缀数组

    后缀树: 字符串匹配算法一般都分为两个步骤,一预处理,二匹配. KMP和AC自动机都是对模式串进行预处理,后缀树和后缀数组则是对文本串进行预处理. 后缀树的性质: 存储所有 n(n-1)/2 个后缀需 ...

  5. 012-数据结构-树形结构-哈希树[hashtree]、字典树[trietree]、后缀树

    一.哈希树概述 1.1..其他树背景 二叉排序树,平衡二叉树,红黑树等二叉排序树.在大数据量时树高很深,我们不断向下找寻值时会比较很多次.二叉排序树自身是有顺序结构的,每个结点除最小结点和最大结点外都 ...

  6. 后缀树(Suffix Tree)

          问题描述:               后缀树(Suffix Tree)   参考资料: http://www.cppblog.com/yuyang7/archive/2009/03/29 ...

  7. 后缀树的线性在线构建-Ukkonen算法

    Ukkonen算法是一个非常直观的算法,其思想精妙之处在于不断加字符的过程中,用字符串上的一段区间来表示一条边,并且自动扩展,在需要的时候把边分裂.使用这个算法的好处在于它非常好写,代码很短,并且它是 ...

  8. 后缀树系列一:概念以及实现原理( the Ukkonen algorithm)

    首先说明一下后缀树系列一共会有三篇文章,本文先介绍基本概念以及如何线性时间内构件后缀树,第二篇文章会详细介绍怎么实现后缀树(包含实现代码),第三篇会着重谈一谈后缀树的应用. 本文分为三个部分, 首先介 ...

  9. 笔试算法题(40):后缀数组 & 后缀树(Suffix Array & Suffix Tree)

    议题:后缀数组(Suffix Array) 分析: 后缀树和后缀数组都是处理字符串的有效工具,前者较为常见,但后者更容易编程实现,空间耗用更少:后缀数组可用于解决最长公共子串问题,多模式匹配问题,最长 ...

随机推荐

  1. mysql删除数据后不释放空间问题

    如果表的引擎是InnoDB,Delete From 结果后是不会腾出被删除的记录(存储)空间的. 需要执行:optimize table 表名; eg:optimize table eh_user_b ...

  2. Linux学习 - 输入输出重定向,管道符,通配符

    一.键盘输入读取read read [选项] [变量名] -p [显示信息] 在等待read输入时,输出提示信息 -t [秒数] 指定read输入等待时间 -n [字符数] 指定read只接收n个字符 ...

  3. Android 图片框架

    1.图片框架:Picasso.Glide.Fresco 2.介绍: picasso:和Square的网络库能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现 Glid ...

  4. 【Linux】【Basis】【网络】网络相关的内核参数

    Linux系统内核设置优化tcp网络,# vi /etc/sysctl.conf,添加以下内容 net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies.当出现SYN等待 ...

  5. my39_InnoDB锁机制之Gap Lock、Next-Key Lock、Record Lock解析

    MySQL InnoDB支持三种行锁定方式: 行锁(Record Lock):锁直接加在索引记录上面,锁住的是key. 间隙锁(Gap Lock): 锁定索引记录间隙,确保索引记录的间隙不变.间隙锁是 ...

  6. jQuery - 按回车键触发跳转

    键盘事件有三种: keyup:按键按下去,抬上来后,事件才生效 (推荐) keydown:按键按下去就生效 keypress:与 keydown 事件类似,当按钮被按下时,会发生该事件,与 keydo ...

  7. 线程开启的第一种方法:通过创建Thread的子类的对象的方式

    package cn.itcast.demo16.demo06.Thread;/** * @author newcityman * @date 2019/7/22 - 21:47 */public c ...

  8. 建立资源的方法(Project)

    <Project2016 企业项目管理实践>张会斌 董方好 编著 终于,进入第5章资源计划编制了,所以就不能还在任务工作表里厮混了是吧,那就先进入资源工作表吧:[任务]>[甘特图]& ...

  9. springboot学习(一)

    最近想学习springboot所以在网上找了很多文章参考怎么构建springboot项目以及集成mybatis 集成mybatis的部分参考了这两篇文章 https://blog.csdn.net/t ...

  10. CF1166A Silent Classroom 题解

    Content 现在有 \(n\) 名学生,我们需要将这些学生分到两个班上.对于两名在同一班级的学生,如果他们的名字首字母相同,他们就会聊天. 现在给定这些学生的名字,问最少有多少对学生会在一起聊天. ...