后缀三姐妹

P.S.后缀大家族关系:后缀自动机fail指针=后缀树,后缀树前序遍历=后缀数组

一、后缀数组:orz罗穗骞集训队论文

       给每个后缀按字典序排序

       rank[]表示从i开始的后缀排名多少

       sa[]表示排名为i的后缀是从哪儿开始的

       倍增 & dc3(反正dc3我是不会QAQ)

       

       每次倍增后将二元组桶排(我自己yy了一个vector的可能比较慢QAQ)

       h数组按照原串顺序求,利用求上一位h后留下的信息

       应用:RMQ,多串拼接,分组,穷举+判断......

//by xxb
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
#include<vector>
using namespace std;
const int Mx=;
int n,m,rank[Mx],mn[Mx][],h[Mx],sa[Mx],rank1[Mx],tmp[Mx],temp=,Ton[],ton1[Mx],num[Mx];
char ch[Mx];
vector <int> v[Mx],ton[Mx];
void pre()
{
int temp1=;num[]=;for(int i=;i<=n;i++) { if(i==temp1*) temp1*=;num[i]=temp1; }
for(int i=;i<=n;i++) Ton[ch[i]-'a']=;
for(int i=,lst=;i<;i++) if(Ton[i]==) ton1[i]=ton1[lst]+,lst=i;
for(int i=;i<=n;i++) rank[i]=ton1[ch[i]-'a'];
while(temp<n)
{
int cnt=;memset(v,,sizeof(v));memset(ton,,sizeof(ton));
for(int i=;i<=n;i++) tmp[i]=rank[i]*n+rank[i+temp];
for(int i=+temp;i<=n+temp;i++) ton[rank[i]].push_back(i-temp);
for(int i=;i<=n;i++)
if(ton[i].size()!=)
for(int j=,siz=ton[i].size();j<siz;j++) v[rank[ton[i][j]]].push_back(ton[i][j]);
for(int i=;i<=n;i++)
for(int j=,siz=v[i].size();j<siz;j++)
{
if(j==||(rank[v[i][j]]!=rank[v[i][j-]]||rank[v[i][j]+temp]!=rank[v[i][j-]+temp])) cnt++;
rank1[v[i][j]]=cnt;
}
for(int i=;i<=n;i++) rank[i]=rank1[i];
temp*=;
}
for(int i=;i<=n;i++) sa[rank[i]]=i;
temp=;
for(int i=;i<=n;i++)
{
if(temp) temp--;
int now=sa[rank[i]-];
while()
{
if(i+temp>n||now+temp>n) break;
if(ch[i+temp]==ch[now+temp]) temp++;
else break;
}
h[rank[i]]=temp;
}
}
//求某两个后缀的最长公共前缀 (RMQ) from 51 to 72
void solve0()
{
pre();
scanf("%d",&m);
for(int i=;i<=n;i++) mn[i][]=min(h[i],h[i+]);
for(int j=;(<<j)<=n;j++)
for(int i=;i<=n;i++)
{
mn[i][j]=min(mn[i][j-],mn[i+(<<(j-))][j-]);
if(i+(<<j)>n) break;
}
while(m--)
{
int fr,to;scanf("%d%d",&fr,&to);
if(fr==to) { printf("%d\n",n-fr+); continue; }
fr=rank[fr],to=rank[to];
if(fr>to) swap(fr,to);fr++;
if(fr==to) { printf("%d\n",h[fr]); continue; }
int cnt=to-fr-,ans=min(mn[fr][num[cnt]],mn[to-(<<num[cnt])][num[cnt]]);
printf("%d\n",ans);
}
}
//不可重叠最长重复子串 (pku1743) from 74 to 97
bool Jud1(int k)
{
int mn=,mx=;
for(int i=;i<=n;i++)
{
if(h[i]>=k) { mn=min(mn,sa[i]); mx=max(mx,sa[i]); }
else { mn=sa[i]; mx=sa[i]; }
if(mx-mn>=k) return true;
}
return false;
}
void solve1()
{
pre();
int l=,r=n;
while(l!=r)
{
int k=(l+r)/,jud=Jud1(k);
if(jud==&&l==r-) { if(Jud1(r)) { printf("%d\n",r); return ; } else { printf("%d\n",l); return ; } }
if(jud==) r=k;
else l=k;
}
printf("%d\n",l);
}
//可重叠的k次最长重复子串 (pku3261) from 99 to 123
bool Jud2(int len,int k)
{
int cnt=;
for(int i=;i<=n;i++)
{
if(h[i]>=len) cnt++;
else cnt=;
if(cnt>=k) return true;
}
return false;
}
void solve2()
{
pre();
int l=,r=n,k;
scanf("%d",&k);
while(l!=r)
{
int len=(l+r)/,jud=Jud2(len,k);
if(jud==&&l==r-) { if(Jud2(r,k)) { printf("%d\n",r); return ; } else { printf("%d\n",l); return ; } }
if(jud==) r=len;
else l=len;
}
printf("%d\n",l);
}
//可重叠最长重复子串 from 125 to 131
void solve3()
{
pre();
int ans=;
for(int i=;i<=n;i++) ans=max(ans,h[i]);
printf("%d\n",ans);
}
//不相同的子串的个数 (spoj694&&spoj705) from 133 to 139
void solve4()
{
pre();
int ans=;
for(int i=;i<=n;i++) ans+=n+-sa[i]-h[i];
printf("%d\n",ans);
}//连续重复子串 (原串是由一个子串重复而成) (pku2406) from 141 to 157
void solve5()
{
pre();
int Tmp[Mx];memset(Tmp,0x3f,sizeof(Tmp));
for(int i=rank[]-;i>=;i--) Tmp[i]=min(Tmp[i+],h[i+]);
for(int i=rank[]+;i<=n;i++) Tmp[i]=min(Tmp[i-],h[i+]);
Tmp[rank[]]=;
for(int k=;k<=n;k++) //枚举子串长度
if(n%k==)
{
if(Tmp[rank[k+]]==n-k)
{
printf("%d\n",n/k);
return ;
}
}
}
//重复次数最多的连续重复子串 (spoj687,pku3693) from 159 to 237
int Rank[Mx],Mn[Mx][],H[Mx],Sa[Mx];
char Ch[Mx];
void clear()
{
memset(v,,sizeof(v));
memset(ton,,sizeof(ton));
memset(rank1,,sizeof(rank1));
memset(tmp,,sizeof(tmp));
memset(Ton,,sizeof(Ton));
memset(ton1,,sizeof(ton1));
}
void pre1()//反着做一遍后缀数组
{
clear();
for(int i=;i<=n;i++) Ch[n-i+]=Ch[i];
num[]=;temp=;
for(int i=;i<=n;i++) Ton[Ch[i]-'a']=;
for(int i=,lst=;i<;i++) if(Ton[i]==) ton1[i]=ton1[lst]+,lst=i;
for(int i=;i<=n;i++) Rank[i]=ton1[Ch[i]-'a'];
while(temp<n)
{
int cnt=;memset(v,,sizeof(v));memset(ton,,sizeof(ton));
for(int i=;i<=n;i++) tmp[i]=Rank[i]*n+Rank[i+temp];
for(int i=+temp;i<=n+temp;i++) ton[Rank[i]].push_back(i-temp);
for(int i=;i<=n;i++)
if(ton[i].size()!=)
for(int j=,siz=ton[i].size();j<siz;j++) v[Rank[ton[i][j]]].push_back(ton[i][j]);
for(int i=;i<=n;i++)
for(int j=,siz=v[i].size();j<siz;j++)
{
if(j==||(Rank[v[i][j]]!=Rank[v[i][j-]]||Rank[v[i][j]+temp]!=Rank[v[i][j-]+temp])) cnt++;
rank1[v[i][j]]=cnt;
}
for(int i=;i<=n;i++) Rank[i]=rank1[i];
temp*=;
}
for(int i=;i<=n;i++) Sa[Rank[i]]=i;
temp=;
for(int i=;i<=n;i++)
{
if(temp) temp--;
int now=Sa[Rank[i]-];
while()
{
if(i+temp>n||now+temp>n) break;
if(Ch[i+temp]==Ch[now+temp]) temp++;
else break;
}
H[Rank[i]]=temp;
}
}
void solve6()
{
pre();pre1();
for(int i=;i<=n;i++) mn[i][]=min(h[i],h[i+]),Mn[i][]=min(H[i],H[i+]);
for(int j=;(<<j)<=n;j++)
for(int i=;i<=n;i++)
{
mn[i][j]=min(mn[i][j-],mn[i+(<<(j-))][j-]);
Mn[i][j]=min(Mn[i][j-],Mn[i+(<<(j-))][j-]);
if(i+(<<j)>n) break;
}
int ans=;
for(int l=;l<=n;l++)
{
for(int i=l;i<=n;i+=l)
{
int fr,to;temp=l;
fr=rank[i-l],to=rank[i];
if(fr>to) swap(fr,to);fr++;
int cnt=to-fr-;temp+=min(mn[fr][num[cnt]],mn[to-(<<num[cnt])][num[cnt]]);
fr=Rank[i-l],to=Rank[i];
if(fr>to) swap(fr,to);fr++;
cnt=to-fr-;temp+=min(Mn[fr][num[cnt]],Mn[to-(<<num[cnt])][num[cnt]]);
ans=max(ans,(temp/l)+);
}
}
printf("%d\n",ans);
}
//最长公共子串 (pku2774) from 239 to 252
void solve7()
{
ch[++n]='$';n++;
scanf("%d",&m);
for(int i=;i<=m;)
{
scanf("%c",&ch[n]);
if(ch[n]>='a'&&ch[n]<='z') i++,n++;
}
pre();int ans=;
for(int i=;i<=n;i++)
if((sa[i]<=n&&sa[i-]>n+)||(sa[i-]<=n&&sa[i]>n+)) ans=max(ans,h[i]);
printf("%d\n",ans);
}int main()
{
scanf("%d",&n);
for(int i=;i<=n;)
{
scanf("%c",&ch[i]);
if(ch[i]>='a'&&ch[i]<='z') i++;
}
return ;
}

