后缀数组 【TJOI2013】 bzoj3172 单词

21 篇文章 0 订阅
4 篇文章 0 订阅

题目大意:
给定一些单词,这些单词组成一篇文章(单词与单词之间有空格),问每个单词在这篇论文中出现多少次。

题目分析:
可以再单词与单词之间插分隔符,然后用后缀数组来排序,但是排完也没有什么用,并不能用这个序列来做什么。
但是我们发现,前缀相同的后缀一定被排到一块了,所以我们可以利用这个性质来解决问题。
引入一个数组height,height[i]代表排名为i的后缀与排名为i-1的后缀的最长公共前缀。
这个数组又一个性质:height[rnk[i]]>=height[rnk[i-1]]-1

简单的证明一下:
设k为排名为后缀i的排名的前一位的后缀,即k=sa[rnk[i-1]];
设j为k和i的最长公共前缀,即j=height[rnk[i]];
那么我们把k和i的第一位去掉,变成k+1和i+1,那么第i+1和k+1至少有j-1这么长的公共前缀,即height[rnk[i+1]]>=j-1 ;
即height[rnk[i+1]]>=height[rnk[i]]-1;
也就是上面说的height[rnk[i]]>=height[rnk[i-1]]-1。

有了这个性质之后,我们就可以再O(n)的时间内处理出height数组。
一个串的子串都可以表示成这个串的后缀的前缀的形式。我们可以找出以每个单词的首字母为起点的后缀再rnk数组中的位置,这个位置前后的一些后缀会和该后缀有公共前缀(LCP),那么这个公共前缀长度不小于该单词的长度,就说明有该单词被这个后缀包含。

所以问题就转变成了再height数组中以某一个位置为中心,向前后扩展,直到有一个LCP的长度小于单词长度,计算这个区间的长度输出答案。
正解应该是用单调栈维护左最小,右最小,然后每次二分查找。
但是bzoj的数据比较水,暴力查找也能过(这道题我主要练习后缀数组,而且比较懒,就没有写单调栈)

注意事项:没有什么可特别注意的,背好模板,别打错变量。
代码如下:

#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<string>
#include<cstring>
#include<iostream>
#define N 1200000
using namespace std;
int n,tot;
char c[N],s[N];
int sa[N],X[N],Y[N],sum[N],rnk[N],tmp[N],t,hi[N],ans[300],len[300];
void get_rank()
{
    for(int i=1;i<=tot;i++) sum[s[i]]++;
    for(int i=1;i<=127;i++) sum[i]+=sum[i-1];
    for(int i=tot;i>=1;i--) tmp[sum[s[i]]--]=i;
    for(int i=1;i<=tot;i++)
    {
        if(i==1 || s[tmp[i]]!=s[tmp[i-1]]) t++;
        rnk[tmp[i]]=t;
    }
}
void radix_sort(int key[],int order[])
{
    for(int i=0;i<=tot;i++) sum[i]=0;
    for(int i=1;i<=tot;i++) sum[key[i]]++;
    for(int i=1;i<=tot;i++) sum[i]+=sum[i-1];
    for(int i=tot;i>=1;i--) tmp[sum[key[order[i]]]--]=order[i];
    for(int i=1;i<=tot;i++) order[i]=tmp[i];
}
void get_height()
{
    for(int i=1;i<=tot;i++)
    {
        if(rnk[i]==1) continue;
        int j=max(hi[rnk[i-1]]-1,0);int k=sa[rnk[i]-1];
        while(s[i+j]==s[k+j]) j++;
        hi[rnk[i]]=j;
    }
}
void prefix_array()
{
    get_rank();
    for(int j=1;j<=tot;j<<=1)
    {
        for(int i=1;i<=tot;i++)
        {
            X[i]=rnk[i];
            Y[i]=i+j>tot?0:rnk[i+j];
            sa[i]=i;
        }
        radix_sort(Y,sa);
        radix_sort(X,sa);
        t=0;
        for(int i=1;i<=tot;i++)
        {
            if(i==1 || X[sa[i]]!=X[sa[i-1]] || Y[sa[i]]!=Y[sa[i-1]]) t++;
            rnk[sa[i]]=t;
        }
    }
    get_height();
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%s",c);
        ans[i]=tot+1;len[i]=strlen(c);
        for(int j=0;c[j];j++) s[++tot]=c[j];
        s[++tot]=' ';
    }
    s[tot--]='\0';
    prefix_array();
    for(int i=1;i<=n;i++)
    {
        int r=rnk[ans[i]]+1;
        while(r<=tot && hi[r]>=len[i]) r++;
        r--;
        int l=rnk[ans[i]];
        while(l>=1 && hi[l]>=len[i]) l--;
        l++;
        printf("%d\n",r-l+2);
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值