[POJ3415]Common Substrings(后缀数组+单调栈)

本文介绍了一种高效的字符串匹配算法,用于寻找两个字符串之间的公共子串。该算法通过构建特殊的数据结构并利用单调栈来优化计算过程,实现了O(n)的时间复杂度。

题目:

我是超链接

题意:

给定两个字符串 A 和 B ,求长度不小于 k 的公共子串的个数(可以相同)。

题解:

首先我们知道:公共子串就是计算A的某个后缀与B的某个后缀的最长公共前缀,如果长度L大于k,则加上L-k+1组。
我们先回忆一下求最长公共前缀的思路:height区间取min值
那么我们有一个暴力的思路:
将两个字符串连接起来,中间用一个没有出现的字符分开。
然后通过height数组分组,某个组内的height都是大于等于k的,也就是任意两个后缀的最长公共前缀都至少为k。
扫描一遍,遇到一个B的后缀就与之前的A后缀进行统计,求出所有的满足的组数;然后再反过来:遇到A的后缀就和前面的B后缀进行统计,这样合起来就是全部的答案,但是这样的做法是 O(n2) 的。

可以发现两个后缀的最长公共前缀为这一段的height值的最小值。
可以通过递增的单调栈维护一下【当前height值】和【“被卡在这一层”即和后面的height值取min为当前height值的元素个数】,逐渐合并,均摊可以达到 O(n) 的复杂度。
我们要统计个数的时候,先把目前的height-k+1加上,如果要出栈的话,意味着height值要变小了,那我们公共子串的长度就减小了,那么这一层所有个数的贡献值都小了,全都要减
然后和上面相同扫描两遍即可

注意:
我们平常模板里的n都是不能用上的,相当于虚位,所以在这种n要被循环到的时候,n++
tot和sum都要开longlong

代码:

#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N=200005;
int c[N],height[N],rank[N],sa[N],y[N],x[N],m,n,stack[N][2];
char st[N],s[N];
void build_sa()
{
    m=300;
    for (int i=0;i<m;i++) c[i]=0;
    for (int i=0;i<n;i++) c[x[i]=s[i]]++;
    for (int i=1;i<m;i++) c[i]+=c[i-1];
    for (int i=n-1;i>=0;i--) sa[--c[x[i]]]=i;

    for (int k=1;k<=n;k<<=1)
    {
        int p=0;
        for (int i=n-k;i<n;i++) y[p++]=i;
        for (int i=0;i<n;i++) if (sa[i]>=k) y[p++]=sa[i]-k;

        for (int i=0;i<m;i++) c[i]=0;
        for (int i=0;i<n;i++) c[x[y[i]]]++;
        for (int i=1;i<m;i++) c[i]+=c[i-1];
        for (int i=n-1;i>=0;i--) sa[--c[x[y[i]]]]=y[i];     

        swap(x,y);p=1;x[sa[0]]=0;
        for (int i=1;i<n;i++) x[sa[i]]=y[sa[i]]==y[sa[i-1]] && ((sa[i]+k>=n?-1:y[sa[i]+k])==(sa[i-1]+k>=n?-1:y[sa[i-1]+k]))?p-1:p++;
        if (p>n) break;
        m=p;
    }
}
void build_height()
{
    for (int i=0;i<n;i++) rank[sa[i]]=i;
    int k=0;height[0]=0;
    for (int i=0;i<n;i++)
    {
        if (!rank[i]) continue;
        if (k) k--;
        int j=sa[rank[i]-1];
        while (i+k<n && j+k<n && s[i+k]==s[j+k]) k++;
        height[rank[i]]=k;
    }
}
int main()
{
    int k,nn;
    while (scanf("%d",&k) && k)
    {
        scanf("%s",s); n=strlen(s);
        scanf("%s",st); nn=strlen(st);
        s[n]='&';int l=n;
        for (int i=0;i<nn;i++) s[++n]=st[i];
        s[++n]='\0';
        build_sa();
        build_height();
        long long sum=0,tot=0;
        int top=0;
        for (int i=0;i<n;i++)
          if (height[i]<k) tot=top=0;
          else 
          {
            int cnt=0;
            if (sa[i-1]<l) cnt++,tot+=height[i]-k+1;
            while (top>0 && height[i]<=stack[top][0])//0为height 1为包含A的个数 
            {
                tot-=stack[top][1]*(stack[top][0]-height[i]);//height想要变小 
                cnt+=stack[top][1];
                top--;
            }
            stack[++top][0]=height[i]; stack[top][1]=cnt;
            if (sa[i]>l) sum+=tot;
          }

        top=0,tot=0;
        for (int i=0;i<n;i++)
          if (height[i]<k) tot=top=0;
          else 
          {
            int cnt=0;
            if (sa[i-1]>l) cnt++,tot+=height[i]-k+1;
            while (top>0 && height[i]<=stack[top][0])
            {
                tot-=stack[top][1]*(stack[top][0]-height[i]);
                cnt+=stack[top][1];
                top--;
            }
            stack[++top][0]=height[i]; stack[top][1]=cnt;
            if (sa[i]<l) sum+=tot;
          }
        printf("%lld\n",sum);
    }
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值