二、后缀树(通常用不到,略)

三、后缀自动机

  如:字符串aabbabd,建出自动机如下图:

  

   每一个后缀都是从s走到终结状态(红色节点)

   copy from hihocoder

  SAM的States

  小Hi:这一节我们将介绍给定一个字符串S,如何确定S对应的SAM有哪些状态。首先我们先介绍一个概念 子串的结束位置集合 endpos。对于S的一个子串s,endpos(s) = s在S中所有出现的结束位置集合。还是以S="aabbabd"为例,endpos("ab") = {3, 6},因为"ab"一共出现了2次,结束位置分别是3和6。同理endpos("a") = {1, 2, 5}, endpos("abba") = {5}。

  小Hi:我们把S的所有子串的endpos都求出来。如果两个子串的endpos相等,就把这两个子串归为一类。最终这些endpos的等价类就构成的SAM的状态集合。例如对于S="aabbabd"

状态 子串 endpos
S 空串 {0,1,2,3,4,5,6}
1 a {1,2,5}
2 aa {2}
3 aab {3}
4 aabb,abb,bb {4}
5 b {3,4,6}
6 aabba,abba,bba,ba {5}
7 aabbab,abbab,bbab,bab {6}
8 ab {3,6}
9 aabbabd,abbabd,bbabd,babd,abd,bd,d {7}

  小Ho:这些状态恰好就是上面SAM图中的状态。

  小Hi:没错。此外,这些状态还有一些美妙的性质,且等我一一道来。首先对于S的两个子串s1和s2,不妨设length(s1) <= length(s2),那么 s1是s2的后缀当且仅当endpos(s1)⊇endpos(s2),s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅。

  小Ho:我验证一下啊... 比如"ab"是"aabbab"的后缀,而endpos("ab")={3,6},endpos("aabbab")={6},是成立的。"b"是"ab"的后缀,endpos("b")={3,4,6}, endpos("ab")={3,6}也是成立的。"ab"不是"abb"的后缀,endpos("ab")={3,6},endpos("abb")={4},两者没有交集也是成立的。怎么证明呢?

  小Hi:证明还是比较直观的。首先证明s1是s2的后缀=>endpos(s1) ⊇ endpos(s2):既然s1是s2后缀,所以每次s2出现时s1以必然伴随出现,所以有endpos(s1) ⊇ endpos(s2)。再证明endpos(s1) ⊇ endpos(s2)=>s1是s2的后缀:我们知道对于S的子串s2,endpos(s2)不会是空集,所以endpos(s1) ⊇ endpos(s2)=>存在结束位置x使得s1结束于x,并且s2也结束于x,又length(s1) <= length(s2),所以s1是s2的后缀。综上我们可知s1是s2的后缀当且仅当endpos(s1) ⊇ endpos(s2)。s1不是s2的后缀当且仅当endpos(s1) ∩ endpos(s2) = ∅是一个简单的推论,不再赘述。

  小Ho:我好像对SAM的状态有一些认识了!我刚才看上面的表格就觉得SAM的一个状态里包含的子串好像有规律。考虑到SAM中的一个状态包含的子串都具有相同的endpos,那它们应该都互为后缀?

  小Hi:你观察力还挺敏锐的。下面我们就来讲讲一个状态包含的子串究竟有什么关系。上文提到我们把S的所有子串按endpos分类,每一类就代表一个状态,所以我们可以认为一个状态包含了若干个子串。我们用substrings(st)表示状态st中包含的所有子串的集合,longest(st)表示st包含的最长的子串,shortest(st)表示st包含的最短的子串。例如对于状态7,substring(7)={aabbab,abbab,bbab,bab},longest(7)=aabbab,shortest(7)=bab。

  小Hi:对于一个状态st,以及任意s∈substrings(st),都有s是longest(st)的后缀。证明比较容易,因为endpos(s)=endpos(longest(st)),所以endpos(s) ⊇ endpos(longest(st)),根据我们刚才证明的结论有s是longest(st)的后缀。

  小Hi:此外,对于一个状态st,以及任意的longest(st)的后缀s,如果s的长度满足:length(shortest(st)) <= length(s) <= length(longsest(st)),那么s∈substrings(st)。 证明也是比较容易,因为:length(shortest(st)) <= length(s) <= length(longsest(st)),所以endpos(shortest(st)) ⊇ endpos(s) ⊇ endpos(longest(st)), 又endpos(shortest(st)) = endpos(longest(st)),所以endpos(shortest(st)) = endpos(s) = endpos(longest(st)),所以s∈substrings(st)。

  小Ho:这么说来,substrings(st)包含的是longest(st)的一系列连续后缀?

  小Hi:没错。比如你看状态7中包含的就是aabbab的长度分别是6,5,4,3的后缀;状态6包含的是aabba的长度分别是5,4,3,2的后缀。

  SAM的Suffix Links

  小Hi:前面我们讲到substrings(st)包含的是longest(st)的一系列连续后缀。这连续的后缀在某个地方会“断掉”。比如状态7,包含的子串依次是aabbab,abbab,bbab,bab。按照连续的规律下一个子串应该是"ab",但是"ab"没在状态7里,你能想到这是为什么么?

  小Ho:aabbab,abbab,bbab,bab的endpos都是{6},下一个"ab"当然也在结束位置6出现过,但是"ab"还在结束位置3出现过,所以"ab"比aabbab,abbab,bbab,bab出现次数更多,于是就被分配到一个新的状态中了。

  小Hi:没错,当longest(st)的某个后缀s在新的位置出现时,就会“断掉”,s会属于新的状态。比如上例中"ab"就属于状态8,endpos("ab"}={3,6}。当我们进一步考虑"ab"的下一个后缀"b"时,也会遇到相同的情况:"b"还在新的位置4出现过,所以endpos("b")={3,4,6},b属于状态5。在接下去处理"b"的后缀我们会遇到空串,endpos("")={0,1,2,3,4,5,6},状态是起始状态S。

  小Hi:于是我们可以发现一条状态序列:7->8->5->S。这个序列的意义是longest(7)即aabbab的后缀依次在状态7、8、5、S中。我们用Suffix Link这一串状态链接起来,这条link就是上图中的绿色虚线。

  小Ho:原来如此。

  小Hi:Suffix Links后面会有妙用,我们暂且按下不表。

  SAM的Transition Function

  小Hi:最后我们来介绍SAM的转移函数。对于一个状态st,我们首先找到从它开始下一个遇到的字符可能是哪些。我们将st遇到的下一个字符集合记作next(st),有next(st) = {S[i+1] | i ∈ endpos(st)}。例如next(S)={S[1], S[2], S[3], S[4], S[5], S[6], S[7]}={a, b, d},next(8)={S[4], S[7]}={b, d}。

  小Hi:对于一个状态st来说和一个next(st)中的字符c,你会发现substrings(st)中的所有子串后面接上一个字符c之后,新的子串仍然都属于同一个状态。比如对于状态4,next(4)={a},aabb,abb,bb后面接上字符a得到aabba,abba,bba,这些子串都属于状态6。

  小Hi:所以我们对于一个状态st和一个字符c∈next(st),可以定义转移函数trans(st, c) = x | longest(st) + c ∈ substrings(x) 。换句话说,我们在longest(st)(随便哪个子串都会得到相同的结果)后面接上一个字符c得到一个新的子串s,找到包含s的状态x,那么trans(st, c)就等于x。

  小Ho:吼~ 终于把SAM中各个部分搞明白了。

