后缀数组全家桶-从哈希乱搞到入门
wjyppm1403 · · 算法·理论
可能更好阅读体验
0. 前言
后缀数组是信息学竞赛中解决字符串匹配的一大利器,其思想和实现非常简单。虽然倍增加排序的思想很简单,但是它的拓展
以下应用魏老师的一句话:
几乎所有字符串算法都存在一个共性:基于所求信息的特殊性质与已经求出的信息,使用增量法与势能分析求得所有信息。这体现了动态规划思想。—— Alex_Wei
希望读者也能好好利用这句话来理解字符串算法。
本文章包含后缀数组入门,以及应用,以及技巧及其好题选讲大礼包!
但是作为本蒟蒻第一个写的算法全家桶,为了考虑到读者感受,写了一大堆没用的废话导致文章及其的长 ( •́ὤ•̀),本文章共 2.1 万字,感谢管理员付出时间来进行审核。
一些基本约定:
- 本文章默认字符串下表从
1 开始。 - 我们用打字机字体表示字符串的内容,如:
s=\texttt{wjyppm1403} 。 - 拼接:
s+t 表示将t 拼接s 后。 - 字符集:即构成字符串中字符的集合。
- 空串:不含任何字符的字符串称为空串。
- 子串:在
s 开头或末尾删去若干字符得到的字符串称作为s 的子串,s 本身和空串也是s 的子串。我们定义s[l,r] 表示l \to r 上所有字符链接而成子串。 - 匹配:称
t 匹配s 当且仅当t 在s 中出现。 - 字符串长度:我们用
|s| 来表示s 的长度。
前后缀:
- 前缀:在
s 末尾删除若干字符得到的字符串称作s 的前缀,记为pre 。 - 后缀:在
s 开头删除若干字符得到的字符串称作s 的后缀,记为suf 。 - 最长公共前缀:
\operatorname{LCP}(s,t) ,表示s,t 的最长公共前缀,即最长的u 使得u 为s,t 的前缀。最长公共后缀同理,我们称为\operatorname{LCS}(s,t) 。LCP 的长度格式为:|\operatorname{LCP}(s,t)| 。 - 字典序:定义空字符小于任何字符。称
s 的 字典序 小于t 当且仅当去掉\operatorname{LCP}(s,t) 后,s 的第一个字符小于t 的第一个字符。等价于以第i 个字符作为第i 关键字比较。
我们先从概念讲起。
1. 后缀树与后缀数组
1.1 朴素后缀树
一个字符串的后缀是指从某个位置开始到结尾的一个子串,即
而后缀树,就是把字符串所有后缀子串通过字典树的方法建立的一颗树。如下图:
若要在字符串上找一个子串是否出现,如
但是,问题在于,你全部显式的建出来那你空间不就炸掉了吗。我们思考这样的朴素后缀树的问题在哪里,这种方法的本质就是把一个长度为
1.2 后缀数组
由于不方便直接对后缀树进行构造,我们利用后缀数组这种简单的方法来替代它,我们定义:
那么将上面后缀树的例子,我们用后缀数组来表示一下:
| 后缀 |
下表 |
字典序 | 后缀数组 |
下表 |
|
|---|---|---|---|---|---|
| 1 | 6 | 1 | |||
| 2 | 4 | 2 | |||
| 3 | 2 | 3 | |||
| 4 | 7 | 4 | |||
| 5 | 5 | 5 | |||
| 6 | 3 | 6 | |||
| 7 | 8 | 7 | |||
| 8 | 1 | 8 |
很明显,后缀数组的下表对应的就是后缀子串的字典顺序,记录的子串的有序排列。例如
上面是一个例子,下面是 OI-Wiki 的例子:
我们定义另外一个数组
那么有
那么现在问题在于如何给这些后缀通过排序求出排名。有一个显而易见的想法是从最后一位开始枚举后缀,然后每次存下当前枚举到的字符串,最后排序并输出就 OK 辣!
但是这样显然复杂度起步就是
但是我们考虑,我们每一次都是一位一位比较的,我们能不能多位进行比较呢。这个时候我们就要用到倍增的思想:
首先对字符串
第二次,我们根据倍增向后移
第三次,移
第四次:
唉?我们好像倍增完了,这样的话我们就求得了所有的
再看一遍整体的过程:
但是这样显然过不去我们可爱的 P3809,因为它要求
void getsa(){
for(int i=1;i<=n;i++){
x[i]=s[i];// x 就是
c[x[i]]++; //桶排
}
for(int i=1;i<=m;i++){
c[i]+=c[i-1];
//做一个前缀和。这样字典序越大,所对应的的 c 越大。
}
for(int i=n;i>=1;i--){
sa[c[x[i]]--]=i;
//为何要减呢?若c[x[i]]>1表示有重复的,要保证排序不一样。
}
for(int len=1;len<=n;len<<=1){// 倍增
int num=0;
for(int i=n-len+1;i<=n;i++){
y[++num]=i;
//n-len+1已经排序完因为它们再倍增就倍增到空气啦。我们直接存在 y 中。
}
for(int i=1;i<=n;i++){
if(sa[i]>len) y[++num]=sa[i]-len;
// 若 i 可作其他位置的第二关键字,我们把他放在对应的第一关键字
}
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) c[x[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--){
sa[c[x[y[i]]]--]=y[i];
y[i]=0;
// 注意, 倒序枚举保证计数排序的稳定性. 基数排序的正确性基于内层计数排序的稳定性.
}
//和以前一样更新sa,但是排序是y[i]
swap(x,y);
num=1;
x[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+len]==y[sa[i-1]+len]) x[sa[i]]=num;
else x[sa[i]]=++num;
}
//以上都是在更新 x
if(num==n) break;// n 个排名互不相同, 排序完成.
m=num;
}
}
关于成熟的模板我们之后再说。对于上面的内容其实都比较还算简单,但是 SA 真正的神就在于 Height 数组,我们这里简称为
1.3 Height 数组
回顾 LCP 的定义:
最长公共前缀:
\operatorname{LCP}(s,t) ,表示s,t 的最长公共前缀,即最长的u 使得u 为s,t 的前缀。
而 Height 数组的定义就是:
绝大多数 SA 的应用都需要
那么怎么求?有一个朴素的想法就是哈希加二分,这个想必读者在做哈希题经常会见到这种操作。不然为什么我们标题叫哈希乱搞到入门,我们有一个结论:
若
rk_{i}<rk_{j}<rk_{k} ,则|\operatorname{LCP}(i,j)| 和|\operatorname{LCP}(j,k)| ,均不小于|\operatorname{LCP}(i,k)| 。
证明?设
若我们希望不要
假设
令
我们考虑,排名为
我们这里引用 Alex_Wei 的图:
通过这个性质,我们可以通过类似于双指针的性质来暴力求解
for(int i=1;i<=len;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=len;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=len&&j+k<=len&&s[i+k]==s[j+k]) k++;
ht[rk[i]]=ST[0][rk[i]]=k;
}
下面是一个完整的模板,但是里面有一些函数还没有讲解,在应用中我们会逐个讲解:
namespace SA{
int len,sa[MN],x[MN],y[MN],rk[MN],c[MN],ht[MN],ST[30][MN];
// 接受 string 和 vector_int 输入,其他输入不保证正确性
// ST表需要手动初始化调用initst函数
template<typename vct>
void getsa(vct &s){
int m=400000;
len=s.size();
s.insert(s.begin(),' ');
for(int i=1;i<=len;i++){
x[i]=s[i];
++c[x[i]];
}
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=len;i>=1;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=len;k<<=1){
int num=0;
for(int i=len-k+1;i<=len;i++) y[++num]=i;
for(int i=1;i<=len;i++){
if(sa[i]>k) y[++num]=sa[i]-k;
}
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=len;i++) c[x[i]]++;
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=len;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
num=1,x[sa[1]]=1;
for(int i=2;i<=len;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num;
else x[sa[i]]=++num;
}
if(num==len) break;
m=num;
}
for(int i=1;i<=len;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=len;i++){
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=len&&j+k<=len&&s[i+k]==s[j+k]) k++;
ht[rk[i]]=ST[0][rk[i]]=k;
}
}
// ST表初始化
void initst(){
for(int i=1;i<30;i++){
for(int j=1;j+(1<<i)-1<=len;j++){
ST[i][j]=min(ST[i-1][j],ST[i-1][j+(1<<(i-1))]);
}
}
}
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
// 手动查询 ST 表
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(ST[d][l],ST[d][r-(1<<d)+1]);
}
}
2. 后缀数组的应用
后缀数组有着许许多多的应用,但是由于对应的例题过于杂且用到的芝士较多,我们的顺序是先讲解技巧,先认识,让后我们在最后一部分的习题环节进行练习。有一些应用是对应少见的例题,所以这里会直接进行讲解而不会放在习题。
2.1 寻找最小的循环移动位置
循环移动位置实际上就是将字符串排为一个环,让后旋转这个环,我们先要断环成链将给定字符串复制一遍放在后面,这样就变为了后缀排序问题。
JSOI2007字符加密
我们发现,循环移动位置实际上就是将字符串排为一个环,让后旋转这个环,我们先要断环成链。把给定字符串复制一遍放在后面。让后你发现题目其实就是把这个改变后的字符串进行后缀排序,我们根据后缀排序的数组让后输出对应最后一位就可以了:
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=8e5+15;
int x[MN],n,m,y[MN],c[MN],sa[MN];
string s;
void getsa(){
for(int i=1;i<=n;i++){
x[i]=s[i];
c[x[i]]++;
}
for(int i=2;i<=m;i++){
c[i]+=c[i-1];
}
for(int i=n;i>=1;i--){
sa[c[x[i]]--]=i;
}
for(int len=1;len<=n;len<<=1){
int num=0;
for(int i=n-len+1;i<=n;i++){
y[++num]=i;
}
for(int i=1;i<=n;i++){
if(sa[i]>len){
y[++num]=sa[i]-len;
}
}
for(int i=1;i<=m;i++){
c[i]=0;
}
for(int i=1;i<=n;i++){
c[x[i]]++;
}
for(int i=2;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--){
sa[c[x[y[i]]]--]=y[i];
y[i]=0;
}
swap(x,y);
num=1;
x[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+len]==y[sa[i-1]+len]){
x[sa[i]]=num;
}else x[sa[i]]=++num;
}
}
}
int main(){
cin>>s;
n=s.length()*2;
m=300000;
s=' '+s+s;
getsa();
for(int i=1;i<=n;i++){
if(sa[i]<=n/2) cout<<s[sa[i]+n/2-1];
}
return 0;
}
2.2 在字符串中寻找最长公共子串
细节我给同学讲解 Trie 习题后教练考我这个问题,还好我即会后缀数组也会哈希做法 www。
同学问我哈希怎么做,显然最长公共子串的长度满足可二分性,考虑二分最长公共子串长度
如何用 SA 做呢?现在我们要求在主串
这一部分主要考察二分的操作使用,在下面的例题中我们也会详细的进行讲解。
2.3 从字符串首尾取字符最小化字典序
给你一个字符串,每次从首或尾取一个字符组成字符串,问所有能够组成的字符串中字典序最小的一个。
例题:「USACO07DEC」Best Cow Line。
一个暴力的想法就是
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=2e6+15;
int x[MN],y[MN],rk[MN],sa[MN],c[MN],n,sn,m;
string s,revs;
void getsa(){
for(int i=1;i<=n;i++){
x[i]=s[i];
c[x[i]]++;
}
for(int i=1;i<=m;i++){
c[i]+=c[i-1];
}
for(int i=n;i>=1;i--){
sa[c[x[i]]--]=i;
}
for(int len=1;len<=n;len<<=1){
int num=0;
for(int i=n-len+1;i<=n;i++){
y[++num]=i;
}
for(int i=1;i<=n;i++){
if(sa[i]>len){
y[++num]=sa[i]-len;
}
}
memset(c,0,sizeof(c));
for(int i=1;i<=n;i++){
c[x[i]]++;
}
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--){
sa[c[x[y[i]]]--]=y[i];
y[i]=0;
}
swap(x,y);
num=1;
x[sa[1]]=1;
for(int i=2;i<=n;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+len]==y[sa[i-1]+len]){
x[sa[i]]=num;
}else x[sa[i]]=++num;
}
}
for(int i=1;i<=n;i++){
rk[sa[i]]=i;
}
}
int main(){
cin>>n;
m=5e5;
for(int i=1;i<=n;i++){
char awa;
cin>>awa;
s.push_back(awa);
}
for(int i=s.length()-1;i>=0;i--){
revs.push_back(s[i]);
}
sn=n;
// cout<<s<<'\n'<<revs<<'\n';
s=' '+s+(char)0+revs;
n=(n*2+1);
getsa();
int tot=0;
for(int l=1,r=sn;l<=r;){
if(rk[l]<rk[n+1-r]){
cout<<s[l++];
}else cout<<s[r--];
if((++tot)%80==0) cout<<'\n';
}
return 0;
}
2.4 与贪心与 DP 的结合
对于后缀数组,在贪心和 DP 中一般不作为主角出现,多用于字符串加速匹配的问题或者利用其性质进行求解。
2.5 与线段树等一类数据结构结合
某些题目让你求满足条件的前若干个数,而这些数又在后缀排序中的一个区间内。这时我们可以用归并排序的性质来合并两个结点的信息,利用线段树维护和查询区间答案。
同时后缀数组会根据题目的不同结合一系列数据结构算法,如莫队,扫描线等。在习题部分我们会单独开讲。
3. Height 数组的应用
3.1 任意两个后缀的 LCP
有了
结论如下:
若
rk_{i}<rk_{j} ,则|\operatorname{LCP}(i,j)|=\min\limits_{p=rk_{i}+1}^{rk_{j}}ht_p 。
感性理解以下,如果
严格证明可以参考[2004] 后缀数组 by. 许智磊。那么通过这样,求两子串最长公共前缀就转化为了 RMQ 问题,这也就是对应了我们模板中的 ST 表,实现如下:
// ST表初始化
void initst(){
for(int i=1;i<30;i++){
for(int j=1;j+(1<<i)-1<=len;j++){
ST[i][j]=min(ST[i-1][j],ST[i-1][j+(1<<(i-1))]);
}
}
}
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
我们这里引用 Alex_wei 的图:
上图就是对
但是,如果我们将整张图逆着旋转
我们得到了一个矩形柱状图!
没错,这个玩意我们还是可以和单调栈结合起来一起考的!众所周知,单调栈可以求出柱状图中面积最大的矩形。
例如我们求所有后缀两两 LCP 长度之和,考虑按排名顺序加入所有后缀并实时维护
3.2 求本质不同子串数
可以用
上面的做法我们求得了
设
其中后面能够 ST 表预处理后
3.3 多个串的最长公共子串
给出
首先我们先把这个字符串给拼接起来,格式入
让后我们对
容易发现
3.4 结合并查集
某些题目求解时要求你将后缀数组划分成若干个连续 LCP 长度大于等于某一值的段,我们可以考虑根据
对
3.5 连续的若干个相同子串
如果题目中出现一些构造字符串循环构成的问题,我们可以不妨考虑枚举这个循环的长度
4. 例题
4.1 并查集技巧
P2852 [USACO06DEC] Milk Patterns G
呃其实就是板子题,这个真没有什么好说的,考虑从从大到小添加每个
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int n,k;
string a;
multiset<int> tt;
namespace SA{
// 省略
}using namespace SA;
int main(){
cin>>n>>k;
k--;
for(int i=1;i<=n;i++){
int c;
cin>>c;
a.push_back(c);
}
getsa(a);
int ans=0;
for(int i=1;i<=n;i++){
tt.insert(ht[i]);
if(i>k) tt.erase(tt.find(ht[i-k]));
ans=max(ans,*tt.begin());
}
cout<<ans;
return 0;
}
P2178 [NOI2015] 品酒大会
若
我们考虑从大到小处理
#include<bits/stdc++.h>
#define ll long long
using namespace std;
constexpr int MN=6e5+15;
int n,m,x[MN],y[MN],cnt[MN],pre[MN],sa[MN],rk[MN],h[MN],pos[MN];
ll mx[MN],mn[MN],ans1[MN],ans2[MN],ans[MN],a[MN],siz[MN];
string s;
namespace SA{
// 省略
}using namespace SA;
void geth(){
for(int i=1,k=0;i<=n;i++){
if(!rk[i]) continue;
if(k) k--;
while(s[i+k]==s[sa[rk[i]-1]+k]) k++;
h[rk[i]]=k;
}
}
void init(){
memset(ans2,128,sizeof(ans2));
for(int i=1;i<=n;i++){
pre[i]=i;
pos[i]=i;
mx[i]=mn[i]=a[i];
ans[i]=-1e18;
siz[i]=1;
}
}
int root(int x){
if(pre[x]==x) return pre[x];
else return pre[x]=root(pre[x]);
}
void merge(int x,int y,int len){
x=root(x),y=root(y);
pre[y]=x;
ans1[len]+=(ll)siz[x]*siz[y];
siz[x]+=siz[y];
ans[x]=max({ans[x],ans[y],mx[x]*mx[y],mx[x]*mn[y],mn[x]*mx[y],mn[x]*mn[y]});
mx[x]=max(mx[x],mx[y]);
mn[x]=min(mn[x],mn[y]);
ans2[len]=max(ans2[len],ans[x]);
}
bool cmp(int x,int y){
return h[x]>h[y];
}
int main(){
cin>>n;
m=3e5;
cin>>s;
s=' '+s;
for(int i=1;i<=n;i++) cin>>a[i];
getsa();
geth();
init();
sort(pos+2,pos+1+n,cmp);
// for(int i=1;i<=n+1;i++){
// cout<<sa[i]<<" ";
// }
// cout<<'\n';
for(int i=2;i<=n;i++){
merge(sa[pos[i]],sa[pos[i]-1],h[pos[i]]);
}
for(int i=n;i>=0;i--) ans1[i]+=ans1[i+1];
for(int i=n;i>=0;i--) ans2[i]=max(ans2[i],ans2[i+1]);
for(int i=0;i<n;i++){
cout<<ans1[i]<<" "<<(ans1[i]?ans2[i]:0)<<'\n';
}
return 0;
}
P6793 [SNOI2020] 字符串
我们考虑,每一次修改修改的都是一段后缀,启发我们对
那么,我们利用上面的思路,从大到小将
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=1e6+15;
int n,ans,K,as[MN],bs[MN],pre[MN];
string a,b,s;
vector<int> pos[MN];
namespace SA{
// 省略
}using namespace SA;
int root(int x){
if(pre[x]==x) return pre[x];
else return pre[x]=root(pre[x]);
}
void merge(int x,int y,int lcpl){
int rx=root(x),ry=root(y);
if(rx==ry) return;
int tmp=min(as[rx],bs[ry]);
ans+=max(0ll,K-lcpl)*tmp;
as[rx]-=tmp;
bs[ry]-=tmp;
tmp=min(bs[rx],as[ry]);
ans+=max(0ll,K-lcpl)*tmp;
bs[rx]-=tmp;
as[ry]-=tmp;
as[ry]=as[rx]+as[ry],bs[ry]=bs[rx]+bs[ry];
pre[rx]=ry;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin>>n>>K>>a>>b;
s=a+'#'+b;
for(int i=1;i<=n;i++){
as[i]=(i+K-1<=n);
}
for(int i=n+2;i<=2*n+1;i++){
bs[i]=(i-n-1+K-1<=n);
}
for(int i=0;i<=2*n+1;i++){
pre[i]=i;
}
getsa(s);
initst();
for(int i=n;i>=0;i--){
for(auto p:pos[i]){
merge(sa[p],sa[p-1],i);
}
}
put(ans);
return 0;
}
P7361 「JZOI-1」拜神
形式化题面如下:
给定一个长为
n 的字符串,询问次数为q ,多次询问区间[l,r] 内最长重复子串的长度。
没有形式化题面感觉都想不出来怎么做 www。
肯定没有那么菜啦,首先考虑二分长度,问题转化为区间内是否存在一个长为
接下来我们考虑这个最长重复子串怎么求,一个比较明显的想法就是后缀数组的 LCP 功能,原命题询问的实质就问是否存在
但是有点问题啊,如果我们直接这么做我们并没有考虑区间位置,也就是说在两个联通块启发式合并的时候我们必须要记录区间的位置。我们不妨考虑对于联通块内每一个位置,我们维护它在当前联通块内上一个元素的位置,记作
问题转化为如何维护
请注意二分的实现:
#include<bits/stdc++.h>
#define pir pair<int,int>
using namespace std;
constexpr int MN=5e4+15;
int n,q,pre[MN];
vector<int> vht[MN];
set<int> st[MN];
string s;
struct Segment{
#define ls t[p].lson
#define rs t[p].rson
struct Node{
int lson,rson,val;
}t[MN<<9];
int tot,rt[MN];
void pushup(int p){
t[p].val=max(t[ls].val,t[rs].val);
}
void modfiy(int &p,int lst,int l,int r,int pos,int v){
p=++tot;
t[p]=t[lst];
if(l==r){
t[p].val=max(t[p].val,v);
return;
}
int mid=(l+r)>>1;
if(mid>=pos) modfiy(ls,t[lst].lson,l,mid,pos,v);
else modfiy(rs,t[lst].rson,mid+1,r,pos,v);
pushup(p);
}
int query(int p,int l,int r,int fl,int fr){
if(l>=fl&&r<=fr){
return t[p].val;
}
int mid=(l+r)>>1,ret=0;
if(mid>=fl) ret=max(ret,query(ls,l,mid,fl,fr));
if(mid<fr) ret=max(ret,query(rs,mid+1,r,fl,fr));
return ret;
}
#undef ls
#undef rs
}sg;
namespace SA{
// 省略
}using namespace SA;
int root(int x){
if(pre[x]==x) return pre[x];
else return pre[x]=root(pre[x]);
// 这里用这种合并方式而不是按秩合并
// 是因为并查集维护的是联通块所属的集合,不用考虑形态变化。
}
void merge(int x,int y,int L){
int rx=root(x),ry=root(y);
if(rx==ry) return;
if(st[rx].size()<st[ry].size()) swap(rx,ry);
pre[ry]=rx;
for(auto p:st[ry]){
auto it=st[rx].lower_bound(p);
if(it!=st[rx].end()){
sg.modfiy(sg.rt[L],sg.rt[L],1,n,*it,p);
}
if(it!=st[rx].begin()){
it--;
sg.modfiy(sg.rt[L],sg.rt[L],1,n,p,*it);
}
}
for(auto p:st[ry]) st[rx].insert(p);
}
int main(){
cin>>n>>q>>s;
getsa(s);
for(int i=2;i<=n;i++){
vht[ht[i]].push_back(i);
}
for(int i=1;i<=n;i++){
pre[i]=i;
st[i].insert(i);
}
for(int i=n;i>=1;i--){
sg.rt[i]=sg.rt[i+1];
for(auto p:vht[i]){
merge(sa[p],sa[p-1],i);
}
}
while(q--){
int L,R;
cin>>L>>R;
int l=0,r=R-L+1;
while(l+1<r){
int mid=(l+r)>>1;
if(sg.query(sg.rt[mid],1,n,L,R-mid+1)>=L){
l=mid;
}else r=mid;
}
cout<<l<<'\n';
}
return 0;
}
4.2 单调栈技巧
P4248 [AHOI2013] 差异
前面两个都好说,关键字与后面这个每一个区间 LCP 之和怎么求回顾我们后缀数组求解 LCP 的式子:
若
rk_{i}<rk_{j} ,则|\operatorname{LCP}(i,j)|=\min\limits_{p=rk_{i}+1}^{rk_{j}}ht_p 。
那么现在问题转化为求每个区间的区间最小值之和,我们利用单调栈,考虑按排名顺序加入所有后缀并实时维护
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=1e6+51;
int top,sta[MN],L[MN],R[MN],ans;
string s;
namespace SA{
// 省略
}using namespace SA;
signed main(){
cin>>s;
getsa(s);
sta[top=1]=1;
for(int i=2;i<=len;i++){
while(top&&ht[sta[top]]>ht[i]) R[sta[top--]]=i;
L[i]=sta[top];
sta[++top]=i;
}
while(top) R[sta[top--]]=len+1;
ans=len*(len-1)*(len+1)/2;
for(int i=2;i<=len;i++){
ans-=2*(R[i]-i)*(i-L[i])*ht[i];
}
cout<<ans;
return 0;
}
P7409 SvT
问题即求:
其中
直接单调栈做就可以了,但是记得要去重哦,因为给出的有重复的。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
constexpr int MN=1e6+15;
constexpr ll MOD=23333333333333333;
int n,m,sta[MN],top,a[MN],w[MN],L[MN],R[MN];
namespace SA{
// 省略
int lcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(st[d][i],st[d][j-(1<<d)+1]);
}
}using namespace SA;
bool cmp(int x,int y){
return rk[x]<rk[y];
}
int main(){
string s;
cin>>n>>m>>s;
getsa(s);
initst();
while(m--){
int t;
ll ans=0;
cin>>t;
for(int i=1;i<=t;i++){
cin>>a[i];
}
sort(a+1,a+1+t);
t=unique(a+1,a+1+t)-a-1;
sort(a+1,a+1+t,cmp);
w[1]=0;
for(int i=2;i<=t;i++){
w[i]=lcp(a[i-1],a[i]);
}
sta[top=0]=0;
for(int i=1;i<=t;i++){
while(top&&w[sta[top]]>w[i]) top--;
L[i]=sta[top];
sta[++top]=i;
}
sta[top=0]=t+1;
for(int i=t;i>=1;i--){
while(top&&w[sta[top]]>=w[i]) top--;
R[i]=sta[top];
sta[++top]=i;
}
for(int i=2;i<=t;i++){
ans=(ans+1ll*w[i]*(R[i]-i)%MOD*(i-L[i])%MOD)%MOD;
}
cout<<ans<<'\n';
}
return 0;
}
P5161 WD 与数列
做这个题之前请先解锁:关键点 Trick。
一个序列整体加一个数后与另一个序列相同,其实就是差分数组相同,原因自行思考。
那么源问题去掉限制就是裸的单调栈,但是问题在于要求不相交的,我们可以考虑容斥,总方案数减去相交的方案。总方案单调栈做,问题在于相交如何求解?
但是若求不相交的两个区间信息,请注意在差分数组上求时应当是相隔一个位置。差分数组中相交或相邻的串在原串中都是相交的。
我们考虑相交怎么做,我们可以考虑枚举长度
#include<bits/stdc++.h>
#define int long long
#define pir pair<int,int>
using namespace std;
constexpr int MN=1e6+15;
int n,ans,a[MN],b[MN],top,tot;
pir st[MN];
struct SA{
// 省略。。。
}A,B;
int clac(int x,int y){
return (x+y)*(y-x+1)/2;
}
signed main(){
vector<int> s,t;
cin>>n;
--n;
for(int i=0;i<=n;i++){
cin>>a[i];
}
for(int i=n;i>=1;i--){
a[i]-=a[i-1];
b[++tot]=a[i];
}
sort(b+1,b+1+tot);
tot=unique(b+1,b+1+tot)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
s.push_back(a[i]);
t.push_back(a[i]);
}
A.getsa(s);
reverse(t.begin(),t.end());
B.getsa(t);
A.initst();
B.initst();
int sum=0;
for(int i=n;i>=1;i--){
ans+=sum;
int now=1;
while(top&&st[top].first>=A.ht[i]){
sum-=st[top].first*st[top].second;
now+=st[top--].second;
}
st[++top]=pir(A.ht[i],now);
sum+=now*A.ht[i];
}
for(int k=1;k<n;k++){
for(int i=1;i<=n/k-1;i++){
int x1=i*k,y1=x1+k,x2=n-y1+2,y2=n-x1+2;
int lcs=min(k-1,B.querylcp(x2,y2)),lcp=A.querylcp(x1,y1);
if(lcs+lcp-k+1<0) continue;
ans-=clac(max(lcp-k+1,0ll),lcs+lcp-k+1);
}
}
cout<<ans+n*(n+1)/2;
return 0;
}
P5115 Check, Check, Check one two!
前面两问都比较好说,问题在于后面的限制,它是两个限制。我们考虑只有一个限制,比如说 LCP 的限制,显然我们可以根据之前我们提到的,并查集的思路,从小往大插,就可以满足
观察
前面和后面都很好处理,中间的 LCP 用单调栈即可。
#include<bits/stdc++.h>
#define int long long
#define ull unsigned long long
using namespace std;
constexpr int MN=1e6+15;
int k1,k2,n,st[MN],top,w[MN];
ull ans,f[MN];
string s;
namespace SA{
int len,sa[MN],x[MN],y[MN],rk[MN],c[MN],ht[MN];
template<typename vct>
void getsa(vct &s){
int m=40000;
len=s.size();
s.insert(s.begin(),0);
for(int i=1;i<=len;i++){
x[i]=s[i];
++c[x[i]];
}
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=len;i>=1;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=len;k<<=1){
int num=0;
for(int i=len-k+1;i<=len;i++) y[++num]=i;
for(int i=1;i<=len;i++){
if(sa[i]>k) y[++num]=sa[i]-k;
}
for(int i=1;i<=m;i++) c[i]=0;
for(int i=1;i<=len;i++) c[x[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=len;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
num=1,x[sa[1]]=1;
for(int i=2;i<=len;i++){
if(y[sa[i]]==y[sa[i-1]]&&y[sa[i]+k]==y[sa[i-1]+k]) x[sa[i]]=num;
else x[sa[i]]=++num;
}
if(num==len) break;
m=num;
}
for(int i=1;i<=len;i++) rk[sa[i]]=i;
for(int i=1,k=0;i<=len;i++){
if(k) k--;
int j=sa[rk[i]-1];
while(i+k<=len&&j+k<=len&&s[i+k]==s[j+k]) k++;
ht[rk[i]]=k;
}
}
} using namespace SA;
ull calc1(int x){
return x*(x+1)/2;
}
ull calc2(int x){
return x*(x+1)*(x+x+1)/6;
}
ull solve(char lim) {
ull cur = 0, ans = 0;
top=0;
memset(w,0,sizeof(w));
for(int i = 2; i <= n; i++) {
int wid = lim ? s[sa[i - 1] - 1] == lim : 1;
while(top && st[top] >= ht[i]) {
cur -= 1ull * w[top] * f[st[top]];
wid += w[top--];
}
st[++top] = ht[i];
w[top] = wid;
cur += 1ull * wid * f[ht[i]];
if(lim ? s[sa[i] - 1] == lim : 1) {
ans += cur;
}
}
return ans;
}
signed main(){
cin>>s>>k1>>k2;
n=s.length();
for(int i=1;i<=n;i++){
int l=max(1ll,i-k2+1),r=min(i,k1);
if(l>r) break;
f[i]=(calc1(r)-calc1(l-1))*(i+1)-(calc2(r)-calc2(l-1));
}
getsa(s);
ans+=solve(0);
for(int i=0;i<26;i++) ans-=solve('a'+i);
cout<<ans;
return 0;
}
练习
CF1073G
[HAOI2016]找相同字符
4.3 SA加速匹配和查询子串
P3763 [TJOI2017] DNA
由于不重合的字符很少,考虑暴力枚举不重合的子串起始位置,让后从这个位置往后跳最长公共前缀的长度,这样如果枚举的位置正确也能保证后续位置也能递推正确。现在问题转化为如何快速求解 LCP,用二分加哈希或 SA 加 ST 表即可实现。
二分加哈希:
#include<bits/stdc++.h>
#define ull unsigned long long
using namespace std;
constexpr ull base=13131;
constexpr int MN=1e5+15;
int lena,lenb;
ull a[MN],b[MN],pw[MN];
string sa,sb;
void init(){
pw[0]=1;
for(int i=1;i<MN;i++) pw[i]=pw[i-1]*base;
}
ull hsha(int l,int r){
return a[r]-a[l-1]*pw[r-l+1];
}
ull hshb(int l,int r){
return b[r]-b[l-1]*pw[r-l+1];
}
bool binfind(int x){
int st=1,r=x+lenb-1,ed=lenb;
for(int i=1;i<=3;i++){
int lt=-1,rt=ed-st+2,ret=0;
while(lt+1<rt){
int mid=(lt+rt)>>1;
if(hsha(x,x+mid-1)==hshb(st,st+mid-1)) lt=mid;
else rt=mid;
}
x+=lt+1;
st+=lt+1;
if(st>ed) return 1;
}
return hsha(x,x+lenb-st)==hshb(st,ed);
}
void solve(){
cin>>sa>>sb;
lena=sa.length(),lenb=sb.length();
if(lena<lenb){
cout<<0<<'\n';
return;
}
sa=" "+sa;
sb=" "+sb;
for(int i=1;i<=lena;i++){
a[i]=a[i-1]*base+sa[i];
}
for(int i=1;i<=lenb;i++){
b[i]=b[i-1]*base+sb[i];
}
int ans=0;
for(int i=1;i<=lena-lenb+1;i++){
if(binfind(i)) ans++;
}
cout<<ans<<'\n';
}
int main(){
init();
int T;
cin>>T;
while(T--){
solve();
}
return 0;
}
SA:
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int n,m;
string s,t;
namespace SA{
// 省略
int lcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(st[d][i],st[d][j-(1<<d)+1]);
}
}using namespace SA;
void solve(){
cin>>s>>t;
n=s.length(),m=t.length();
s=" "+s,t=" "+t;
string sst;
for(int i=1;i<=n;i++){
sst.push_back(s[i]);
}
sst.push_back('#');
for(int i=1;i<=m;i++){
sst.push_back(t[i]);
}
getsa(sst);
initst();
int ret=0;
for(int i=1;i<=n-m+1;i++){
int curs=i,curt=1;
for(int j=1;j<=4;j++){
if(curt<=m){
int lcpl=lcp(curs,curt+n+1);
curs+=lcpl+(j<4);
curt+=lcpl+(j<4);
}
}
ret+=curt>m;
}
cout<<ret<<'\n';
}
int main(){
int T;
cin>>T;
while(T--){
solve();
}
return 0;
}
SP1812 LCS2
2.2 有讲两种做法。
现在我们要求在主串
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=3e6+15;
int col[MN],vis[MN],L[MN],R[MN],sum,tot,ans;
string s,str;
deque<int> q;
namespace SA{
// 省略
}using namespace SA;
void add(int x){
if(col[x]==0) return;
vis[col[x]]++;
if(vis[col[x]]==1) sum++;
}
void del(int x){
if(col[x]==0) return;
vis[col[x]]--;
if(vis[col[x]]==0) sum--;
}
int main(){
while(cin>>s){
tot++;
L[tot]=str.length()+1;
str=str+s;
R[tot]=str.length();
str.push_back(tot);
}
getsa(str);
for(int i=1;i<=tot;i++){
for(int j=L[i];j<=R[i];j++){
col[rk[j]]=i;
}
}
add(1);
for(int r=2,l=1;r<=len;r++){
while(!q.empty()&&ht[q.back()]>=ht[r]) q.pop_back();
q.push_back(r);
add(r);
if(sum==tot){
while(tot==sum&&l<r) del(l++);
add(--l);
}
while(!q.empty()&&q.front()<=l) q.pop_front();
if(tot==sum) ans=max(ans,ht[q.front()]);
}
cout<<ans;
return 0;
}
P5028 Annihilate
把字符串加分隔符连接在一起,让后跑后缀数组求
考虑计算 LCP 的过程就是一段
放这个题就是为了看见 SA 不要僵化思路,这种一般是学多做题多了导致的 www。
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=3e6+15,MK=55;
int n,pos[MN],minn[MK],ans[MK][MK];
vector<int> str;
namespace SA{
// 省略
}using namespace SA;
int main(){
cin>>n;
for(int i=1;i<=n;i++){
string s;
cin>>s;
for(auto c:s){
str.push_back(c);
pos[str.size()]=i;
}
str.push_back(1000+i);
}
getsa(str);
for(int i=2;i<=len;i++){
for(int j=1;j<=n;j++) minn[j]=min(minn[j],ht[i]);
minn[pos[sa[i-1]]]=ht[i];
for(int j=1;j<=n;j++){
ans[pos[sa[i]]][j]=ans[j][pos[sa[i]]]=max(ans[pos[sa[i]]][j],minn[j]);
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(i!=j) cout<<ans[i][j]<<" ";
}
cout<<'\n';
}
return 0;
}
P2463 [SDOI2008] Sandy 的卡片
一个序列整体加一个数后与另一个序列相同,其实就是差分数组相同,原因自行思考。
但是若求不相交的两个区间信息,请注意在差分数组上求时应当是相隔一个位置。差分数组中相交或相邻的串在原串中都是相交的。
那么先求差分数组,让后两个差分数组中间加分隔符连起来建立 SA,让后问题转化为求所有子串最长公共子串,考虑二分,只要有
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int n,lenn[MN],sta[MN],top,a[1145][1145];
int pos[MN];
bool vis[MN];
string s;
namespace SA{
// 省略
}using namespace SA;
bool check(int x){
while(top) vis[sta[top--]]=0;
for(int i=1;i<=len;i++){
if(ht[i]<x) while(top) vis[sta[top--]]=0;
if(!vis[pos[sa[i]]]){
vis[pos[sa[i]]]=1;
sta[++top]=pos[sa[i]];
if(top==n) return 1;
}
}
return 0;
}
int main(){
cin>>n;
int l=0,r=1e6,ans=0;
for(int i=1;i<=n;i++){
cin>>lenn[i];
for(int j=1;j<=lenn[i];j++){
cin>>a[i][j];
}
r=min(r,lenn[i]-1);
}
for(int i=1;i<=n;i++){
for(int j=2;j<=lenn[i];j++){
s.push_back(a[i][j]-a[i][j-1]);
pos[s.length()]=i;
}
s.push_back('#');
}
getsa(s);
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)){
l=mid+1;
ans=mid;
}else r=mid-1;
}
cout<<ans+1;
return 0;
}
SP220 PHRASES - Relevant Phrases of Annihilation
划分为 2 个子问题:
- 最长公共子串。
- 最长且不重叠出现两次及以上的子串。
第一问我们不说,我们考虑第二问如何求解,我们可以对于
结合起来就可以啦,时间复杂度
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=2e6+15;
int n,L[MN],R[MN];
string s;
vector<int> vct;
namespace SA{
void clear(){
memset(sa,0,sizeof(sa));
memset(rk,0,sizeof(rk));
memset(ork,0,sizeof(ork));
memset(buc,0,sizeof(buc));
memset(id,0,sizeof(id));
memset(ht,0,sizeof(ht));
memset(st,0,sizeof(st));
}
// 省略
}using namespace SA;
bool check(int x){
vct.clear();
for(int i=1;i<=len;i++){
if(ht[i]<x){
bool flag=true;
for(int j=1;j<=n;j++){
int mn=MN,mx=0;
for(int p:vct){
if(p<L[j]||p>R[j]) continue;
mn=min(mn,p);
mx=max(mx,p);
}
if(mx-mn<x){
flag=false;
break;
}
}
if(flag) return true;
vct.clear();
}
vct.push_back(sa[i]);
}
bool flag=true;
for(int j=1;j<=n;j++){
int mn=MN,mx=0;
for(int p:vct){
if(p<L[j]||p>R[j]) continue;
mn=min(mn,p);
mx=max(mx,p);
}
if(mx-mn<x){
flag=false;
break;
}
}
return flag;
}
void solve(){
cin>>n;
s.clear();
for(int i=1;i<=n;i++){
string st;
cin>>st;
L[i]=s.length()+1;
R[i]=s.length()+st.length();
s+=st;
s.push_back('#');
}
getsa(s);
int l=1,r=len,ans=0;
while(l<=r){
int mid=(l+r)>>1;
if(check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
cout<<ans<<'\n';
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
int T;cin>>T;
while(T--) solve();
return 0;
}
P4143 采集矿石
第一眼看上去好像没什么思路啊 www。考虑发掘性质,从大到小的话,如果我们选取的子串在不断扩大的话,排名会逐渐下降,但重要度因为是求和,所以是单调不减,我们考虑利用冰火战士的思路,两次二分出这个排名和重要度之和的交点,让后我们检查是否符合要求。
现在问题在于如何求某个子串在所有字符串本质不同子串的排名。所有本质不同子串的计算我们之前提到过,也就是:
两个条件,第一个条件对应的后缀排名是一段排名区间
#include<bits/stdc++.h>
#define int long long
#define pir pair<int,int>
using namespace std;
constexpr int MN=1e6+15;
int n,v[MN],sumh[MN],sumsa[MN];
vector<pir> ans;
string s;
namespace SA{
// 省略
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
// 手动查询 ST 表
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(ST[d][l],ST[d][r-(1<<d)+1]);
}
}using namespace SA;
int clac(int x,int y){
int l=1,r=x=rk[x]+1;
while(l+1<r){
int mid=(l+r)>>1;
if(querylcp(mid,x)>=y) r=mid;
else l=mid;
}
return sumsa[l]-sumh[l+1]-y+1;
}
signed main(){
cin>>s;
n=s.length();
for(int i=1;i<=n;i++){
cin>>v[i];
}
getsa(s);
initst();
for(int i=n;i>=1;i--){
sumh[i]=sumh[i+1]+ht[i];
sumsa[i]=sumsa[i+1]+n-sa[i]+1;
}
for(int i=1;i<=n;i++) v[i]+=v[i-1];
for(int i=1;i<=n;i++){
int l=1,r=n-i+2;
while(l+1<r){
int mid=(l+r)>>1;
if(clac(i,mid)>=v[i+mid-1]-v[i-1]) l=mid;
else r=mid;
}
if(clac(i,l)==v[i+l-1]-v[i-1]){
ans.push_back(pir(i,i+l-1));
}
}
cout<<ans.size()<<'\n';
for(auto p:ans) cout<<p.first<<" "<<p.second<<'\n';
return 0;
}
练习
P4081 [USACO17DEC] Standing Out from the Herd P
P6640 [BJOI2020] 封印
P2408 不同子串
P5431
4.4 对串进行分块操作
P1117 [NOI2016] 优秀的拆分
显然 AABB 由两个形如 AA 的串拼接起来的,考虑维护两个数组
现在问题在于如何求解
对于固定的
从这道题开始,设置关键点变成了经典套路。
下列代码
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int T,f[MN],g[MN];
string s;
struct SA{
int len,sa[MN],x[MN],y[MN],rk[MN],c[MN],ht[MN],ST[30][MN];
// 省略
// ST表初始化
void initst(){
for(int i=1;i<30;i++){
for(int j=1;j+(1<<i)-1<=len;j++){
ST[i][j]=min(ST[i-1][j],ST[i-1][j+(1<<(i-1))]);
}
}
}
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
// 手动查询 ST 表
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(ST[d][l],ST[d][r-(1<<d)+1]);
}
}A,B;
void solve(){
cin>>s;
int n=s.length();
A.getsa(s);
A.initst();
reverse(s.begin(),s.end());
B.getsa(s);
B.initst();
for(int i=1;i<=n;i++){
f[i]=g[i]=0;
}
for(int len=1;len<=(n>>1);len++){
for(int l=len;l<=n;l+=len){
int r=l+len,lcp=min(len,A.querylcp(l,r)),lcs=min(len-1,B.querylcp(n-(l-1)+1,n-(r-1)+1));
if(lcp+lcs>=len){
int cov=lcp+lcs-len+1;
f[r+lcp-cov]++;
f[r+lcp]--;
g[l-lcs+cov]--;
g[l-lcs]++;
}
}
}
for(int i=1;i<=n;i++) f[i]+=f[i-1],g[i]+=g[i-1];
long long ans=0;
for(int i=1;i<n;i++) ans+=1ll*f[i]*g[i+1];
cout<<ans<<'\n';
}
int main(){
cin>>T;
while(T--){
solve();
}
return 0;
}
SP687 REPEATS - Repeats
借助上面的套路,考虑枚举循环节长度
我们考虑二分
还可以更优化,我们当
Alex_wei 的 Sol 3 看不懂 Orz。
#include<bits/stdc++.h>
using namespace std;
constexpr int MN = 5e4 + 5;
int n;
struct SA {
int len, sa[MN], x[MN], y[MN], rk[MN], c[MN], ht[MN], ST[16][MN];
// 省略
int querylcp(int i, int j) {
if((i = rk[i]) > (j = rk[j])) swap(i, j);
int d = __lg(j - (i++));
return min(ST[d][i], ST[d][j - (1 << d) + 1]);
}
} A, B;
void solve() {
cin >> n;
string str;
for(int i = 1; i <= n; i++) {
char x;
cin >> x;
str.push_back(x);
}
A.getsa(str);
A.initst();
reverse(str.begin(), str.end());
B.getsa(str);
B.initst();
int ans = 1;
for(int len = 1; len <= n; len++) {
for(int l = len, r = len + len; r <= n; l += len, r += len) {
int lcp = A.querylcp(l, r);
int lcs = B.querylcp(n - r + 1, n - l + 1);
ans = max(ans, (lcp + lcs - 1) / len + 1);
}
}
cout << ans << '\n';
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int T;
cin >> T;
while(T--) {
solve();
}
return 0;
}
4.5 与DP贪心结合
这里我们是单独介绍一些利用贪心思路和 DP 思路求解的问题。一般来说,这里的问题 SA 不是主角,是起到一个打辅助的作用的。
CF822E Liar
一个显然的贪心,我们在新选择一个字符串并起来的时候应当尽可能的进行匹配,我们一定会匹配到第一个
字符串匹配,之前做过一堆 KMP 的题,这里肌肉记忆设
DP 加贪心加 SA,很好的题啊!
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=2e5+15,MK=35;
int f[MN][MK],X,n,m;
string s,t,sst;
namespace SA{
// 省略
int lcp(int i,int j){
if((i=rk[i])>(j=rk[j])) swap(i,j);
int d=__lg(j-(i++));
return min(st[d][i],st[d][j-(1<<d)+1]);
}
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(st[d][l],st[d][r-(1<<d)+1]);
}
}using namespace SA;
int lcpst(int i,int j){
if(i>n||j>m) return 0;
j+=n+1;
return lcp(i,j);
}
int main(){
cin>>n>>s>>m>>t;
sst=s+'#'+t;
getsa(sst);
initst();
cin>>X;
for(int i=1;i<=n;i++){
for(int j=1;j<=X;j++){
int L=lcpst(i,f[i][j-1]+1);
f[i+L][j]=max(f[i+L][j],f[i][j-1]+L);
f[i+1][j]=max(f[i+1][j],f[i][j]);
}
}
if(f[n+1][X]==m) cout<<"YES";
else cout<<"NO";
return 0;
}
P6095 [JSOI2015] 串分割
有一个贪心的想法就是我们贪心让最大位数最小。那么答案串的长度最多就是
让后我们考虑如何比较字符串大小,考虑二分排名
最后要求输出最大值数字串,考虑存下排名的值输出这个排名的后缀长为
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=1e6+15;
int n,K;
string s;
namespace SA{
// 省略
}using namespace SA;
bool check(int mid){
int len=n/K;
for(int i=1;i<=len;i++){
int st=i,cnt=0;
for(int j=1;j<=K;j++){
if(cnt<(n%K)&&rk[st]<=mid){
st+=len+1;
cnt++;
}else st+=len;
}
if(st-i==n) return 1;
}
return 0;
}
signed main(){
cin>>n>>K>>s;
if(n%K==0){
cout<<8;
return 0;
}
s=s+s;
getsa(s);
int l=1,r=2*n;
while(l+1<r){
int mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid;
}
for(int i=1;i<=ceil(n*1.0/K);i++){
cout<<s[sa[l]+i-1];
}
return 0;
}
4.6 与数据结构或离线算法结合
SP8093 莫队
先解决如何查一个查询串是多少个模板串的子串。我们考虑将所有模板串用分隔符连接起来跑 SA,那么查询串是一个模板串的子串,在 SA 上的范围表示的是一个区间的形式,这个区间的性质表示为
让后第二个,查询区间颜色数,既然我们已经有了每一个查询串对应的 SA 区间,考虑莫队维护区间颜色数,让后就做完了。
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
struct Query{
int l,r,id;
}qry[MN];
int n,m,qtot,qlen,col[MN],sumc,cnt[MN],ans[MN];
vector<int> str;
namespace SA{
// 省略
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
// 手动查询 ST 表
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(ST[d][l],ST[d][r-(1<<d)+1]);
}
}using namespace SA;
bool cmp(Query x,Query y){
if(x.l/qlen!=y.l/qlen) return x.l<y.l;
return x.r<y.r;
}
void add(int x){
cnt[col[x]]++;
if(cnt[col[x]]==1) sumc++;
}
void del(int x){
cnt[col[x]]--;
if(!cnt[col[x]]) sumc--;
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++){
string s;
cin>>s;
for(auto c:s){
str.push_back(c);
col[str.size()]=i;
}
str.push_back('z'+i);
}
getsa(str);
for(int i=1;i<=m;i++){
int slen,L=1,R=len;
string s;
cin>>s;
slen=s.length();
s=" "+s;
for(int j=1;j<=slen;j++){
int l=L,r=R;
while(l<=r){
int mid=(l+r)>>1;
if(str[sa[mid]+j-1]<s[j]) l=mid+1;
else r=mid-1;
}
swap(l,L);
r=R;
while(l<=r){
int mid=(l+r)>>1;
if(str[sa[mid]+j-1]<=s[j]) l=mid+1;
else r=mid-1;
}
R=r;
}
if(L<=R){
qry[++qtot]={L,R,i};
}
}
qlen=sqrt(len);
sort(qry+1,qry+1+qtot,cmp);
int l=1,r=0;
for(int i=1;i<=qtot;i++){
while(l<qry[i].l) del(sa[l++]);
while(l>qry[i].l) add(sa[--l]);
while(r<qry[i].r) add(sa[++r]);
while(r>qry[i].r) del(sa[r--]);
ans[qry[i].id]=sumc;
}
for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
return 0;
}
CF1608G Alphabetic Tree
哈哈其实没有那么多啦,我实现的时候其实也就不到 300 行吧,其实就是板子加板子,只是不过我是晚上写的太困了调了一万年。
因为信息具有可减性,战术将询问差分扫描线,即
对于后缀数组,查一个查询串是多少个模板串的子串。我们考虑将所有模板串用分隔符连接起来跑 SA,那么查询串是一个模板串的子串,在 SA 上的范围表示的是一个区间的形式,这个区间的性质表示为
那么对于本题来说也是一样的,我们对
但是本题上树了,我们必须要考虑哈希求解,那么这样的话我们必须求解出
巨大码农,代码。
[P4094 HEOI2016字符串]
又是最长公共子串,好烦啊~
直接二分长度,让后问题转化为判定性问题,那么一个长度可行当且仅当:
- 开头在
[a,b-mid+1] -
那么问题转化为询问满足以上两个条件的后缀
我主席树又写错了,希望大家认真实现,我已经做麻了呜呜呜。
#include<bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MN=1e6+15;
int n,m;
string s;
struct Segment{
#define ls t[p].lson
#define rs t[p].rson
struct Node{
int lson,rson,val;
}t[MN*20+1145];
int rt[MN],tot;
int insert(int lst,int l,int r,int x){
int p=++tot;
t[p]=t[lst];
t[p].val++;
if(l==r) return p;
int mid=(l+r)>>1;
if(x<=mid) t[p].lson=insert(t[lst].lson,l,mid,x);
else t[p].rson=insert(t[lst].rson,mid+1,r,x);
return p;
}
int query(int u,int v,int l,int r,int fl,int fr){
if(l>=fl&&r<=fr){
return t[v].val-t[u].val;
}
int mid=(l+r)>>1,ret=0;
if(mid>=fl) ret+=query(t[u].lson,t[v].lson,l,mid,fl,fr);
if(mid<fr) ret+=query(t[u].rson,t[v].rson,mid+1,r,fl,fr);
return ret;
}
int query(int u,int v,int l,int r){
return query(rt[u-1],rt[v],1,n,l,r);
}
#undef ls
#undef rs
}sg;
namespace SA{
int len,sa[MN],x[MN],y[MN],rk[MN],c[MN],ht[MN],ST[30][MN];
// 省略
// ST表初始化
void initst(){
for(int i=1;i<30;i++){
for(int j=1;j+(1<<i)-1<=len;j++){
ST[i][j]=min(ST[i-1][j],ST[i-1][j+(1<<(i-1))]);
}
}
}
// 查询位置为 i 和 位置为 j 的后缀的 LCP
int querylcp(int i,int j){
int d=__lg(j-(i++));
return min(ST[d][i],ST[d][j-(1<<d)+1]);
}
// 手动查询 ST 表
int queryst(int l,int r){
int d=__lg(r-l+1);
return min(ST[d][l],ST[d][r-(1<<d)+1]);
}
}using namespace SA;
bool check(int x,int a,int b,int c){
int l=1,r=rk[c],L,R;
while(l<r){
int mid=(l+r)>>1;
if(querylcp(mid,rk[c])<x) l=mid+1;
else r=mid;
}
L=r;
l=rk[c],r=n;
while(l<r){
int mid=(l+r+1)>>1;
if(querylcp(rk[c],mid)<x) r=mid-1;
else l=mid;
}
R=r;
return sg.query(L,R,a,b-x+1)>0;
}
void solve(int a,int b,int c,int d){
int l=0,r=min(b-a+1,d-c+1);
while(l<r){
int mid=(l+r+1)>>1;
if(check(mid,a,b,c)) l=mid;
else r=mid-1;
}
cout<<r<<'\n';
}
signed main(){
cin>>n>>m>>s;;
getsa(s);
initst();
for(int i=1;i<=n;i++){
sg.rt[i]=sg.insert(sg.rt[i-1],1,n,sa[i]);
}
while(m--){
int a,b,c,d;
cin>>a>>b>>c>>d;
solve(a,b,c,d);
}
return 0;
}
P2336 [SCOI2012] 喵星球上的点名
将姓和名用分隔符连接,问题相当于给定
将所有文本串用分隔符后建后缀数组,对每个模式串求出以其为前缀的排名区间,这个讲过 100 万遍了不再重复。第一位就是问区间颜色数,考虑离线下来跑莫队或者扫描线 BIT,第二问相当于对每种颜色查询与其有交的区间数。对每个区间和每个颜色在第一个位置统计答案,则每个位置对其颜色贡献在左端点落一个区间,右端点落另一端区间的区间数量,这是二位数点,还是扫描线 BIT。
#include<bits/stdc++.h>
using namespace std;
constexpr int N=1e6+15, INF=1e9;
int nn, q, n, m=400000;
int sa[N], ra[N], h[N], t1[N], t2[N], c[N];
int st[20][N], lg[N];
int s[N], col[N], hd[N], len[N], bu[N];
int pre[N], ans1[N], ans2[N];
int lp[N];
struct Query { int id, l, r; } qry[N];
struct BIT {
int t[N];
void clear() {
memset(t, 0, sizeof(t));
}
void upd(int i, int v) {
if (i) for (; i<=n; i+=i&-i) t[i] += v;
}
int query(int i) {
int res = 0;
for (; i; i-=i&-i) res += t[i];
return res;
}
int query(int l, int r) {
return query(r) - query(l-1);
}
} bit1, bit2;
void getsa() {
// 省略
}
void geth() {
// 省略
}
void initST() {
// 省略
}
int getmin(int a, int b) {
if (a == b) return INF;
if (a > b) swap(a, b);
int d = lg[b-(a++)];
return min(st[d][a], st[d][b-(1<<d)+1]);
}
bool cmp(Query a, Query b) {
return a.r < b.r;
}
int main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> nn >> q;
int x, c = 10000;
for (int i=1; i<=nn; ++i) {
for (int j=0; j<2; ++j) {
int len; cin >> len;
while (len--) {
cin >> x;
s[++n] = x;
col[n] = i;
}
s[++n] = ++c;
}
}
for (int i=1; i<=q; ++i) {
cin >> len[n+1];
hd[n+1] = i;
for (int j=len[n+1]; j--; ) {
cin >> x;
s[++n] = x;
col[n] = -i;
}
s[++n] = ++c;
}
getsa();
geth();
initST();
for (int i=1; i<=n; ++i) {
if (col[sa[i]] > 0) {
pre[i] = bu[col[sa[i]]];
bu[col[sa[i]]] = i;
}
if (hd[i]) {
qry[hd[i]].id = hd[i];
int l=1, r=ra[i];
while (l < r) {
int mi = (l+r)>>1;
if (getmin(mi, ra[i]) >= len[i]) r = mi;
else l = mi+1;
}
qry[hd[i]].l = lp[hd[i]] = l;
l = ra[i], r = n;
while (l < r) {
int mi = (l+r+1)>>1;
if (getmin(ra[i], mi) >= len[i]) l = mi;
else r = mi-1;
}
qry[hd[i]].r = r;
}
}
sort(qry+1, qry+q+1, cmp);
sort(lp+1, lp+q+1);
for (int i=1, j=1, k=1; i<=n; ++i) {
for (; j<=q && lp[j]==i; ++j) bit2.upd(i, 1);
if (col[sa[i]] > 0) {
ans2[col[sa[i]]] += bit2.query(i) - bit2.query(pre[i]);
bit1.upd(i, 1);
bit1.upd(pre[i], -1);
}
for (; k<=q && qry[k].r==i; ++k) {
ans1[qry[k].id] = bit1.query(qry[k].l, qry[k].r);
bit2.upd(qry[k].l, -1);
}
}
for (int i=1; i<=q; ++i) cout << ans1[i] << "\n";
for (int i=1; i<=nn; ++i) cout << ans2[i] << " ";
return 0;
}
4.7 与其他结合
CF1654F Minimal String Xoration
关键性质:位运算在每一位独立。
设
倍增计数排序
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int n,m,v,a[MN],b[MN],c[MN];
string s;
bool cmp(int x,int y){
if(b[x]==b[y]) return b[x^v]<b[y^v];
return b[x]<b[y];
}
int main(){
cin>>n>>s;
m=1<<n;
for(int i=0;i<m;i++) a[i]=i,b[i]=s[i]-'a';
sort(a,a+m,cmp);
for(int i=1;i<=n;i++){
v=(1<<(i-1));
sort(a,a+m,cmp);
int cnt=0;
for(int j=0;j<m;j++){
if(j==0||cmp(a[j-1],a[j])) c[a[j]]=++cnt;
else c[a[j]]=cnt;
}
for(int j=0;j<m;j++) b[j]=c[j];
}
for(int i=0;i<m;i++) cout<<s[i^a[0]];
}
GYM102803E Everybody Lost Somebody
给同学做了,好题。
给出 SA 数组和 Height 数组我们能得到什么信息,具体来说:
- 对于
2\le i \le n ,有s[sa_{i-1}]\le s[sa_{i}] 。更进一步,若rk_{sa(i-1)+1}>rk_{sa(i)+1} ,则必须有s[sa_{i-1}] < s[sa_{i}] 。 - 对于
2\le i \le n ,对于0\le j \le ht_{i} ,有s[sa_{i-1}+j]\le s[sa_{i}+j] 。更进一步,若sa_{i-1}+ht_{i}\le n ,则必须有s[sa_{i-1}+ht_{i}] < s[sa_{i}+ht_{i}] 。
对于
现在考虑
进一步可以发现,只要在合并的时候保持后缀大小顺序的连边,就可以通过了。时间复杂度
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=5200;
int n,sa[MN],rk[MN],pre[MN],ht[MN];
char ans[MN];
vector<int> G[MN];
int root(int x){
if(pre[x]==x) return pre[x];
else return pre[x]=root(pre[x]);
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>sa[i];
rk[sa[i]]=i;
pre[i]=i;
}
for(int i=2;i<=n;i++){
cin>>ht[i];
}
for(int i=2;i<=n;i++){
if(ht[i]==-1){
int x=sa[i-1]+1,y=sa[i]+1;
if(rk[x]>rk[y]) G[sa[i]].push_back(sa[i-1]);
}
else{
int x=sa[i-1],y=sa[i];
for(int j=1;j<=ht[i];j++){
pre[root(x+j-1)]=pre[root(y+j-1)];
}
if(x+ht[i]<=n) G[y+ht[i]].push_back(x+ht[i]);
}
}
ans[sa[1]]='a';
for(int i=2;i<=n;i++){
ans[sa[i]]=ans[sa[i-1]];
for(auto v:G[sa[i]]){
if(ans[v]+1>ans[sa[i]]) ans[sa[i]]=ans[v]+1;
}
for(int j=1;j<n;j++){
if(root(sa[j])==root(sa[i])) ans[sa[j]]=ans[sa[i]];
}
}
for(int i=1;i<=n;i++) cout<<ans[i];
return 0;
}
5. 后缀数组变形-树上后缀数组
P5353
可以参考 STARSczy题解的思路,这里我就不再详细展开了(打字太累了)
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=1e6+15;
int n,fa[MN],rk[MN],c[MN],sa[MN],x[MN],y[MN],tmp[MN];
string s;
vector<int> adj[MN];
namespace SAonTree{
void dfs(int u){
rk[u]+=c[rk[u]]++;
sort(adj[u].begin(),adj[u].end());
for(int v:adj[u]) dfs(v);
}
void getsa(string s){
int len=s.length();
memset(c,0,sizeof(c));
for(int i=1;i<=len;i++) c[s[i-1]+1]++;
for(int i=1;i<=1000;i++) c[i]+=c[i-1];
for(int i=1;i<=len;i++) rk[i]=c[s[i-1]]+1;
for(int w=0;w<=__lg(len);w++){
memset(c,0,sizeof(c));
memset(tmp,0,sizeof(tmp));
for(int i=1;i<=len;i++){
x[i]=rk[i];
y[i]=rk[fa[i]];
c[y[i]+1]++;
}
for(int i=1;i<=len;i++) c[i]+=c[i-1];
for(int i=1;i<=len;i++) tmp[++c[y[i]]]=i;
memset(c,0,sizeof(c));
for(int i=1;i<=len;i++) rk[tmp[i]]+=c[x[tmp[i]]]++;
for(int i=1;i<=len;i++) sa[rk[i]]=i;
for(int i=1;i<=len;i++){
if(x[sa[i-1]]==x[sa[i]] && y[sa[i-1]]==y[sa[i]])
rk[sa[i]]=rk[sa[i-1]];
}
for(int i=len;i>=1;i--) fa[i]=fa[fa[i]];
}
memset(c,0,sizeof(c));
dfs(1);
for(int i=1;i<=len;i++) sa[rk[i]]=i;
}
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
cin>>n;
for(int i=2;i<=n;i++){
cin>>fa[i];
adj[fa[i]].push_back(i);
}
cin>>s;
SAonTree::getsa(s);
for(int i=1;i<=n;i++) cout<<sa[i]<<" ";
return 0;
}
例题:
P5346
假设我们求出来了
- 操作 1:
O(1) 回答。 - 操作 2:考虑主席树求第
k 小,只需要在节点的父亲版本上更新即可。O(\log n) 回答 - 操作 3:按照 DFN 序更新即可。
O(\log n) 回答。 而排名求解利用树上后缀数组即可:
#include<bits/stdc++.h>
using namespace std;
constexpr int MN=2e6+15;
int n,q,fa[MN],a[MN],b[MN],tot;
vector<int> adj[MN],s;
struct Segment{
#define ls t[p].lson
#define rs t[p].rson
struct Node{
int lson,rson,val;
}t[MN*20+1145];
int tot,rt[MN];
int insert(int lst,int l,int r,int x){
int p=++tot;
t[p]=t[lst];
t[p].val+=1;
if(l==r) return p;
int mid=(l+r)>>1;
if(mid>=x) ls=insert(t[lst].lson,l,mid,x);
else rs=insert(t[lst].rson,mid+1,r,x);
return p;
}
int query(int u,int v,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1;
int rz=t[t[v].rson].val-t[t[u].rson].val;
if(k<=rz) return query(t[u].rson,t[v].rson,mid+1,r,k);
return query(t[u].lson,t[v].lson,l,mid,k-rz);
}
#undef ls
#undef rs
}sg0,sg1;
namespace ly
{
namespace IO
{
#ifndef LOCAL
constexpr auto maxn=1<<20;
char in[maxn],out[maxn],*p1=in,*p2=in,*p3=out;
#define getchar() (p1==p2&&(p2=(p1=in)+fread(in,1,maxn,stdin),p1==p2)?EOF:*p1++)
#define flush() (fwrite(out,1,p3-out,stdout))
#define putchar(x) (p3==out+maxn&&(flush(),p3=out),*p3++=(x))
class Flush{public:~Flush(){flush();}}_;
#endif
namespace usr
{
template<typename type>
inline type read(type &x)
{
x=0;bool flag(0);char ch=getchar();
while(!isdigit(ch)) flag^=ch=='-',ch=getchar();
while(isdigit(ch)) x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
return flag?x=-x:x;
}
template<typename type>
inline void write(type x)
{
x<0?x=-x,putchar('-'):0;
static short Stack[50],top(0);
do Stack[++top]=x%10,x/=10;while(x);
while(top) putchar(Stack[top--]|48);
}
inline char read(char &x){do x=getchar();while(isspace(x));return x;}
inline char write(const char &x){return putchar(x);}
inline void read(char *x){static char ch;read(ch);do *(x++)=ch;while(!isspace(ch=getchar())&&~ch);}
template<typename type>inline void write(type *x){while(*x)putchar(*(x++));}
inline void read(string &x){static char ch;read(ch),x.clear();do x+=ch;while(!isspace(ch=getchar())&&~ch);}
inline void write(const string &x){for(int i=0,len=x.length();i<len;++i)putchar(x[i]);}
template<typename type,typename...T>inline void read(type &x,T&...y){read(x),read(y...);}
template<typename type,typename...T>
inline void write(const type &x,const T&...y){write(x),putchar(' '),write(y...),sizeof...(y)^1?0:putchar('\n');}
template<typename type>
inline void put(const type &x,bool flag=1){write(x),flag?putchar('\n'):putchar(' ');}
}
#ifndef LOCAL
#undef getchar
#undef flush
#undef putchar
#endif
}using namespace IO::usr;
}using namespace ly::IO::usr;
namespace SAonTree{
int rk[MN],c[MN],tmp[MN],x[MN],y[MN],sa[MN];
void dfs(int u){
rk[u]+=c[rk[u]]++;
sort(adj[u].begin(),adj[u].end());
for(int v:adj[u]) dfs(v);
}
void getsa(vector<int> s){
int len=s.size();
memset(c,0,sizeof(c));
for(int i=1;i<=len;i++) c[s[i-1]+1]++;
for(int i=1;i<=5e5;i++) c[i]+=c[i-1];
for(int i=1;i<=len;i++) rk[i]=c[s[i-1]]+1;
for(int w=0;w<=__lg(len);w++){
memset(c,0,sizeof(c));
memset(tmp,0,sizeof(tmp));
for(int i=1;i<=len;i++){
x[i]=rk[i];
y[i]=rk[fa[i]];
c[y[i]+1]++;
}
for(int i=1;i<=len;i++) c[i]+=c[i-1];
for(int i=1;i<=len;i++) tmp[++c[y[i]]]=i;
memset(c,0,sizeof(c));
for(int i=1;i<=len;i++) rk[tmp[i]]+=c[x[tmp[i]]]++;
for(int i=1;i<=len;i++) sa[rk[i]]=i;
for(int i=1;i<=len;i++){
if(x[sa[i-1]]==x[sa[i]] && y[sa[i-1]]==y[sa[i]])
rk[sa[i]]=rk[sa[i-1]];
}
for(int i=len;i>=1;i--) fa[i]=fa[fa[i]];
}
memset(c,0,sizeof(c));
dfs(1);
for(int i=1;i<=len;i++) sa[rk[i]]=i;
for(int i=1;i<=n;i++){
a[i]=rk[i];
}
}
}using namespace SAonTree;
namespace Tree{
int siz[MN],dfn[MN],dtot;
void dfsTree(int u){
dfn[u]=++dtot;
siz[u]=1;
sg1.rt[dfn[u]]=sg1.insert(sg1.rt[dfn[u]-1],1,n,a[u]);
for(auto v:adj[u]){
sg0.rt[v]=sg0.insert(sg0.rt[u],1,n,a[v]);
dfsTree(v);
siz[u]+=siz[v];
}
}
}using namespace Tree;
int main(){
read(n,q);
for(int i=2;i<=n;i++){
read(fa[i]);
adj[fa[i]].push_back(i);
}
for(int i=1;i<=n;i++){
read(a[i]);
b[++tot]=a[i];
}
sort(b+1,b+1+tot);
tot=unique(b+1,b+1+tot)-b-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(b+1,b+1+tot,a[i])-b;
s.push_back(a[i]);
}
getsa(s);
sg0.rt[1]=sg0.insert(0,1,n,a[1]);
dfsTree(1);
while(q--){
int op,x,k;
read(op,x);
if(op==1){
put(n+1-a[x]);
}else if(op==2){
read(k);
put(sa[sg0.query(0,sg0.rt[x],1,n,k)]);
}else{
read(k);
put(sa[sg1.query(sg1.rt[dfn[x]-1],sg1.rt[dfn[x]+siz[x]-1],1,n,k)]);
}
}
return 0;
}
6. 后言
实际上,我们一些技巧基本都体现了我们开头所提到的增量法与势能分析,通过已求信息逐步推导新信息。一些技巧例如关键点思想或并查集块合并通过将全局问题转化为局部问题,动态问题转化为静态问题。
字符串后缀,是字符串的大杀器,在做题过程中,我们能够体会到后缀独特的性质,能够将我们必须暴力枚举的子串简单化,并且将它的对立面——前缀联系起来,后缀数组这一利器,能够解决大部分的问题,但是,有一些问题是后缀数组所不能解决的。这个时候,就要出动我们的 SAM 啦,敬请期待,字符串终极神器——后缀自动机 - 洛谷专栏。
UPD on 2025.7.7:孩子们,我题全部都做完了,结果练 SAM 发现题单里的题都用后缀数组实现过一遍了,充分证明了 SA 可以替代大部分 SAM。然而不是这样的。
完结撒花!
参考
- Oi-Wiki
- Alex_wei 的字符串基础及其题解
- Hoks 的 P6095 题解
- LostKeyToReach 的 P7409 题解
- 云浅知处的课件(保密)
- 罗勇军的算法竞赛书
- 算法竞赛进阶指南
- chenly8128 的 P5028 题解
- wjyppm1403的 P7361 题解
- STARSczy树上后缀数组题解
- 许智磊--后缀数组