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. Java SE教程

    第0讲 开山篇 读前介绍:本文中如下文本格式是超链接,可以点击跳转 >>超链接<< 我的学习目标:基础要坚如磐石   代码要十份规范   笔记要认真详实 一.java内容介绍 ...

  2. linux挂在samba服务器到本地(用于备份文件到nas或者windows的文件服务器)

    1.安装工具 首先在linux上安装samba访问工具 sudo apt-get install smbclient sudo apt-get install cifs-utils 2.查看服务器目录 ...

  3. Maven学习(十六)-----Maven插件

    Maven插件 Maven 是一个执行插件的框架,每一个任务实际上是由插件完成的.Maven 插件通常用于: 创建 jar 文件 创建 war 文件 编译代码文件 进行代码单元测试 创建项目文档 创建 ...

  4. Swoole实现h5版聊天室笔记

    声明:该聊天室目前只有一对多,一对一的聊天功能,另外,因为没有使用到mysql,所以还存在比较多的缺陷地方,但知道原理就差不多了,这里主要分享下swoole简易的聊天室制作思路. 开发环境:cento ...

  5. [转]git命令之git remote的用法

    git remote git  remote -v git init git add xxx git commit -m 'xxx' git remote add origin ssh://softw ...

  6. 在Emacs 23里字体的调整(转自ChinaUnix.net)

    首先,在Emacs中,通过菜单Options --> Set Default Font,设置好你喜欢的字体. 然后,把光标放到你所在的字体上,用命令M-x describe-font来查看你当前 ...

  7. scrapy+selenium+chromedriver解析动态渲染页面

    背景:动态页面是页面是通过js代码渲染出来的,无法直接使用scrapy爬虫,这是就需要先把js代码转为静态的html,再用scrapy爬虫就可以解决 解决办法:增加SeleniumMiddleware ...

  8. “Hello World!”团队第十三次会议

    今天是我们团队“Hello World!”团队召开的第十三次会议.博客内容: 一.会议时间 二.会议地点 三.会议成员 四.会议内容 五.todo list 六.会议照片 七.燃尽图 一.会议时间 2 ...

  9. YQCB冲刺周第一天

    团队讨论的照片 任务看板为 今天小组成员讨论了每个页面的排版,每个页面的跳转,以及页面的排版. 今天准备编写登录界面的.注册界面的代码. 遇到的困难是用户记账时选择的分类标准很多,最后将其整合,删减.

  10. Saver 保存与读取

    tensorflow 框架下的Saver 功能,用以保存和读取运算数据 Saver 保存数据 代码 import tensorflow as tf # Save to file #remember t ...