Ukkonen算法是一个非常直观的算法,其思想精妙之处在于不断加字符的过程中,用字符串上的一段区间来表示一条边,并且自动扩展,在需要的时候把边分裂。使用这个算法的好处在于它非常好写,代码很短,并且它是在线的,时间复杂度为\(O(n)\) ,是后缀树构建算法的佳选。

算法

我们保存当前节点now的位置,以及剩下还没有实际上插入的后缀数量remain。设当前字符串中已插入的字符数量为\(n\)。

最开始remain+1,n+1,代表当前字符串中多了一个字符,多了一个需要插入的后缀。很明显,当前我们要插入后缀的长度为remain,因为后缀是连续的。所以这个后缀的开头位置为n-remain+1 。如果当前要插入后缀的长度大于当前出边的长度,那么不断往后跳直到符合要求。

这时有三种情况:

  • 不存在需要的出边,那么我们直接加边即可。
  • 存在需要的出边,并且所需字符与边上的字符相同,即要插入的后缀被隐含在这条边中了,那么我们退出
  • 存在需要的边,但所需字符与边上字符不同。这时候我们就需要分裂这条边。
  • 如果插入了边,并且当前点为root,那么remain-1

这时候有一个很明显的问题,如果我们每一次都退回根节点重新查找,那么时间复杂度可以达到\(O(n^2)\)。但我们可以发现一个性质,当前插入的这个后缀的下一个后缀就是我们要插入的下一个后缀。比如说,我们当前插入了abc这个后缀,那么下一个插入后缀必定是bc。这样,我们每次可以把这一次add中的上一个点通过一种特殊的后缀连接连到这个点,那么我们就可以快速跳link来找下一个插入位置了。

如果我们处理的是子串,那么这样就够了,但是如果我们处理的是后缀,那么还需要在最后加入一个没有出现过的字符来把所有的隐含点拿出来。

时间复杂度

每次跳link的时候需要插入的长度其实都是在减的,而需要插入的长度一共最多为\(O(n)\),所以跳来跳去的部分复杂度为\(O(n)\)。而我们注意到每次插入的复杂度都是\(O(1)\)的,并且后缀树的节点个数最多为\(2n-1\),所以插入也是\(O(n)\)的,因此总的复杂度为\(O(n)\)。

资料

在学这个算法的过程中找到了很多资料,选几个比较好的出来分享一下:

Visualization of Ukkonen's Algorithm: 一个非常棒的算法可视化的动画

Ukkonen算法模拟与教程: 良心作者,大家给他点赞!!(后面那个Github上的代码是错的不要学)

代码

这是bzoj3238的代码。不用管graph那一块啦,后缀树就是ST。

#include<cstdio>
#include<cstring>
#include<cctype>
#include<cstdlib>
#include<algorithm>
using namespace std;
typedef long long giant;
const int maxc=28;
const int maxn=1e6+10;
giant ans;
char s[maxn];
struct graph {
struct edge {
int v,w,nxt;
} e[maxn];
int h[maxn],tot,dep[maxn],size[maxn];
graph ():tot(0) {}
void add(int u,int v,int w) {
e[++tot]=(edge){v,w,h[u]};
h[u]=tot;
}
void dfs(int x,int fa) {
size[x]=0;
bool flag=false;
for (int i=h[x],v=e[i].v;i;i=e[i].nxt,v=e[i].v) {
flag=true;
dep[v]=dep[x]+e[i].w;
dfs(v,x);
ans-=(giant)dep[x]*size[x]*size[v]*2ll;
size[x]+=size[v];
}
if (!flag) {
--dep[x];
size[x]=(dep[x]>dep[fa]);
}
}
} G;
struct ST {
const static int inf=1e8;
int t[maxn][maxc],len[maxn],start[maxn],link[maxn],s[maxn],tot,n,rem,now;
ST ():tot(1),n(0),rem(0),now(1) {len[0]=inf;}
int node(int sta,int l) {
start[++tot]=sta,len[tot]=l,link[tot]=1;
return tot;
}
void add(int x) {
s[++n]=x,++rem;
for (int last=1;rem;) {
while (rem>len[t[now][s[n-rem+1]]]) rem-=len[now=t[now][s[n-rem+1]]];
int ed=s[n-rem+1];
int &v=t[now][ed];
int c=s[start[v]+rem-1];
if (!v) {
v=node(n-rem+1,inf);
link[last]=now;
last=now;
} else if (x==c) {
link[last]=now;
last=now;
break;
} else {
int u=node(start[v],rem-1);
t[u][x]=node(n,inf);
t[u][c]=v,start[v]+=rem-1,len[v]-=rem-1;
link[last]=v=u,last=v;
}
if (now==1) --rem; else now=link[now];
}
}
void run() {
for (int i=1;i<=tot;++i) for (int j=1;j<maxc;++j) if (t[i][j]) G.add(i,t[i][j],len[t[i][j]]);
}
} st;
int main() {
#ifndef ONLINE_JUDGE
freopen("test.in","r",stdin);
freopen("my.out","w",stdout);
#endif
scanf("%s",s+1);
int n=strlen(s+1);
for (int i=1;i<=n;++i) st.add(s[i]-'a'+1);
st.add(27);
st.run();
ans=(giant)(n-1)*n*(n+1)>>1;
G.dfs(1,0);
printf("%lld\n",ans);
return 0;
}