北京培训记day2的更多相关文章

  1. 北京培训记day5

    高级数据结构 一.左偏树&斜堆 orz黄源河论文 合并,插入,删除根节点 打标记 struct Node { int fa,l,r,w,dep } tree[Mx]; int Merge(in ...

  2. 北京培训记day3

    网络流 一.基础知识点: [容量网络] 图G(V,E)为有向网络,在V中指定一个源点和一个汇点,流量从源点出发经过有向网络流向汇点.对于每一条有向边有权值C,称作弧的容量.有向边称为弧.这样的有向网络 ...

  3. 北京培训记day1

    数学什么的....简直是丧心病狂啊好不好 引入:Q1:前n个数中最多能取几个,使得没有一个数是另一个的倍数   答案:(n/2)上取整 p.s.取后n/2个就好了 Q2:在Q1条件下,和最小为多少 答 ...

  4. 北京培训记day4

    智商题QAQ-- T1:求>=n的最小素数,n<=10^18 暴力枚举n-n+100,miller-rabin筛法 T2:给定一个01矩阵,每次选择一个1并将(x,y)到(1,1)颜色反转 ...

  5. 福建省队集训被虐记——DAY2

    唉--第二天依然被虐--但是比第一天好一点--我必须负责任的指出:志灿大神出的题比柯黑的不知道靠谱到哪里去了--柯黑出的简直不可做 但是被虐的命运是无法改变的--求各位神犇别D我 黄巨大真是强啊,不愧 ...

  6. 我的屌丝giser成长记-研二篇

    之前有提到过的,本来按照计划中,研一结束就该去深圳中科院研究所实习的,之前跟里面师兄说好了的,奈何导师又接到一个新的科研研究项目,跟学院的几个其他老师一起合作的,主要是关于土地流转系统,而且是一个挺大 ...

  7. Python基础篇-day2

    主要内容: for循环 while循环 格式化输出(2) 数据统计及记录 ############################################################# 1 ...

  8. .Net面试经验,从北京到杭州

    首先简单说下,本人小本,目前大四软件工程专业,大三阴差阳错地选了.Net方向,也是从大三开始接触.Net.自认为在学生中.net基础还可以,嘿嘿,吹一下. 大四第一学期学校安排去北京培训,培训了两个月 ...

  9. BJOI2018爆零记

    没啥可说的 Day1 0分 T1 给你一个二进制串,每次修改一个位置,询问[l,r]区间中有多少二进制子串重排后能被3整除 T2 一个无向图(无重边自环)每个点有一个包含两种颜色的染色集合,一个边的两 ...

随机推荐

  1. 在xampp中配置dvwa

    DVWA主要是用于学习Web的常见攻击,比如SQL注入.XSS等的一个渗透测试系统,下面我将结合XAMPP来说明它的安装过程. 一.环境 OS:Windows 10 XAMPP:5.6.24 DVWA ...

  2. JAVA编程思想(第四版)学习笔记----4.8 switch(知识点已更新)

    switch语句和if-else语句不同,switch语句可以有多个可能的执行路径.在第四版java编程思想介绍switch语句的语法格式时写到: switch (integral-selector) ...

  3. 批量创建10个用户stu01-stu10

    1.批量创建10个用户stu01-stu10,并且设置随机8位密码,要求不能用shell循环(例如:for,while等),只能用命令及管道实现. ##方法1: [root@server tmp]# ...

  4. 64位下pwntools中dynELF函数的使用

    这几天有同学问我在64位下怎么用这个函数,于是针对同一道题写了个利用dynELF的方法 编译好的程序 http://pan.baidu.com/s/1jImF95O 源码在后面 from pwn im ...

  5. 4-3 管理及IO重定向

    1. 系统设定默认输出设备:标准输出(STDOUT,1) 系统设定默认输入设备:标准输入(STDIN,0) 系统设定默认错误设备:标准错误(STDERR,2) 2. 标准输入:键盘 标准输出和错误输出 ...

  6. linux基础知识3_根文件系统详解

    文件系统: rootfs:根文件系统 /boot:系统启动相关的文件,如内核.initrd以及grub /dev:设备文件 块设备:随机访问 字符设备:线性访问,按字符为单位 设备号:主设备号(maj ...

  7. UC浏览器中touch事件的异常记录

    以前也在UC上面栽过几个坑,不过都是页面显示方面的.上个周的时候,商品详情页重做,要添加个上拉显示详情的效果. 有两个条件需要判断: 1.是否到达底部: 2.到达底部之后拖动的y轴距离. 效果写完后, ...

  8. JS--遍历对象

    var person = { Name:"Frank", Age:23 } Object.keys(person).forEach(function(key){ console.l ...

  9. NYOJ----776删除元素

    删除元素 时间限制:1000 ms  |  内存限制:65535 KB 描述 题意很简单,给一个长度为n的序列,问至少删除序列中多少个数,使得删除后的序列中的最大值<= 2*最小值 输入 多组测 ...

  10. LA 3231 - Fair Share

    You are given N processors and M jobs to be processed. Two processors are specified to each job. To ...