后缀排序SA 详解+模板

这里以洛谷P3809为例题。

题目描述

读入一个长度为 nn 的由大小写英文字母或数字组成的字符串,请把这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。位置编号为 11 到 nn 。

输入输出格式

输入格式:

一行一个长度为 nn 的仅包含大小写英文字母或数字的字符串。

输出格式:

一行,共n个整数,表示答案。

题解:

这里介绍的是倍增法(O(nlogn)),有兴趣的同学们可以去了解一下DC3法(O(n))。

后缀数组(SA)是啥?

所谓字符串的一个后缀就是从此字符串任意i位置一直到最末的字符为止所构成的一个新字符串。而题目要求的后缀数组即是让我们求出这个字符串的所有非空后缀按字典序从小到大排序,然后按顺序输出后缀的第一个字符在原串中的位置。

如何求解?

我们一般用常数小,方便写的倍增法解决。我们首先单独处理解决每个字符的排序rank值,然后把这一次的SA作为初始值,开始倍增操作。我们已经知道了i往后2^k这么长的字符串的排名,如何推得i往后2^(k+1)长度字符串的排名呢?也就是说这个字符串分为两段,我们知道前一段的排名和后一段的排名,以前一段的排名作为第一关键字,后一段的排名作为第二关键字,一遍排序后即可求出这一整段的排名了。

