参考:

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. shell获取目录下(包括子目录)所有文件名、路径、文件大小

    一例shell脚本:取得目录下(包括子目录)所有文件名.路径与文件大小. 代码,shell脚本: lsdir.sh #!/bin/bash # #site: www.jquerycn.cn funct ...

  2. 最基础的SSM框架整合篇

    一.简单理解 Spring.Spring MVC和MyBatis的整合主要原理就是将我们在单独使用Spring MVC和MyBatis过程中需要自己实例化的类都交由Ioc容器来管理,过程分为两步: 第 ...

  3. 【C/C++】习题3-3 数数字/算法竞赛入门经典/数组和字符串

    [题目] 把前n个(n<=10000)的整数顺序写在一起:123456789101112-- 数一数0~9各出现多少次(输出10个整数,分别是0,1,2,--,9出现的次数) [解答] 暴力求解 ...

  4. Apifox(1)比postman更优秀的接口自动化测试平台

    Apifox介绍 Apifox 是 API 文档.API 调试.API Mock.API 自动化测试一体化协作平台,定位 Postman + Swagger + Mock + JMeter.通过一套系 ...

  5. Log4j漏洞源码分析

    Log4j漏洞源码分析 这几天Log4j的问题消息满天飞,今天我们就一起来看看从源码角度看看这个漏洞是如何产生的. 大家都知道这次问题主要是由于Log4j中提供的jndi的功能. 具体涉及到的入口类是 ...

  6. css预处理器和css Modules是干嘛的?

    CSS预处理器 1.css和js的区别 js是编程语言,它可以声明变量,编写逻辑.而css实际上只是个"表",表头是选择器,内容是里面的样式.它并不能写逻辑啥的.也就是说,对于cs ...

  7. Decorator 模式转载

    原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://tianli.blog.51cto.com/190322/35287 摘要:本文深 ...

  8. epoll 使用详解

    epoll - I/O event notification facility在linux的网络编程中,很长的时间都在使用select来做事件触发.在linux新的内核中,有了一种替换它的机制,就是e ...

  9. [BUUCTF]PWN——jarvisoj_level3

    jarvisoj_level3 附件 步骤 例行检查,32位,nx保护 运行一下程序 32位ida载入,shift+f12没有看到程序里有可以直接利用的后面函数,根据运行时的字符串找到了程序的关键函数 ...

  10. matplotlib模块详解

    简单绘图,折线图,并保存为图片 import matplotlib.pyplot as plt x=[1,2,3,4,5] y=[10,5,15,10,20] plt.plot(x,y,'ro-',c ...