地对空导弹(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:点数上界 2n1,边数上界 3n4
  • lcs(i,j)=len(lca(id[i],id[j])),即前缀 i,j最长公共后缀是两个前缀在 link 树上的 lca 的长度
  • 子串出现次数:这个串在 sam 上的位置所代表的 endpos 大小。
  • 每一个前缀是 endpos 集合里的元素,因此统计大小时这些节点初始化为1。
  • 判断子串:在 SAM 上直接走
  • 本质不同子串数:len[i]len[link(i)]
  • 构造非法串:建立虚点表示非法,将sam上每个位置的空余字符指向这个虚点。这样也可以求出最小非法串长度——DP即可。
  • SAM 是一张DAG,link 是一棵树
  • endpos 集合的具体数字信息——利用线段树合并维护,也可以启发式合并之类的。
  • 最小循环表示:建立 SS,从根开始贪心走小的字符走 n 步即可。
  • 字典序第 k 小子串。核心思路是求出 SAM 上走到每个点之后还能走出 sizi 个点。然后类似平衡树进行查找。
  • 要求本质不同:则直接初始化 sizi=1,DAG上直接记忆化搜索
  • 不要求本质不同,则需要初始化为 |endpos(i)|,这里是因为走到这里有这么多个相同的位置。
  • 最长公共子串(多个串版本)

选出最短的串建立SAM,剩下的串在SAM上游走,每个串记录一下走到SAM上各个节点时匹配长度的最大值 tmp[i],具体的我们用两个指针 now,mxlen 表示当前匹配到节点 now,长度为 mxlen,则我们在匹配下一个字符 c 的时候不断跳 link 直到 ch[now][c] 存在,跳的时候不断令 mxlen=len[now],最后令 now=ch[now][c],mxlen+1mxlen。最后还需要跑一次 dfs(link-tree),来对子树的 tmp 取得 max,但需要注意对 len[i] 取较小值。最后把每一个串的 tmp 取最小值就是在这个点的最长公共匹配长度。模板以下。


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 过程中维护 fir,满足
  • np:fir[np]=len[np]1
  • now:fir[now]=fir[q]
  • 查询 P 时直接搜到对应状态输出 fir[pos]|P|+1
  • 查找 S[l,r] 对应位置:跳 idrlinktree 上的祖先,跳到第一个 lenrl+1 的点。

这个位置是第一个表出 [l,r] 的位置,这是SAM的后缀性质决定的。它的 endpos 集合里的位置都是 S[l,r] 这个串的结尾位置,因此这个子树里所有的位置与其可以匹配。

应用

封印

给定串 S,T,多次询问 S[l,r]T 的最长公共子串。

核心:

T 建立 SAM,求出 fi 表示以 i 结尾的 S 字符串与 T 的最长匹配长度。

则询问 (l,r) 的答案是 maxi[l,r]min(il+1,fi)

显然求出 f 后 ST 表上二分即可。

f 的求解很简单,将 S 放在SAM上跑,就像做最长公共子串那样做,处理完 S[1,i] 后的 len 就是 fi

Cyclical Quest

给定 S,多次询问,每次给定一个 T,求 S 中有多少个子串与 T 循环同构。

字符串

给定 S,多次询问 a,b,c,d,求 S[a,b] 的子串与 S[c,d] 的最大 LCP 长度。

考虑二分答案 mid,问题即化为求解 S[c,c+mid1]S[a,b] 中是否出现过。

这等价于我们找到 S[c,c+mid1]linktree 第一次出现的位置,查看那个点的 endpos 集合是否存在有在 [a+mid1,b] 中的元素。

我们考虑写一个可持久化版本的线段树合并,类似于主席树。利用倍增找到这个位置,然后用线段树区间求和即可。思路很简单。

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

给定 S,求最大的 k,满足存在 l,r,使得 i[0,k1],j[0,rl+1],S[l+j]=S[l+(rl+1)i+j],也就是连续重复 k 次。

枚举答案长度 len,选取 k·len,(k+1)·len 作为一组划分点,那么答案就是 (pre(l,r)+suf(l,r)1+len1)/len。这个 pre,suf 可以使用 SAM 正反插然后借助 lca 进行求解。

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

给定 S,多次询问,每次给定 L,R,T,从 S[L,R] 中选出字典序最小的子串 S1,满足 S1 的字典序大于 T。如果无解报告 -1

显然答案 S[x,y] 满足 S[x,y1]T 的某一个前缀,且 S[y]>T[yx+1]

不妨枚举 yx+1,显然最长的那个解最优,则问题化为判断 S[l,r1] 中是否存在一个串等于 T[1,k1],且这个串最后一个字符比 T[k] 大。

注意到这个问题其实和第四题有一定相似性,如果我们将 T 按顺序接在 S 后面。然后同样利用线段树合并求出每个 endpos 集合,注意这里只关心 S 里的元素。

其实也就是在匹配的基础上外加了一个对串尾的后继字符的一个大小限制。

但是这是容易的。其实我们不妨给每个字符设定权值为 2xa,然后求解区间和的同时求解一个 or 和,这样就可以判断是否有解了。

同时答案的拼凑也是相当简单的,具体细节见代码。

#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";
        }
    }
}
posted @   spdarkle  阅读(3)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
历史上的今天:
2023-02-02 树链剖分习题集
2023-02-02 树链剖分入门
点击右上角即可分享
微信分享提示