地对空导弹(SAM)
算法原理请见 oi-wiki SAM
模板:建立SAM & 统计 endpos 大小
#include<bits/stdc++.h>
using namespace std;
#define N 2050500
int lik[N],ch[N][27],len[N],num=1,lst=1,mn[N],n,val[N];
vector<int>e[N];
int siz[N];
void insert(int c){
int p=lst,np=++num;len[np]=len[p]+1;val[num]=1;
for(;p&&!ch[p][c];p=lik[p])ch[p][c]=np;
if(!p){
lik[np]=1;lst=np;return ;
}
int q=ch[p][c];
if(len[q]==len[p]+1){
lik[np]=q;lst=np;return ;
}
int now=++num;lik[now]=lik[q];len[now]=len[p]+1;
for(int i=0;i<=26;i++)ch[now][i]=ch[q][i];
for(;p&&ch[p][c]==q;p=lik[p])ch[p][c]=now;
lik[q]=lik[np]=now;lst=np;
}
void init(){
for(int i=1;i<=num;i++)e[lik[i]].push_back(i);
}
void dfs(int u,int fa){
int tag=0;
for(auto v:e[u]){
if(v==fa)continue;
dfs(v,u);siz[u]+=siz[v];tag=1;
}
siz[u]+=val[u];
}
char s[N];
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
cin>>s+1;n=strlen(s+1);
for(int i=1;i<=n;i++)insert(s[i]-'a'+1);init();
dfs(1,0);long long ans=0;
// for(int i=1;i<=num;i++)cout<<len[i]<<"\n";
for(int i=1;i<=num;i++)if(siz[i]!=1)ans=max(ans,1ll*siz[i]*len[i]);
cout<<ans<<"\n";
}
有用信息。
- link-tree 父亲所代表最长字符串是儿子所代表最短字符串删去末尾字符而成
- endpos 集合,要么包含要么不交,在link-tree上,祖先关系对应包含关系,对于一个endpos集合里的串,长度连续。
- sam:点数上界
,边数上界 - lcs(i,j)=len(lca(id[i],id[j])),即前缀
的最长公共后缀是两个前缀在 link 树上的 lca 的长度 - 子串出现次数:这个串在
上的位置所代表的 大小。 - 每一个前缀是 endpos 集合里的元素,因此统计大小时这些节点初始化为1。
- 判断子串:在 SAM 上直接走
- 本质不同子串数:
- 构造非法串:建立虚点表示非法,将sam上每个位置的空余字符指向这个虚点。这样也可以求出最小非法串长度——DP即可。
- SAM 是一张DAG,link 是一棵树
- endpos 集合的具体数字信息——利用线段树合并维护,也可以启发式合并之类的。
- 最小循环表示:建立 SS,从根开始贪心走小的字符走
步即可。 - 字典序第
小子串。核心思路是求出 SAM 上走到每个点之后还能走出 个点。然后类似平衡树进行查找。
- 要求本质不同:则直接初始化
,DAG上直接记忆化搜索 - 不要求本质不同,则需要初始化为
,这里是因为走到这里有这么多个相同的位置。
- 最长公共子串(多个串版本)
选出最短的串建立SAM,剩下的串在SAM上游走,每个串记录一下走到SAM上各个节点时匹配长度的最大值
,具体的我们用两个指针 表示当前匹配到节点 ,长度为 ,则我们在匹配下一个字符 的时候不断跳 直到 存在,跳的时候不断令 ,最后令 。最后还需要跑一次 dfs(link-tree),来对子树的 取得 ,但需要注意对 取较小值。最后把每一个串的 取最小值就是在这个点的最长公共匹配长度。模板以下。
void dfs(int u,int fa){
for(auto v:e[u]){
if(v==fa)continue;
dfs(v,u);
tmp[u]=max(tmp[u],tmp[v]);
}
tmp[u]=min(tmp[u],len[u]);mxid[u]=min(mxid[u],tmp[u]);
}
for(int i=0;i<s[1].size();i++)insert(s[1][i]-'a'+1);
for(int i=1;i<=num;i++)e[lik[i]].push_back(i);
for(int k=1;k<=cnt;k++){
int m=s[k].size();
int now=1,mxlen=0;
for(int i=1;i<=m;i++){
int x=s[k][i-1]-'a'+1;
while(now&&!ch[now][x])now=lik[now],mxlen=len[now];
if(!now){
now=1;mxlen=0;continue;
}
now=ch[now][x];mxlen++;tmp[now]=max(tmp[now],mxlen);
}
dfs(1,0);
for(int i=1;i<=num;i++)tmp[i]=0;
}int ans=0;
// for(int i=1;i<=num;i++)cout<<mxid[i]<<" ";cout<<"\n";
for(int i=1;i<=num;i++)ans=max(ans,mxid[i]);cout<<ans<<"\n";
- 首次出现位置:在建立 SAM 过程中维护
,满足
- np:
- now:
- 查询
时直接搜到对应状态输出
- 查找
对应位置:跳 在 上的祖先,跳到第一个 的点。
这个位置是第一个表出
的位置,这是SAM的后缀性质决定的。它的 endpos 集合里的位置都是 这个串的结尾位置,因此这个子树里所有的位置与其可以匹配。
应用
封印
给定串
核心:
对
则询问
显然求出
Cyclical Quest
给定
字符串
给定
考虑二分答案
这等价于我们找到
我们考虑写一个可持久化版本的线段树合并,类似于主席树。利用倍增找到这个位置,然后用线段树区间求和即可。思路很简单。
vector<int>e[N];
void build(){
for(int i=1;i<=num;i++)e[lik[i]].push_back(i);
}
int lc[N<<4],rc[N<<4],sum[N<<4],idseg;
int merge(int a,int b){
int now=++idseg;
sum[now]=sum[a]+sum[b];
if(!a||!b){
lc[now]=lc[a+b],rc[now]=rc[a+b];
return now;
}
lc[now]=merge(lc[a],lc[b]);
rc[now]=merge(rc[a],rc[b]);
return now;
}
void insert(int &x,int l,int r,int pos){
if(!x)x=++idseg;
++sum[x];
if(l==r)return ;
int mid=l+r>>1;
if(pos<=mid)insert(lc[x],l,mid,pos);
else insert(rc[x],mid+1,r,pos);
}
int gsum(int x,int l,int r,int L,int R){
if(!x)return 0;
if(L<=l&&r<=R)return sum[x];
int mid=l+r>>1;
if(L<=mid&&mid<R)return gsum(lc[x],l,mid,L,R)+gsum(rc[x],mid+1,r,L,R);
if(L<=mid)return gsum(lc[x],l,mid,L,R);
return gsum(rc[x],mid+1,r,L,R);
}
int rt[N];
void dfs(int u,int fa){
if(siz[u])insert(rt[u],1,n,idt[u]);
f[u][0]=fa;
for(int i=1;i<=20;i++)f[u][i]=f[f[u][i-1]][i-1];
for(auto v:e[u])dfs(v,u),rt[u]=merge(rt[u],rt[v]);
}
int get(int pos,int ln){
pos=idsam[pos];
for(int i=20;i>=0;--i)if(len[f[pos][i]]>=ln)pos=f[pos][i];
return pos;
}
repeats
给定
枚举答案长度
struct Surface_to_air_missiles{
int ch[N][27],len[N],idt[N],lik[N],lst=1,num=1,tmp[N],siz[N],f[N][21],idsam[N],dep[N];
void insert(int c,int id){
int p=lst,np=++num;lst=np;len[np]=len[p]+1;siz[np]=1;idt[np]=id;idsam[id]=np;
for(;p&&!ch[p][c];p=lik[p])ch[p][c]=np;
if(!p){
lik[np]=1;return ;
}
int q=ch[p][c];
if(len[q]==len[p]+1){
lik[np]=q;return ;
}
int now=++num;len[now]=len[p]+1;lik[now]=lik[q];
for(int i=0;i<26;i++)ch[now][i]=ch[q][i];
for(;p&&ch[p][c]==q;p=lik[p])ch[p][c]=now;
lik[q]=lik[np]=now;
}
vector<int>e[N];
void build(){
for(int i=1;i<=num;i++)e[lik[i]].push_back(i);
}
void dfs(int u,int fa){
f[u][0]=fa;dep[u]=dep[fa]+1;
for(int i=1;i<=18;i++)f[u][i]=f[f[u][i-1]][i-1];
for(int v:e[u])dfs(v,u);
}
int lca(int u,int v){
if(dep[u]>dep[v])swap(u,v);
for(int i=18;i>=0;--i)if(dep[f[v][i]]>=dep[u])v=f[v][i];
if(v==u)return u;
for(int i=18;i>=0;--i)if(f[v][i]!=f[u][i])v=f[v][i],u=f[u][i];
return f[u][0];
}
int lcs(int a,int b){
a=idsam[a],b=idsam[b];
return len[lca(a,b)];
}
}pre,suf;
int lcs(int a,int b){
return pre.lcs(a,b);
}
int lcp(int a,int b){
return suf.lcs(n-a+1,n-b+1);
}
int ans=0;
void calc(int len){
for(int l=len,r=len<<1;r<=n;l+=len,r+=len){
int suf=lcp(l,r),pre=lcs(l,r);
// cout<<" ! "<<l<<" "<<r<<" "<<pre<<" "<<suf<<"\n";
ans=max(ans,(pre+suf-1)/len);
}
}
Security
给定 -1
。
显然答案
不妨枚举
注意到这个问题其实和第四题有一定相似性,如果我们将
其实也就是在匹配的基础上外加了一个对串尾的后继字符的一个大小限制。
但是这是容易的。其实我们不妨给每个字符设定权值为
同时答案的拼凑也是相当简单的,具体细节见代码。
#include<bits/stdc++.h>
using namespace std;
#define N 650500
bool st;
int ch[N][30],n,m,len[N<<1],idt[N<<1],lik[N<<1],lst=1,num=1,siz[N<<1],f[N<<1][21],idsam[N<<1],w[N*21],val[N<<1],n1;
void insert(int c,int id){
int p=lst,np=++num;lst=np;len[np]=len[p]+1;siz[np]=(id<=n1);idt[np]=id;idsam[id]=np;
for(;p&&!ch[p][c];p=lik[p])ch[p][c]=np;
if(!p){
lik[np]=1;return ;
}
int q=ch[p][c];
if(len[q]==len[p]+1){
lik[np]=q;return ;
}
int now=++num;len[now]=len[p]+1;lik[now]=lik[q];
for(int i=0;i<28;i++)ch[now][i]=ch[q][i];
for(;p&&ch[p][c]==q;p=lik[p])ch[p][c]=now;
lik[q]=lik[np]=now;
}
vector<int>e[N];
void build(){
for(int i=1;i<=num;i++)e[lik[i]].push_back(i);
}
/*
回想一下SA的做法:二分答案,SA里查找符合LCP长度条件的范围,进而利用主席树判断是否存在
转移到SAM上,我们能知道什么?
首先我们需要反串处理后缀
这样我们可以通过link-tree上的LCA求得最长公共前缀长度lcsuf(i,j)=max(lca(pos_i,pos_j))
翻转后同样可以考虑倍增答案,倍增树上k级祖先
然后问题变成这个祖先的子树里是否存在一个定区间的前缀
其实还是主席树?
貌似不太行
怕什么啊
考虑线段树合并式可持久化?
照样是二分答案
其实就转化为s[c,c+mid-1]是否出现过
其实也可以想办法先找到这个东西对应的位置
然后看其link里的子树有没有在[a,b-mid+1]的即可
找这个点怎么办
其实可以考虑在SAM上的意义,假设找[l,r]
首先endpos包含了r,所以说肯定是id_r的祖先
其次应该是最上面的,这个最上面的意义是?
它所代表串的长度必须要大于等于r-l+1
也就是max(x)>=r-l+1
这样才能说明包含了这个串
这样就OK了
然后同样的,我就是要判断这玩意的endpos集合是否包含了[a+mid-1,b]中的某些数
但主要是这玩意需要可持久化
endpos集合的处理只能是线段树合并,但是怎么可持久化
简单,合并的过程中用虚点类似主席树建立即可
*/
int lc[N*21],rc[N*21],sum[N*21],idseg;
int merge(int a,int b){
int now=++idseg;
sum[now]=sum[a]+sum[b];w[now]=w[a]|w[b];
if(!a||!b){
lc[now]=lc[a+b],rc[now]=rc[a+b];
return now;
}
lc[now]=merge(lc[a],lc[b]);
rc[now]=merge(rc[a],rc[b]);
return now;
}
void insert(int &x,int l,int r,int pos){
if(!x)x=++idseg;
++sum[x];w[x]|=val[pos];
if(l==r)return ;
int mid=l+r>>1;
if(pos<=mid)insert(lc[x],l,mid,pos);
else insert(rc[x],mid+1,r,pos);
}
#define pr pair<int,int>
#define mk make_pair
pr merge(pr a,pr b){
return mk(a.first+b.first,a.second|b.second);
}
pr gsum(int x,int l,int r,int L,int R){
if(!x)return mk(0,0);
if(L<=l&&r<=R)return mk(sum[x],w[x]);
int mid=l+r>>1;
if(L<=mid&&mid<R)return merge(gsum(lc[x],l,mid,L,R),gsum(rc[x],mid+1,r,L,R));
if(L<=mid)return gsum(lc[x],l,mid,L,R);
return gsum(rc[x],mid+1,r,L,R);
}
int rt[N];
void dfs(int u,int fa){
if(siz[u])insert(rt[u],1,n1,idt[u]);
f[u][0]=fa;
for(int i=1;i<=18;i++)f[u][i]=f[f[u][i-1]][i-1];
for(auto v:e[u])dfs(v,u),rt[u]=merge(rt[u],rt[v]);
}
/*
或许可以考虑倍增答案
这样也许是可以做到更优解?
不行的,我们注意到找到这个点是需要二分的
*/
int get(int pos,int ln){
pos=idsam[pos];
for(int i=18;i>=0;--i)if(len[f[pos][i]]>=ln)pos=f[pos][i];
return pos;
}
struct node {
int l,r,sl,sr;
char st;
}q[N];
bool exs[N];
string ans[N];
bool ed;
signed main(){
// cout<<((&ed)-((&st)))/1024.0/1024.0<<"\n";
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
string s;cin>>s;s="{"+s;n1=s.size();
cin>>m;int tag=0;
for(int i=1;i<=m;i++){
cin>>q[i].l>>q[i].r;q[i].l++,q[i].r++;
string t;cin>>t;tag^=1;q[i].sl=s.size()+1;s+=t;q[i].sr=s.size();q[i].st=t[0];
}
s+=tag?"{":"|";
n=s.size();
for(int i=1;i<=n1;i++){
if(s[i]=='|'||s[i]=='{')val[i]=0;
else val[i]=(1<<s[i]-'a');
}
for(int i=0;i<n;i++)insert(s[i]-'a',i+1);
// for(int i=1;i<=num;i++)cout<<len[i]<<" ";cout<<"\n";
build();dfs(1,0);
for(int i=1;i<=m;i++){
int a=q[i].l,b=q[i].r,c=q[i].sl,d=q[i].sr;int tag=0;
// cout<<"!!: "<<a<<" "<<b<<" "<<c<<" "<<d<<"\n";
for(int j=d;j>=c;--j){
/*
找到S[a,b-1]中与[c,j]匹配,然后最后一位比S[j+1]大
*/
int pos=get(j,j-c+1);//S[c,j]
int ql=a+j-c,qr=b-1;
if(ql>qr)continue;
pr now=gsum(rt[pos],1,n1,ql,qr);
if(now.first&&(j!=d?now.second>=(1<<(s[j]-'a')+1):now.second)){
/*有解!*/
/*
考虑如何输出方案
我们其实可以二分这个起始位置
因为长度是确定的
你是什么煞笔
TMD答案是确定的
*/
for(int k=c;k<=j;k++)cout<<s[k-1];
int k=now.second;
for(int i=(j==d?0:s[j]-'a'+1);i<26;i++){
if((now.second>>i)&1){
char x=i+'a';
cout<<x<<"\n";break;
}
}
tag=1;
break;
}
}
if(!tag){
pr now=gsum(rt[1],1,n1,a-1,b-1);
if(now.second>=(1<<s[c-1]-'a'+1)){
for(int i=s[c-1]-'a'+1;i<26;i++)if((now.second>>i)&1){
char x=i+'a';
cout<<x<<"\n";break;
}
}
else cout<<"-1\n";
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
2023-02-02 树链剖分习题集
2023-02-02 树链剖分入门