更新至 11.23。
1.SA
模拟退火后缀数组(Suffix Array)是一种很奇妙的算法。主要原因是它可以做到在 (O(nlog n)) 时间内完成排序。
关于如何完成这个比较基础,具体可见洛谷日报。
而后缀排序的重点在于“字典序排序”的一些奇妙性质。所以对于一般字符串的字典序排序,以下性质也适用。
首先可以发现的是 (operatorname{LCP}(i,j)=min(operatorname{LCP}(i,k),operatorname{LCP}(k,j)),kin[i,j])。这个比较显然主要我也不怎么会严格证明。具体可以见洛谷日报的证明。
考虑有了这个我们可以干什么。考虑这样一道题:按一定方式给定一堆字符串(总长度可能很大),问其中本质不同前缀的个数。
那么显然可以发现,相邻两字符串的 (operatorname{LCP}) 就是他们本质相同的前缀。换句话说,除此之外的部分都是本质不同的。
而根据那个奇怪的性质,相邻两个字符串 ((x,x+1)) 的 (operatorname{LCP}) 一定 (geq (i,k),kgeq i+1) 的 (operatorname{LCP})。所以显然成立。
但是这个相邻的 (operatorname{LCP}) 怎么求呢?
其实是有一个很simple的 (O(n)) 求法。什么SA-IS?完全不会。
具体来说,我们可以求出第 (i) 个位置与字典序在它前面的串的 (operatorname{LCP}) (h_i)。可以发现有 (h_{i}=h_{i-1}+1)。于是乎就均摊 (O(n)) 了。
那么我们可以做什么了呢?求本质不同子串!每个后缀的前缀唯一对应一个子串,所以直接减就好了。
例:本质不同子串
#include<iostream>
#include<cstdio>
#include<cstring>
#define N 100010
using namespace std;
int b[N],sa[N],rk[N],a[N],id[N];
char s[N];
void SA_(int n,int m)
{
for(int i=0;i<=m;i++) b[i]=0;
for(int i=1;i<=n;i++) b[rk[i]]++;
for(int i=1;i<=m;i++) b[i]+=b[i-1];
for(int i=n;i>=1;i--) sa[b[rk[id[i]]]--]=id[i];
}
void SA(int n)
{
int m=124;
for(int i=1;i<=n;i++) rk[i]=s[i]-'0'+1,id[i]=i;
SA_(n,m);int t=0;
for(int p=1;p<n;m=t,t=0,p<<=1)
{
for(int i=1;i<=p;i++) id[++t]=n-p+i;
for(int i=1;i<=n;i++) if(sa[i]>p) id[++t]=sa[i]-p;
SA_(n,m); swap(id,rk); rk[sa[1]]=t=1;
for(int i=2;i<=n;i++) rk[sa[i]]=(t+=id[sa[i-1]]!=id[sa[i]] || id[sa[i-1]+p]!=id[sa[i]+p]);
}
}
int ht[N];
void get_ht(int n)
{
for(int i=1,p=0;i<=n;ht[rk[i]]=p,i++)
if(rk[i]!=1) for(p=p-!!p;sa[rk[i]-1]+p<=n && i+p<=n && s[i+p]==s[sa[rk[i]-1]+p];p++);
}
int main()
{
int n;
scanf("%d%s",&n,s+1);
SA(n);
get_ht(n);
long long ans=1ll*n*(n+1)/2;
for(int i=1;i<=n;i++) ans-=ht[i];
printf("%lld
",ans);
return 0;
}
// 压行?怎么可能?这叫 建筑美(
看到这你或许会问:这个不是SAM也能做吗?而且SAM是 (O(n)) 的。
的确,绝大部分SA能做的SAM都能做,而且SAM跑得快、支持在线、还更好些(所以我学SA干什么)。
别急,这里还有一个SA的晋升版本:
2.后缀平衡树(好像不一定是这么叫的)
没想到吧,后缀平衡树居然不是后缀树变过来的(我也没想到)。
首先我们还是考虑一般情况:给定一个字符串 (S) 的一堆子串,每次问某个子串 (s0) 与其他每个串的 (operatorname{LCP}) 最大是多少。动态修改子串集合。
这个可以怎么做?考虑使用平衡树套Hash。具体可以见Hash学习笔记中的一道口胡的题(里面好像还有一个强制在线)。
这个是 (O(nlog^2 n)) 的,虽然比较暴力已经足够优秀了。但是如果我们插入的字符串有一些规律可循,是不是有更快的做法。
【模板】后缀平衡树
题意
维护一个字符串,支持加一堆字符,删一堆字符,询问某个字符串出现次数。强制在线。总字符串长度 (leq 10^6)。
出题人真的是丧心病狂。。。
AC自动机能过?那就强制在线。
SAM还能过?那就每次加一堆字符。
啥?两只 (log) 艹过去了?那就开到 (10^6)。真·5步出题法。
显然我们需要一个更高妙的做法。考虑多一只 (log) 的瓶颈在于每次判断字典序时必须 (O(log n)) 处理。再加上判断 (O(log n)) 次,所以总 (O(log^2 n))。
平衡树的 (log n) 没办法优化,考虑优化判断字典序。可以发现,我们要加入的字符串 (u) 在加入前它的前缀一定已经出现过了,所以前缀和当前要比较的节点 (p) 均出现过。
可以发现,当前加入的字符串 (u) 除了最后一个字符之外其他都与前缀 (u-1) 完全一致,所以我们先暴力比较 (u) 与 (p) 的最后一个字符,如果相同意味着这个 (u-1) 和 (p-1) 的字典顺序决定了 (u) 与 (p) 的字典顺序。但是直接这样比较还是 (O(log n))。
考虑如果我们维护出了所有前缀的 (rank),那么显然 (rank) 的相对顺序就对应最后的结果。但是我们不能直接维护rank,这样会平白多出一个 (log n)。考虑我们只需要知道 (rank) 的相对顺序即可。考虑利用平衡树的性质,每个点取一个权值 (v_i=frac{L+R} 2),然后根据 (v_i) 将区间分为两段递归处理。可以发现,这样满足 (v_{ls_u}< v_u< v_{rs_u})。
这样建树的时间复杂度 (O(|S|log |S|))。
考虑这题维护的东西:出现次数。
这个就很好办了。考虑差分,比如要查 ( exttt{AB}),我们就查字典序在 ( exttt{AA[}) 和 ( exttt{AB[}) 之间的字符。(( exttt{[}) 的字典序大于所有大写字母)。具体来说,由于后缀平衡树中不存在字符 ( exttt{[}) ,我们可以直接用字典序小于 ( exttt{AB[}) 的数量减去小于 ( exttt{AA[}) 的数量。
由于这里需要保证相对顺序,所以我们必须使用重量平衡树,这里可以是替罪羊树(当然好像treap也行,splay应该会被卡)。
总时间复杂度 (O(|S|log |S|)),空间复杂度 (O(|S|))。
特别,这里需要支持删除。但是我们不能按照普通的替罪羊树那样打标记,因为这个点会被替换掉。
所以我们直接暴力删除,按照BST的方式找到左子树中最右端的点,然后拼接上去。由于树深是 (O(log n)),所以这样复杂度也是对的。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 2000010
#define db double
#define alp 0.72
#define MAXD 1e16
using namespace std;
char str[N],s[N];
db v[N];
int ch[N][2],siz[N];
int swp[N],stot,rt;
void upd(int u){siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1;}
void dfs(int u)
{
if(!u) return;
dfs(ch[u][0]),swp[++stot]=u;dfs(ch[u][1]);
ch[u][0]=ch[u][1]=0;
}
void build(int &u,int l,int r,db lf=0,db rf=MAXD)
{
if(l>r) return;
int mid=(l+r)>>1;db mf=(lf+rf)/2;
u=swp[mid];
v[u]=mf;
build(ch[u][0],l,mid-1,lf,mf),build(ch[u][1],mid+1,r,mf,rf);
upd(u);
}
void reb(int &u,db lf,db rf)
{
if(max(siz[ch[u][0]],siz[ch[u][1]])<siz[u]*alp) return;
stot=0;dfs(u);
build(u,1,stot,lf,rf);
}
int cmp(int x,int y){return s[x]==s[y]?v[x-1]<v[y-1]:s[x]<s[y];}
void insert(int &u,int k,db lf=0,db rf=MAXD)
{
if(!u){siz[u=k]=1;v[u]=(lf+rf)/2;ch[u][0]=ch[u][1]=0;return;}
if(cmp(k,u)) insert(ch[u][0],k,lf,v[u]);
else insert(ch[u][1],k,v[u],rf);
upd(u),reb(u,lf,rf);
}
void erase(int &u,int k)
{
if(u==k)
{
if(!ch[u][0] || !ch[u][1]){u=ch[u][0]|ch[u][1];return;}
int p=ch[u][0],las=u;
for(;ch[p][1];las=p,p=ch[p][1]) siz[p]--;
if(las==u) ch[p][1]=ch[u][1];
else ch[p][0]=ch[u][0],ch[p][1]=ch[u][1],ch[las][1]=0;
u=p;
upd(u);
return;
}
if(cmp(k,u)) erase(ch[u][0],k);
else erase(ch[u][1],k);
upd(u);
}
bool cmp_s(int u){for(int i=1;str[i];i++,u=u-!!u) if(str[i]!=s[u]) return str[i]<s[u];return false;}
int answer(int u)
{
if(!u) return 0;
if(cmp_s(u)) return answer(ch[u][0]);
else return answer(ch[u][1])+siz[ch[u][0]]+1;
}
void get_c(char s[],int mask)
{
int len=strlen(s);
for(int i=0;i<len;i++)
{
mask=(mask*131+i)%len;
char t=s[i];
s[i]=s[mask];
s[mask]=t;
}
}
char opt[7];
int main()
{
int n,m,k,las=0;
scanf("%d%s",&m,s+1);n=strlen(s+1);
for(int i=1;i<=n;i++)
insert(rt,i);
for(int i=1;i<=m;i++)
{
scanf("%s",opt);
if(opt[0]=='D'){scanf("%d",&k);while(k --> 0) erase(rt,n),n--;continue;}
scanf("%s",str+1);
get_c(str+1,las);
int l=strlen(str+1);
if(opt[0]=='A') for(int j=1;j<=l;j++) s[++n]=str[j],insert(rt,n);
else if(opt[0]=='Q')
{
reverse(str+1,str+l+1);
str[l+1]='Z'+1,str[l+2]=' ';
int ans=answer(rt);
str[l]--;
ans-=answer(rt);
printf("%d
",ans);
las^=ans;
}
}
return 0;
}
//压行?不存在的。
bzoj3682 Phorni
题意
维护一个 (n) 个元素的序列,每个元素是字符串 (s) 的一个后缀。
支持:在 (s) 开头加一个字符,改变序列 (a_i) 表示的后缀位置,求序列 ([l,r]) 元素中字典序最小的编号(如有相同输出编号最小)。强制在线。
第一眼:这不是**题吗?直接SA就好了。
然后发现:强制在线。
SA没了,直接后缀平衡树就好了。本来后缀平衡树也被叫做“动态后缀数组”。
首先将串反过来,就变成了在末尾加字符,查询前缀。
用平衡树维护各前缀之间的相对位置,然后用一颗线段树处理 (n) 个前缀的相对权值即可。
注意这里这个相对权值的精度要求可能比较高,直接比较可能把两个相同的前缀看做不同的。需要特判该情况。
复杂度 (O(nlog n))。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 2000010
#define db double
#define alp 0.74
#define MAXD 1e16
using namespace std;
char s[N];
db v[N];
int siz[N],ch[N][2];
void upd(int u){siz[u]=siz[ch[u][0]]+siz[ch[u][1]]+1;}
int ton[N],td;
void dfs(int u)
{
if(!u) return;
dfs(ch[u][0]),ton[++td]=u,dfs(ch[u][1]);
ch[u][0]=ch[u][1]=0;
}
void bld(int &u,int l,int r,db lf=0,db rf=MAXD)
{
if(l>r) return;
int mid=(l+r)>>1;
u=ton[mid];v[u]=(lf+rf)/2;
bld(ch[u][0],l,mid-1,lf,v[u]),bld(ch[u][1],mid+1,r,v[u],rf);upd(u);
}
void reb(int &u,db lf,db rf)
{
if(max(siz[ch[u][0]],siz[ch[u][1]])<=siz[u]*alp) return;
td=0;dfs(u);
bld(u,1,td,lf,rf);
}
int cmp(int x,int y){return s[x]==s[y]?v[x-1]<v[y-1]:s[x]<s[y];}
void insert(int &u,int k,db lf=0,db rf=MAXD)
{
if(!u){siz[u=k]=1;v[u]=(lf+rf)/2;ch[u][0]=ch[u][1]=0;return ;}
if(cmp(k,u)) insert(ch[u][0],k,lf,v[u]);
else insert(ch[u][1],k,v[u],rf);
upd(u);reb(u,lf,rf);
}
int val[N<<2];
int p[N];
void update(int u){val[u]=p[val[u<<1]]==p[val[u<<1|1]]?min(val[u<<1],val[u<<1|1]):(v[p[val[u<<1]]]<v[p[val[u<<1|1]]]?val[u<<1]:val[u<<1|1]);}
void build(int u,int l,int r)
{
if(l==r){val[u]=l;return;}
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
update(u);
}
void change(int u,int l,int r,int k)
{
if(l==r) return;
int mid=(l+r)>>1;
if(k<=mid) change(u<<1,l,mid,k);
else change(u<<1|1,mid+1,r,k);
update(u);
}
int qry(int u,int l,int r,int L,int R)
{
if(L<=l && r<=R) return val[u];
int mid=(l+r)>>1,w1=0,w2=0;
if(L<=mid) w1=qry(u<<1,l,mid,L,R);
if(R>mid) w2=qry(u<<1|1,mid+1,r,L,R);
if(!w1 || !w2) return w1+w2;
return p[w1]==p[w2]?min(w1,w2):(v[p[w1]]<v[p[w2]]?w1:w2);
}
char opt[3];
int main()
{
freopen("1.in","r",stdin);
freopen("1.ans","w",stdout);
int n,m,l,T,rt=0;
scanf("%d%d%d%d",&n,&m,&l,&T);
scanf("%s",s+1);
reverse(s+1,s+l+1);
for(int i=1;i<=l;i++) insert(rt,i);
for(int i=1;i<=n;i++) scanf("%d",&p[i]);
build(1,1,n);
int las=0;
for(int i=1;i<=m;i++)
{
int v,w;
scanf("%s%d",opt+1,&v);
if(opt[1]=='I')
{
v^=las*T;
s[++l]='a'+v;
insert(rt,l);
}
else if(opt[1]=='C')
{
scanf("%d",&w);
p[v]=w;
change(1,1,n,v);
}
else
{
scanf("%d",&w);
printf("%d
",las=qry(1,1,n,v,w));
}
}
return 0;
}
3.SA-IS
见这里
4.Lyndon 分解
首先定义 ( ext{Lyndon Word}) :所有后缀中字典序最小的。
比如 (ababb) 就是一个 ( ext{Lyndon Word})。
Lyndon 分解具体来说,你需要把一个字符串分解为若干个 ( ext{Lyndon Word}),并且字典序单调不增。
这里先给出算法:
我们枚举当前串左端点 (i),处理循环串的左右端点 ([l,r])。每次比较 (r+1) 和 (l) 的字典序,如果 (r+1) 大,那么我们当前的分割还是可行的。否则就是不可行。我们直接把 (l) 移回当前串的左端点即可。复杂度 ((n))。
考虑为什么是对的。由于我比较菜,这里只能“显然法”证明。
假如 (u,v) 是 ( ext{Lyndon Word}),而 (u<v),那么 (uv) 也是一个 ( ext{Lyndon Word})。否则 (u|v) 是一个 ( ext{Lyndon}) 分解。(结论看起来很显然)
所以根据上面这个结论,我们考虑一个性质:对于某个串 ([l,r]),([l,r]) 是 ( ext{Lyndon Word}) 且 ([1,l-1]) 的 ( ext{Lyndon}) 分解已经完成。假如 (s_{r+1}<s_{l}),那么在 (r) 处分解一定是一个 ( ext{Lyndon Word})。我们可以直接切开重新处理。
那么具体来说我们就要分类讨论。当前左端点 (i)(即 ([1,i-1]) 的 ( ext{Lyndon}) 分解已经完成),同时周期串的开头末尾分别为 ([l,r])。
如果 (s_{l}=s_{r+1}),那么我们可以认为这里存在循环的可能,所以 (r+1) 我们需要保留。同时我们让 (l+1),即现在周期串是 ([l+1,r+1])。
比如当前串是 ( exttt{aabaaba}),那么我们暂定的分解是 ( exttt{aab|aab|a}),而当前周期串为 ( exttt{aba})。
如果 (s_{l}<s_{r+1}),那么后面的串一定会把 ([l,r]) 所有保留的串都合并掉,所以令 (l=i)。
如果 (s_{l}>s_{r+1}),那么至此就是一个合法的 ( ext{Lyndon}) 分解,前面所有的暂定分解都成立,所以直接按暂定分解处理即可。
#include<cstdio>
#include<algorithm>
#include<cstring>
#define N 5000010
using namespace std;
char s[N];
int p[N],t;
int main()
{
scanf("%s",s+1);
int n=strlen(s+1);
for(int i=1,j=1,k=2;i<=n;j=i,k=i+1)
{
for(;k<=n && s[j]<=s[k];k++)
{
if(s[j]<s[k]) j=i;
else j++;
}
for(;i<=j;i+=k-j) p[++t]=i+(k-j)-1;
}
int ans=0;
for(int i=1;i<=t;i++) ans^=p[i];
printf("%d
",ans);
return 0;
}
5.Runs
由于我比较菜,很多地方有点模棱两可甚至需要“感性证明”,如果想要严谨证明可以参考WC2019课件
首先先要给出“循环和循环”的标准定义:
定义一个整数 (p) 是一个字符串 (s) 的 period 当且仅当 (forall iin[1,|s|-p],s_{i}=s_{i+p})。
即某一个字符串不断循环得到 (s)。这里并不一定要整循环,比如 (3) 就是 ( exttt{abbabba}) 的一个 period。
接下来给出 Run 的标准定义:
定义三元组 ((i,j,p)) 是一个 Run 当且仅当 (s_{j+1} eq s_{j-p+1},s_{i-1} eq s_{i+p-1}) 且 (p) 是 ([i,j]) 最小的出现至少两次的循环节。
通俗来讲就是对于子串 (s[i:j]) 的最小的 period (p(pleq frac{j-i+1}{2})) 且不能往左往右延伸。显然对于任意 ((i,j)) 其最小 period (p) 唯一。
比如 ( exttt{ababababba}) 中:
((1,8,2)) 是一个 Run。
((3,8,2)) 不是,因为这个这个周期串可以继续向左延伸。
((1,8,4)) 不是,因为 (p=4) 不是 (s[1:8]) 的最小 period。
((6,10,3)) 不是,因为 (p=3) 没有出现至少两次,即 (p>frac{10-6+1}{2})。
接下来考虑一些 Run 的性质。首先给出一些关于 Run 的定义:
定义 (Runs(s)) 是字符串 (s) 的所有 (Run) 的集合。
定义一个 Run ((l,r,p)) 的指数是 (frac {r-l+1} p),即 period (p) 在 (s[l:r]) 中的出现次数。记作 (e_{(l,r,p)})。
定义两种偏序关系 (<_0,<_1)。由于只要满足 (a<_0 b ightarrow b<_1 a),所以一般这里 (<_0) 表示“字典序较小”,(<_1) 表示“字典序较大”。一般我们认为空字符字典序最小,即 (varnothing<_0 exttt{a} , exttt{a}<_0 exttt{aa})
定义 (s[lambda_l:lambda_r]) 是 ((i,j,p)) 在 (<) 上的 Lyndon Root,当且仅当 ([lambda_l,lambda_r]subseteq [i,j]) 且 (s[lambda_l:lambda_r]) 是一个 Lyndon Word。
定义 (B(r)) 表示一个 Run (r) 的所有除去开头的 Lyndon Root 的集合。除去开头的意思是集合中所有元素 (s[i:j]) 要求 (i eq r_l)。显然有 (|B(r)|geq lfloor e_r-1 floorgeq 1)。
定义 (Beg(B(r))) 表示 (B(r)) 的所有起始端点组成的集合。
定义 (l_{ell}(i)) 表示所有从 (i) 开始的 Lyndon Word 中最长的那个。即 (l_{ell}(i)=[i,j],j=max{j'|s[i:j'] ext{是一个 Lyndon Word}})。
为了方便,我们额外定义一个 Runs ((i,j,p)) 是 (ell(ellin{0,1})) 型的,当且仅当 (s_{j-p+1}<_{ell}s_{j+1})。
有了以上定义,我们就可以推出一些定理了:
引理1:对于字符串 (s=t^kt[:p]),其中 (t) 是一个 Lyndon Word,(t[:p]) 表示 (t) 的一个前缀且在这里允许为空。现在在其后加入字符 (a) 且 (a eq t_{p+1})。如果 (a<t_{p+1}) 那么 (s=t^kt[:p]a) 是任意前缀中最长的 Lyndon Word。
这个其实就是上面 Lyndon 分解所用的结论。
推论1:对于 (ell) 型 Run ((i,j,p)),其所有定义在 (<_{ell}) 上的 Lyndon Root (lambda=[l_{lambda},r_{lambda}]) 都有 (lambda=l_{ell}(l_{lambda})),即其所有 Lyndon Root 都是相同起点中最长的 Lyndon Word。
推论2:对于任意 Run (r(i,j,p)) ,其所有的 Lyndon Root 左端点不重复,即有 (|Beg(B(r))|=|B(r)|)。
引理2:对于任意位置 (pin[1,|s|]),有且仅有一个 (ellin{0,1}) 满足 (l_{ell}(i)=s[i:i]),这里默认字符串末尾存在一个空字符。
证明:考虑找到向右第一个与当前字符不同的位置 (p')(包括最后的空字符)。即 (min{p'|p'>i,s_{p'} eq s_p})。令 (ell) 满足 (s_{p'}<_{ell}s_p),由引理1显然有 (l_{ell}=[i,i]),(l_{!ell} eq[i,i])。(此处 WC2019 的课件貌似有误?课件好像取的是 (max))。
推论3:(forall r,r ext{is a Run},r eq r'),有 (Beg(B(r)) cap Beg(B(r'))=varnothing)。
具体证明大概就是根据引理 2 用反证推出如果存在交集,取交集的位置就能推出周期性,与 Run 的定义不符。
结论1:任意位置 (p) 最多存在于一个 (Beg(B(r))) 中。由一个 Lyndon Root 能唯一确定一个 Run (r)。由于 (forall r,1 otin Beg(B(r))) 故 (sum|Beg(B(r))|leq n-1),故有 (|Runs(s)|<|s|)。
结论2:由于 (Beg(B(r))>e_r-1)。由结论 1 可推得 (sum{e_r}leq 3n-3)。
接下来考虑如何快速求的 (Runs(s))。
考虑我们先求出 (l_{ell}(i)),即以 (i) 开头最长的 Lyndon Word。
考虑 Lyndon Word 的性质。对于两个定义于 (ell) 的 Lyndon Word (u,v(u<_{ell}v)),有 (uv) 也是定义于 (ell) 的 Lyndon Word。
那么我们用一个单调栈维护当前存在的 Lyndon Word,然后从后往前加字符,每次比较与栈顶元素的大小,如果能合并就合并。可以证明这样栈顶得到的串就是最长的 Lyndon Word。
获得了所有 (l_{ell}(i)) 后,我们令 (p=l_{ell}(i)-i+1),观察其向左向右能延伸到的最远的地方 ([l',r'])。对于三元组 ((l',r',p)),我们只需要判断其是否满足 (pleq frac{r'-l'+1} 2) 即可。
实现细节:一种比较 simple 的方式是用 Hash+二分处理两个串的 lcp/lcs。对于子串大小比较直接比较 lcp 的后一位即可。判断延伸到的最远的位置 ([l',r']),可以发现对于 (l') 一定有 (s[l':i]=s[l'+p-1:s+p-1]),所以直接取 (s[:i],s[:l_{ell}(i)]) 的 lcp 即可。(r') 同理。这样总复杂度是 (O(nlog n))。
上述复杂度瓶颈在于求 lcp。用 SA-IS 套 (O(n)-O(1)) ST表可以优化到 (O(n))。
至此就完成了 Runs 的求解。
LOJ173 Runs
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#define N 1000010
#define ull unsigned long long
#define B 2333
using namespace std;
char str[N];
int s[N],n;
ull h[N],bs[N];
ull get(int l,int r){return h[r]-h[l-1]*bs[r-l+1];}
int lcp(int x,int y)
{
int l=1,r=n-max(x,y)+1,res=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(get(x,x+mid-1)==get(y,y+mid-1)) l=mid+1,res=mid;
else r=mid-1;
}
return res;
}
int lcs(int x,int y)
{
int l=1,r=min(x,y),res=0;
while(l<=r)
{
int mid=(l+r)>>1;
if(get(x-mid+1,x)==get(y-mid+1,y)) l=mid+1,res=mid;
else r=mid-1;
}
return res;
}
bool cmp(int l1,int r1,int l2,int r2)//s[l1:r1]<s[l2:r2]
{
int l=lcp(l1,l2);
if(l>min(r1-l1,r2-l2)) return r1-l1<r2-l2;
return s[l1+l]<s[l2+l];
}
struct runs{
int i,j,p;
runs(int i=0,int j=0,int p=0):i(i),j(j),p(p){}
bool operator ==(const runs a)const{return i==a.i && j==a.j && p==a.p;}
bool operator <(const runs a)const{return i==a.i?j<a.j:i<a.i;}
};
vector<runs>ans;
int st[N],t,run[N];
void lyndon()
{
t=0;
for(int i=n;i;i--)
{
st[++t]=i;
for(;t>1 && cmp(i,st[t],st[t]+1,st[t-1]);t--);
run[i]=st[t];
}
}
void init()
{
bs[0]=1;
for(int i=1;i<=n;i++) h[i]=h[i-1]*B+s[i],bs[i]=bs[i-1]*B;
}
void get_runs()
{
for(int i=1;i<=n;i++)
{
int l1=i,r1=run[i],l2=l1-lcs(l1-1,r1),r2=r1+lcp(l1,r1+1);
if(r2-l2+1>=(r1-l1+1)*2) ans.push_back(runs(l2,r2,r1-l1+1));
}
}
int main()
{
scanf("%s",str+1);
n=strlen(str+1);
for(int i=1;i<=n;i++) s[i]=str[i]-'a'+1;
init();
lyndon();
get_runs();
for(int i=1;i<=n;i++) s[i]=27-s[i];
lyndon();
get_runs();
sort(ans.begin(),ans.end());
ans.erase(unique(ans.begin(),ans.end()),ans.end());
printf("%d
",(int)ans.size());
for(auto u:ans) printf("%d %d %d
",u.i,u.j,u.p);
return 0;
}
6.KMP自动机与 border tree
咕咕咕。
7.隐式后缀树
暂时移到这里,密码可以猜一猜或者线下问我。