如图(原出处:https://blog.csdn.net/yxuanwkeith/article/details/50636898):



代码:

我在做这道题的时候其实遇到了一点小小的波折,第一次的代码总是TLE,死活卡不过去:

70分代码:

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int Maxn=1000005;
char a[Maxn];
int n,f[Maxn][25],rank[Maxn],tp1[Maxn],tp2[Maxn];
int sa[Maxn],t[Maxn],num1[Maxn];
int num2[Maxn],sum[Maxn],now[Maxn];
int main(){
    scanf("%s",a+1);
    n=strlen(a+1);
    for(int i=1;i<=n;i++){
        t[a[i]-'0'+1]++;
    }
    for(int i=1;i<=200;i++){
        t[i]+=t[i-1];
        sum[i]=t[i-1]+1;
    }
    for(int i=n;i>=1;i--){
        rank[i]=sum[a[i]-'0'+1];
        sa[t[a[i]-'0'+1]--]=i;
    }
    for(int j=1;(1<<j)<=n;j++){
        memset(t,0,sizeof(t));
        for(int i=1;i<=n;i++){
            tp1[i]=num1[i]=rank[i];
            num2[i]=rank[i+(1<<(j-1))];
        }
        for(int i=1;i<=n;i++)t[num2[i]]++;
        for(int i=1;i<=n;i++){t[i]+=t[i-1];sum[i]=t[i-1]+1;}
        for(int i=n;i>=1;i--){tp2[sa[i]]=rank[sa[i]]=sum[num2[sa[i]]];now[t[num2[sa[i]]]--]=sa[i];}
        for(int i=1;i<=n;i++){sa[i]=now[i];}
        memset(t,0,sizeof(t));
        for(int i=1;i<=n;i++)t[num1[i]]++;
        for(int i=1;i<=n;i++){t[i]+=t[i-1];sum[i]=t[i-1]+1;}
        for(int i=n;i>=1;i--){rank[sa[i]]=sum[num1[sa[i]]];now[t[num1[sa[i]]]--]=sa[i];}
        for(int i=1;i<=n;i++){sa[i]=now[i];}
        for(int i=1;i<=n;i++){
            if(tp1[sa[i]]==tp1[sa[i-1]]){
                if(tp2[sa[i]]==tp2[sa[i-1]])rank[sa[i]]=rank[sa[i-1]];
                else rank[sa[i]]=i;
            }
            else{
                rank[sa[i]]=i;
            }
        }
    }
    for(int i=1;i<=n;i++){
        printf("%d ",sa[i]);
    } 
    return 0;
}
后来在走投无路的时候,我报着死马当活马医的心态,做了一个小小的优化,就是统计一下
rank[sa[i]]=i

的次数,也就是如果每个位置的排名都已经没有重复的了,那么我们再往后搜也无法改变SA的结果了。

没想到居然,整整快了将近10s??

AC代码:

#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int Maxn=2000005;
char a[Maxn];
int n,f[Maxn][25],rank[Maxn],tp1[Maxn],tp2[Maxn];
int sa[Maxn],t[Maxn],num1[Maxn];
int num2[Maxn],sum[Maxn],now[Maxn],cnt;
int main(){
    scanf("%s",a+1);
    n=strlen(a+1);
    for(int i=1;i<=n;i++){
        t[a[i]-'0'+1]++;
    }
    for(int i=1;i<=200;i++){
        t[i]+=t[i-1];
        sum[i]=t[i-1]+1;
    }
    for(int i=n;i>=1;i--){
        rank[i]=sum[a[i]-'0'+1];
        sa[t[a[i]-'0'+1]--]=i;
    }
    for(int j=1;(1<<j)<=n;++j){
        for(int i=1;i<=n;++i){
            tp1[i]=num1[i]=rank[i];t[i]=0;
            num2[i]=rank[i+(1<<(j-1))];
        }
        for(int i=1;i<=n;++i)t[num2[i]]++;
        for(int i=1;i<=n;++i){t[i]+=t[i-1];sum[i]=t[i-1]+1;}
        for(int i=n;i>=1;--i){tp2[sa[i]]=rank[sa[i]]=sum[num2[sa[i]]];now[t[num2[sa[i]]]--]=sa[i];}
        for(int i=1;i<=n;++i){sa[i]=now[i];t[i]=0;}
        for(int i=1;i<=n;++i)t[num1[i]]++;
        for(int i=1;i<=n;++i){t[i]+=t[i-1];sum[i]=t[i-1]+1;}
        for(int i=n;i>=1;--i){rank[sa[i]]=sum[num1[sa[i]]];now[t[num1[sa[i]]]--]=sa[i];}
        for(int i=1;i<=n;++i){sa[i]=now[i];}
        cnt=0;
        for(int i=1;i<=n;++i){
            if(tp1[sa[i]]==tp1[sa[i-1]]){
                if(tp2[sa[i]]==tp2[sa[i-1]])rank[sa[i]]=rank[sa[i-1]];
                else rank[sa[i]]=i,cnt++;
            }
            else{
                rank[sa[i]]=i,cnt++;
            }
        }
        if(cnt>=n)break;
    }
    for(int i=1;i<=n;i++){
        printf("%d ",sa[i]);
    } 
    return 0;
}
于是我再一次感受到了玄学的魅力.........


  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
为了计算这个后缀表达式的值,我们需要使用栈来辅助计算。具体步骤如下: 1. 从左到右遍历后缀表达式中的每个元素。 2. 如果当前元素是一个数字,将其压入栈中。 3. 如果当前元素是一个运算符,弹出栈顶的两个数字,进行相应的运算,将运算结果压回栈中。 4. 遍历完后缀表达式后,栈中仅剩下一个数字,即为表达式的值。 根据上述算法,可以得到后缀表达式1 2 + 3 4 - / 5 6 * +的计算过程如下: 1. 遍历到元素1,将1压入栈中:栈[1] 2. 遍历到元素2,将2压入栈中:栈[1, 2] 3. 遍历到元素+,弹出栈顶的两个数字2和1,进行相加运算,将结果3压回栈中:栈[3] 4. 遍历到元素3,将3压入栈中:栈[3, 3] 5. 遍历到元素4,将4压入栈中:栈[3, 3, 4] 6. 遍历到元素-,弹出栈顶的两个数字4和3,进行相减运算,将结果1压回栈中:栈[3, 1] 7. 遍历到元素/,弹出栈顶的两个数字1和3,进行相除运算,将结果0压回栈中:栈[0] 8. 遍历到元素5,将5压入栈中:栈[0, 5] 9. 遍历到元素6,将6压入栈中:栈[0, 5, 6] 10. 遍历到元素*,弹出栈顶的两个数字6和5,进行相乘运算,将结果30压回栈中:栈[0, 30] 11. 遍历到元素+,弹出栈顶的两个数字30和0,进行相加运算,将结果30压回栈中:栈[30] 12. 遍历完后缀表达式,栈中仅剩下一个数字30,即为表达式的值。 因此,后缀表达式1 2 + 3 4 - / 5 6 * +的值为30。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值