【HAOI2016/BZOJ4566】找相同字符 后缀数组+单调栈

本文介绍一种利用单调栈解决两个字符串公共子串数量问题的方法。通过连接两个字符串并插入分隔符,将问题转化为求分隔符前后出现过的子串个数。采用后缀数组和单调栈实现高效计算。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原题走这里

鉴于我实在不是很懂单调栈和单调队列这一系列东西,所以我决定稍微具体讲一下单调栈。

恩,本题实质上就是求两个字符串的公共子串数,其中只要出现位置不同,就算是不同的子串。
处理多个字符串的经典套路:把两个字符串连在一起,中间用分割符分割。
于是问题就转化为了:求分隔符前后都出现过的子串个数。
子串就是后缀的前缀,于是问题又转化成了:求整个串中,任意两个后缀的LCP之和,这两个子串要一个在分割符前,一个在分割符后。

恩,求后缀的LCP我们可以用后缀数组中的height数组,两个后缀的LCP长度就是height上的区间最小值。
于是我们要求的实际上就是:

SA[i]<n+1<SA[j]minikjheight[k]+SA[j]<n+1<SA[i]minikjheight[k] ∑ S A [ i ] < n + 1 < S A [ j ] min i ≤ k ≤ j h e i g h t [ k ] + ∑ S A [ j ] < n + 1 < S A [ i ] min i ≤ k ≤ j h e i g h t [ k ]

其中SA是排名第 i i 的后缀的编号。
让我们姑且只考虑左边,一个明显的暴力解法是枚举i,j然后RMQ求出区间最小值。
然而这个解法明显不符合时间复杂度的要求,因此我们的方法有待改进。

首先我们发现,右端点固定的一系列区间最小值,能够构成一个单调增的序列,其中相同的值可以合并。
如果我们不断将右端点右移,则序列末端的一些值会被更新为同一个值,可以将这几个值合并,相当于这几个值被从队列末端弹掉了。
与此同时如果你又扫到了某一个 SA[i]<n+1 S A [ i ] < n + 1 i i ,你又要把它加进序列的末端。
这相当于一个栈的结构,而其中的元素又是单调的,就是所谓的单调栈。

于是,我们可以使用单调栈,求出式子的值即可,时间复杂度为O(n),加上构造后缀数组,共 O(nlogn) O ( n l o g n )

诸君,我讨厌单调栈。

讲真,一开始,我的解法是二分,然后二分合并左右两边结果的时候用的又是单调栈,然后还AC了,后来看题解才发现,只用单调栈就可以了。

具体详见代码如下:

// luogu-judger-enable-o2
#include <bits/stdc++.h>
#define LL long long
using namespace std;
int n,len,SA[400010],r[400010],tp[400010],t[400010],h[400010],st[400010],s[400010],top;
char ch[400010];
void Sort(int m) {
    memset(t,0,sizeof(int)*(m+1));
    for(int i=1; i<=len; i++) {
        t[r[i]]++;
    }
    for(int i=1; i<=m; i++) {
        t[i]+=t[i-1];
    }
    for(int i=len; i; i--) {
        SA[t[r[tp[i]]]--]=tp[i];
    }
}
void build_SA() {
    Sort(127);
    for(int i=1,p=0,m=127; i<=len&&p<len; i<<=1,m=p) {
        p=0; 
        for(int j=len-i+1; j<=len; j++) {
            tp[++p]=j;
        }
        for(int j=1; j<=len; j++) {
            if(SA[j]>i) {
                tp[++p]=SA[j]-i;
            }
        }
        Sort(m);
        swap(r,tp);
        r[SA[1]]=p=1;
        for(int j=2; j<=len; j++) {
            r[SA[j]]=((tp[SA[j]]==tp[SA[j-1]])&&(tp[SA[j]+i]==tp[SA[j-1]+i])?p:++p);
        }
    }
    for(int i=1,j,k=0; i<=len; h[r[i++]]=k) {
        for(k?k--:0,j=SA[r[i]-1]; ch[i+k]==ch[j+k]; k++);
    }
}
LL solve(bool b) {
    LL ret=0,temp=0;
    top=0; 
    for(int i=len,j=0; i; i--,j=0) {
        if((SA[i]>n)^b){
            ret+=temp;
        }
        while(st[top]>h[i]&&top)
        {
            temp-=1LL*s[top]*st[top];
            j+=s[top--];
        }
        st[++top]=h[i];
        s[top]=j;
        if((SA[i]<=n)^b) { //向单调栈内插入元素
            s[top]++;
        } 
        temp+=1LL*s[top]*st[top];
    }
    return ret;
}
int main() {
    scanf("%s",ch+1);
    n=strlen(ch+1);
    ch[n+1]='$';
    scanf("%s",ch+n+2);
    len=strlen(ch+1);
    for(int i=1;i<=len;i++)
    {
        tp[i]=i;
        r[i]=ch[i];
    }
    build_SA();
    cout<<solve(0)+solve(1)<<endl;
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值