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. Linux 下 的 Oracle,如何安装 tnsname

    运行 netca 即可:

  2. 【LG3321】[SDOI2015]序列统计

    [LG3321][SDOI2015]序列统计 题面 洛谷 题解 前置芝士:原根 我们先看一下对于一个数\(p\),它的原根\(g\)有什么性质(好像就是定义): \(g^0\%p,g^1\%p,g^2 ...

  3. Pandas v0.23.4手册汉化

    Pandas手册汉化 此页面概述了所有公共pandas对象,函数和方法.pandas.*命名空间中公开的所有类和函数都是公共的. 一些子包是公共的,其中包括pandas.errors, pandas. ...

  4. php小项目小结

    最近一直断更,并不是出于什么问题,而是想找个合适的机会去整理下html基本的一些琐碎的知识点 近期突发感冒,吊水,吊错药,原因只是重名重姓,这不是个梗,很是痛苦的现实事故 so,只能用剩下的半天去完成 ...

  5. JS Windows.document对象

    四中选择器:class ,id , name , 标签 通过选择器获取对象: ...................................ClassName('');  -- class选择 ...

  6. javaweb(二十二)——基于Servlet+JSP+JavaBean开发模式的用户登录注册

    一.Servlet+JSP+JavaBean开发模式(MVC)介绍 Servlet+JSP+JavaBean模式(MVC)适合开发复杂的web应用,在这种模式下,servlet负责处理用户请求,jsp ...

  7. Python之requests的安装

    在 windows 系统下,只需要输入命令 pip install requests ,即可安装. 在 linux 系统下,只需要输入命令 sudo pip install requests ,即可安 ...

  8. Unity标准材质官方教程合集

  9. JAVA基础学习之路(九)[2]String类常用方法

    字符与字符串: 1.将字符数组变为字符串(构造方法) public String(char[] value) Allocates a new String so that it represents ...

  10. Python数据挖掘——数据概述

    Python数据挖掘——数据概述 数据集由数据对象组成: 数据的基本统计描述 中心趋势度量 均值 中位数 众数 中列数 数据集的最大值和最小值的平均 度量数据分布 极差 最大值与最小值的差 四分位数 ...