后缀树的线性在线构建-Ukkonen算法的更多相关文章

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

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

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

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

  3. 后缀树的建立-Ukkonen算法

    参考: Ukkonen算法讲解 Ukkonen算法动画 Ukkonen算法,以字符串abcabxabcd为例,先介绍一下运算过程,最后讨论一些我自己的理解. 需要维护以下三个变量: 当前扫描位置# 三 ...

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

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

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

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

  6. 【Todo】字符串相关的各种算法,以及用到的各种数据结构,包括前缀树后缀树等各种树

    另开一文分析字符串相关的各种算法,以及用到的各种数据结构,包括前缀树后缀树等各种树. 先来一个汇总, 算法: 本文中提到的字符串匹配算法有:KMP, BM, Horspool, Sunday, BF, ...

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

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

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

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

  9. 字符串 --- KMP Eentend-Kmp 自动机 trie图 trie树 后缀树 后缀数组

    涉及到字符串的问题,无外乎这样一些算法和数据结构:自动机 KMP算法 Extend-KMP 后缀树 后缀数组 trie树 trie图及其应用.当然这些都是比较高级的数据结构和算法,而这里面最常用和最熟 ...

随机推荐

  1. VirtualBox上LInux命令初步学习

    大二的寒假已经接近了尾声,寒假期间我初步使用了VirtualBox虚拟机软件,并安装了ubuntu的操作系统进行了Linux语言的学习.然而寒假期间的学习没有太多的计划,纯粹是为了完成作业而应付性的学 ...

  2. 20155332 2016-2017-2 《Java程序设计》实验一 Java开发环境的熟悉

    实验内容 使用JDK编译.运行简单的Java程序: 使用IDEA 编辑.编译.运行.调试Java程序. 实验知识点 JVM.JRE.JDK的安装位置与区别: 命令行运行javac:java:javac ...

  3. # 20155337 《Android程序设计》实验四实验报告

    20155337 <Android程序设计>实验四实验报告 实验一 实验内容 Android Stuidio的安装测试: 参考<Java和Android开发学习指南(第二版)(EPU ...

  4. 【LG1445】樱花

    [LG1445]樱花 题面 洛谷 题解 \[ \frac 1x+\frac 1y=\frac 1{n!}\\ \frac{x+y}{xy}=\frac 1{n!}\\ n!(x+y)=xy\\ xy- ...

  5. Python socket网络模块

    一.基于TCP协议的socket通信 以打电话为理解方式进行TCP的通信. Server端代码: import socket phone = socket.socket(socket.AF_INET, ...

  6. TPO 02 - The Origins of Cetaceans

    TPO 02 - The Origins of Cetaceans It should be obvious that cetaceans[n. 鲸目动物]-whales, porpoises [n. ...

  7. idea下增加scala

    1 idea工具下,下载scala插件 2 idea下新建scala工程 File——New——module 如果按照上图,设置后点击下载,出现下图下载过慢情况下, 这里我选择了等待,大概等了半小时才 ...

  8. 【springmvc+mybatis项目实战】杰信商贸-6.重点知识回顾

    1.重点知识回顾 Maven1)覆盖仓库文件,实际企业开发,公司会架一个测试服务器,在测试服务器中架私服.我们开发人员的程序,都连接私服.当本地没有项目中要使用的jar,Myeclipse maven ...

  9. 【python 3.6】调用另一个文件的类的方法

    文件1:test12.py 文件2:test13.py 文件1 如下: #!/usr/bin/python # -*- coding: utf-8 -*- ''' ''' class abcd(obj ...

  10. dp算法之硬币找零问题

    题目:硬币找零 题目介绍:现在有面值1.3.5元三种硬币无限个,问组成n元的硬币的最小数目? 分析:现在假设n=10,画出状态分布图: 硬币编号 硬币面值p 1 1 2 3 3 5 编号i/n总数j ...