题解:SP12713 STRSOCU - Strings

题解

后缀数组。先不考虑“本质不同”这一条件。

求整个串有多少个子串满足条件,等价于对每个后缀求出,它有多少个前缀满足条件。

那么我们可以考虑单个后缀,把这个串拉出来,统计他的合法前缀个数。

前缀有互相包含的关系,所以大前缀在 \(t\) 中的出现次数一定会覆盖小前缀,即\(t\) 中出现的次数随长度增加而减少,所以出现次数恰好为 \(k\) 的前缀一定是一段区间,根据单调性二分出左右端点即可。


所以问题转化为,对于某个特定后缀的某个特定前缀,求它在 \(t\) 中的出现次数。这就可以用后缀数组解决了,把 \(s\)\(t\) 拼接起来,后缀排序,求出 \(height\) 数组。

我们知道,如果一段连续的区间 \([l,r]\) 满足 \(\min _{k \in [l+1,r]} height_k \geq x\),那么排名在 \([l,r]\) 之间的后缀至少有一个长度为 \(x\) 的公共前缀。换句话说,这个公共前缀在这边至少出现了 \(r-l+1\) 次,在字符串 \(t\)至少出现了 \(num(l,r)\) 次,\(num(l,r)\) 表示在排名为 \([l,r]\) 的后缀中,属于 \(t\) 的后缀个数。

如果是求恰好出现次数呢?只需要求出极长区间 \([l,r]\) 使得 \(p \in [l,r]\)\(height\) 的区间最小值至少为 \(x\)\(x\) 为查询的子串长度),这个极长区间中拼接前属于 \(t\) 的后缀个数,就是查询串恰好出现的次数。

极长区间就是指 \([l,r]\) 满足条件,但 \(l-1\)\(r+1\) 加入任何一个都不满足条件,求极长区间可以二分或者倍增。


那怎么求“本质不同”的子串数目呢?非常简单,我们枚举每个后缀的时候,有些比较短的前缀可能是被算过的,对于后缀 \(i\),左边界就应该至少是 \(height_{rk_i}+1\),因为前面 \(height_{rk_i}\) 的那些前缀已经被别的后缀算过了。


现在这个题就做完了,枚举每个后缀 \(O(n)\),对每个后缀二分左右边界 \(O(\log n)\),二分的 check 函数求恰好出现的次数 \(O(\log n)\),总时间复杂度 \(O(n \log^2 n)\)

代码

#include<bits/stdc++.h>
#define For(i,il,ir) for(int i=(il);i<=(ir);++i)
#define Rof(i,ir,il) for(int i=(ir);i>=(il);--i)
using namespace std;
const int maxn=16005;

string s,t;
int lg[maxn];
int lens,lent;

int n,K,m;
int ct[maxn],a[maxn],ork[maxn<<1];
int sa[maxn],rk[maxn<<1],hgt[maxn];
void Sort(int op){
    For(i,0,m) ct[i]=0;
    For(i,1,n) a[i]=sa[i],ct[rk[a[i]+op]]++;
    For(i,1,m) ct[i]+=ct[i-1];
    Rof(i,n,1) sa[ct[rk[a[i]+op]]--]=a[i];
}
void SA(){ // SA 板子
    m=128;
    For(i,1,n) sa[i]=i,rk[i]=(int)s[i];
    for(int len=1;len<n;len<<=1)
    {
        Sort(len),Sort(0);
        For(i,1,(n<<1)) ork[i]=rk[i]; m=0;
        For(i,1,n)
            if(i && ork[sa[i]]==ork[sa[i-1]] && ork[sa[i]+len]==ork[sa[i-1]+len]) rk[sa[i]]=m;
            else rk[sa[i]]=++m;
        if(m==n) break;
    }
    int k=0;
    For(i,1,n)
        if(rk[i]==1) hgt[rk[i]]=k=0;
        else{
			if(k) --k;
            int j=sa[rk[i]-1];
            while(s[i+k]==s[j+k]) ++k;
            hgt[rk[i]]=k;
        }
}
int st[maxn][18];//15
int lsts[maxn],sumt[maxn];
int query(int l,int r){ // RMQ 求 height 数组区间 min
    if(++l>r) return n-sa[l]+1;
    int p=lg[r-l+1];
    return min(st[l][p],st[r-(1<<p)+1][p]);
}
int check(int id,int len){ //求排名为 id 的后缀,它的长度为 len 的前缀,在 t 中的出现次数。
    int l=id,r=id;
    Rof(i,15,0){
        if(l-(1<<i)>=1 && query(l-(1<<i),id)>=len) l-=(1<<i);
        if(r+(1<<i)<=n && query(id,r+(1<<i))>=len) r+=(1<<i);
    }
    return sumt[r]-sumt[l-1]; // sumt 表示属于 t 的后缀个数(如果这个后缀是 s 的,那就不应该被算进去)
}
int calcl(int pos){ //二分左边界
    int L=1,R=n-pos+1,ans=R+1;
    while(L<=R){
        int M=L+R>>1;
        int tmp=check(rk[pos],M);
        if(tmp>K) L=M+1;
        else if(tmp==K) ans=M,R=M-1;
        else R=M-1;
    }
    return ans;
}
int calcr(int pos){ //二分右边界
    int L=1,R=n-pos+1,ans=0;
    while(L<=R){
        int M=L+R>>1;
        int tmp=check(rk[pos],M);
        if(tmp>K) L=M+1;
        else if(tmp==K) ans=M,L=M+1;
        else R=M-1;
    }
    return ans;
}
void solve()
{
    cin>>s>>t>>K;
    lens=s.size(),lent=t.size();
    s=' '+t+'#'+s,n=lent+lens+1;
    SA();

    For(i,1,n){
        lsts[i]=(sa[i]>=lent+2)?i:lsts[i-1];
        sumt[i]=sumt[i-1]+(sa[i]<=lent);
    }
    For(i,1,n) st[i][0]=hgt[i];
    For(j,1,15) For(i,1,n-(1<<j)+1) st[i][j]=min(st[i][j-1],st[i+(1<<j-1)][j-1]);

    int res=0;
    For(i,lent+2,n){
        int pre=lsts[rk[i]-1];
        int lcp=pre?query(pre,rk[i]):0;
        int L=max(calcl(i),lcp+1),R=calcr(i);
        if(L<=R) res+=(R-L+1);
    }
    printf("%d\n",res);
}
void clear(){
    For(i,0,n+1) hgt[i]=ct[i]=0;
    For(i,0,(n<<1)) rk[i]=ork[i]=0;
}
signed main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
    For(i,2,16000) lg[i]=lg[i>>1]+1;
    int T;cin>>T; 
    For(tt,1,T){
        printf("Case #%d:\n",tt);
        solve(),clear();
    }
    return 0;
}
posted @ 2025-05-07 22:49  wanggk  阅读(13)  评论(0)    收藏  举报