参考:

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. A Child's History of England.5

    Above all, it was in the Roman time, and by means of Roman ships, that the Christian Religion was fi ...

  2. Z可读作zed的出处?

    Commercial and international telephone and radiotelephone SPELLING ALPHABETS between World War I and ...

  3. css通配样式初始化(多款,供君自选)

    腾讯官网 body,ol,ul,h1,h2,h3,h4,h5,h6,p,th,td,dl,dd,form,fieldset,legend,input,textarea,select{margin:0; ...

  4. Linux磁盘分区(三)之查看磁盘分区常用命令

    Linux磁盘分区(三)之查看磁盘分区常用命令转自https://blog.csdn.net/x356982611/article/details/77893264 1.df     df -T 总的 ...

  5. 【分布式】Zookeeper伪集群安装部署

    zookeeper:伪集群安装部署 只有一台linux主机,但却想要模拟搭建一套zookeeper集群的环境.可以使用伪集群模式来搭建.伪集群模式本质上就是在一个linux操作系统里面启动多个zook ...

  6. 面试 Java 后端开发的感受

    上周,密集面试了若干位Java后端候选人,工作经验在3到5年间.我的标准其实不复杂(适用90%小小小公司,BAT等自动忽略): 第一能干活,第二Java基础要好,第三最好熟悉些分布式框架.我相信其它公 ...

  7. Java文件操作(求各专业第一名的学生)

    两个文件:info.txt 存放学生基本信息 学号 学院 专业 姓名 1001 计算机学院 软件工程 刘月 1002 生物工程 服装设计 孙丽 score.txt存放分数信息 学号 学科 成绩 100 ...

  8. 【JavaScript】创建全0的Array

    1.创建一个长度为m的全0数组 var arr = new Array(m).fill(0); 2.创建一个m行n列的全0数组 var arr = new Array(m).fill(new Arra ...

  9. 1、Linux下安装JDK

    1.Linux下安装JDK 1 权限设置(可忽略) 1.1 安装过程与Windows安装过程相差不多,下载解压安装 1.切换root用户( 如果当前登录的用户权限够的话,请忽略这步) 由于创建目录的位 ...

  10. JavaXML解析的四种方法(连载)

    1. xml简介 XML:指可扩展标记语言, Extensible Markup Language:类似HTML.XML的设计宗旨是传输数据,而非显示数据. 一个xml文档实例: 1 <?xml ...