后缀树的线性在线构建-Ukkonen算法
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算法的更多相关文章
- [算法]从Trie树(字典树)谈到后缀树
我是好文章的搬运工,原文来自博客园,博主July_,地址:http://www.cnblogs.com/v-July-v/archive/2011/10/22/2316412.html 从Trie树( ...
- 从Trie树(字典树)谈到后缀树
转:http://blog.csdn.net/v_july_v/article/details/6897097 引言 常关注本blog的读者朋友想必看过此篇文章:从B树.B+树.B*树谈到R 树,这次 ...
- 后缀树的建立-Ukkonen算法
参考: Ukkonen算法讲解 Ukkonen算法动画 Ukkonen算法,以字符串abcabxabcd为例,先介绍一下运算过程,最后讨论一些我自己的理解. 需要维护以下三个变量: 当前扫描位置# 三 ...
- 笔试算法题(40):后缀数组 & 后缀树(Suffix Array & Suffix Tree)
议题:后缀数组(Suffix Array) 分析: 后缀树和后缀数组都是处理字符串的有效工具,前者较为常见,但后者更容易编程实现,空间耗用更少:后缀数组可用于解决最长公共子串问题,多模式匹配问题,最长 ...
- 后缀树系列一:概念以及实现原理( the Ukkonen algorithm)
首先说明一下后缀树系列一共会有三篇文章,本文先介绍基本概念以及如何线性时间内构件后缀树,第二篇文章会详细介绍怎么实现后缀树(包含实现代码),第三篇会着重谈一谈后缀树的应用. 本文分为三个部分, 首先介 ...
- 【Todo】字符串相关的各种算法,以及用到的各种数据结构,包括前缀树后缀树等各种树
另开一文分析字符串相关的各种算法,以及用到的各种数据结构,包括前缀树后缀树等各种树. 先来一个汇总, 算法: 本文中提到的字符串匹配算法有:KMP, BM, Horspool, Sunday, BF, ...
- 广义后缀树(GST)算法的简介
导言 最近软件安全课上,讲病毒特征码的提取时,老师讲了一下GST算法.这里就做个小总结. 简介 基本信息 广义后缀树的英文为Generalized Suffix Tree,简称GST. 算法目的 ...
- 012-数据结构-树形结构-哈希树[hashtree]、字典树[trietree]、后缀树
一.哈希树概述 1.1..其他树背景 二叉排序树,平衡二叉树,红黑树等二叉排序树.在大数据量时树高很深,我们不断向下找寻值时会比较很多次.二叉排序树自身是有顺序结构的,每个结点除最小结点和最大结点外都 ...
- 字符串 --- KMP Eentend-Kmp 自动机 trie图 trie树 后缀树 后缀数组
涉及到字符串的问题,无外乎这样一些算法和数据结构:自动机 KMP算法 Extend-KMP 后缀树 后缀数组 trie树 trie图及其应用.当然这些都是比较高级的数据结构和算法,而这里面最常用和最熟 ...
随机推荐
- 20155328 2016-2017-2 《Java程序设计》 课程总结
20155328 2016-2017-2 <Java程序设计> 课程总结 目录 一.每周作业链接汇总 二.实验报告链接汇总 三.代码托管链接 四.课堂项目实践 五.课程收获与不足 六.问卷 ...
- 20155328 《Java程序设计》 实验二(Java面向对象程序设计) 实验报告
20155328 <Java程序设计> 实验二(Java面向对象程序设计) 实验报告 单元测试 一.单元测试和TDD 编程时需理清思路,将编程需求等想好,再开始编.此部分可用伪代码实现. ...
- 20145234黄斐《Java程序设计》第九周
教材学习内容总结 整合数据库 JDBC入门 JDBC是用于执行SQL的解决方案,开发人员使用JDBC的标准接口,数据库厂商则对接口进行操作,开发人员无须接触底层数据库驱动程序的差异性,数据库本身是个独 ...
- 【LG4317】花神的数论题
[LG4317]花神的数论题 题面 洛谷 题解 设\(f_{i,up,tmp,d}\)表示当前在第\(i\)位,是否卡上界,有\(tmp\)个一,目标是几个一的方案数 最后将所有\(d\)固定,套数位 ...
- Codeception (安装)
来源:http://codeception.com/install 注意:打开Codeception的官网需要FQ 1. 下载 下载地址:http://codeception.com/thanks 或 ...
- git remote: error: hook declined to update
提交一个项目,push的时候,报错: remote: error: File xxx.rar is MB; this exceeds Git@OSC's file size limit of 100 ...
- 前端--javaScript之简单介绍
一.javaScript(以下简称js)的历史 1992年Nombas开发出C-minus-minus(C--)的嵌入式脚本语言(最初绑定在CEnvi软件中).后将其改名ScriptEase.(客户端 ...
- intellij idea maven配置及maven项目创建
1. 下载Maven 官方地址:http://maven.apache.org/download.cgi 解压并新建一个本地仓库文件夹 2.配置maven环境变量 3.配置配置本地仓库路径 4.配置阿 ...
- 网络流小结(HNOI2019之前)
\(\text{一:Dinic最大流}\) 最坏复杂度 \({\mathcal O(n^2m)}\) 一般可以处理 \(10^4\) ~ \(10^5\) 的网络. struct Edge { int ...
- Python模块random使用详情
python常用模块目录 1.random.random()#用于生成一个0到1的随机浮点数:0<= n < 1.0 import random mcw = random.random() ...