2016-8-7夏令营总结(后缀数组+RMQ)

来源:互联网 发布:iphone网络不可用 编辑:程序博客网 时间:2024/06/01 10:57

8月7号,我们学习了字符串的后缀数组的知识。后缀数组是一种十分强大的算法,是一个把这个串的所有后缀排个序的数组。
后缀数组的代码量十分的大,可以分为几个部分:倍增算法、基数排序、求最长公共前缀与RMQ。

定义

首先要弄清楚,后缀数组是什么?
后缀数组就是给一个字符串的每一个后缀排个序的数组。(后缀就是一个字符串的某一位开头一直到结尾的子串)

倍增算法

我们先定义两个数组rank[]和sa[]。rank[]表示以第i位为开头的后缀排名为rank[i],sa[]表示排名为i的数组是以第sa[i]位为开头。
倍增算法就是通过求长度为1,2,4,8……2^k的子串的排序,从而得到整个后缀的排序。我们搜到长度为2^k的子串时,它的排名是由它的前面长度为2^k-1的排名和后面长度为2^k-1的排名决定的。因为字符串的比较是基数排序的方式(先比较最大位,最大位不同就能决定两个字符串的大小关系,如果相同比较第二大位……以此类推),所以两个字符串比较大小时,先比较前面2^k-1位的大小关系,前面大的肯定就大,然后再比较后面的。我们把这前后两个排名组成一个二元组,然后就是对这一堆二元组进行双关键字排序了。

基数排序

我们要怎么进行双关键字排序呢?用快排吗?就要o(nlogn)的时间了。考虑到只有两个关键字,所以就会想到了基数排序。我们先对第二个元素进行排序。但是我们发现sa存的就是每一个2^k-1的排名的位置,所以我们可以通过sa数组算出每个二元的在原字符串是第几位为开头的。那么我们就可以算出第一元是以第几位为开头的。然后第一元就直接通过基数排序来得到总排名。然后把rank与sa数组更新。

那么我们的倍增算法+基数排序的过程什么时候结束呢?只要当我们把所有某个长度的子串排序后,它们的排名没有相同的时候,当前排名就是最后的排名了。因为这时候每个后缀的前2^k位的子串的排名已经求出来了,并且没有重复,所以这个前2^k位排名就是最后的排名。

最长公共前缀

那么我们能用后缀数组来做什么呢?最常用的应用应该就是求一个字符串S的每一个后缀之间的最长公共前缀了。我们要怎么求这些最长公共前缀呢?
看看下面的图:
这里写图片描述
要是我们想要求aaaab与aabaaaab的最长公共前缀的话,我们不难发现,其实这个前缀是这两个后缀在排过序后他们两个之间所有后缀之间的最长公共前缀。如果真的是这样,那么我们就可以定义一个len[]数组,表示位置为i的后缀在排序后与它上一个的最长公共前缀长度,然后求出每一个后缀的len。然后我们就可以在这要求的两个后缀在排过序后之间的后缀的len的最小值,这个这个最小值就是我们要求的最长公共前缀的最小值。但是我们怎么证明这个最小值就是答案呢?
这里写图片描述
如图就是证明(较为简略简单与简陋)
那么我们只要预处理每个后缀的len,剩下的就是求指定的两个后缀之间的最小值就可以了,而这个问题就可以转化为我们的RMQ问题。
但是我们求每个后缀的len又有一个问题了——我们怎么求这个len。直接每一个后缀都暴力搜吗?
又再看回这个图:
这里写图片描述
我们求出两个后缀A,B的最长公共前缀长度(A在上面B在下面),记为x后。将这两个后缀同时去掉第一位后,A变成E,B变成C,那么C与E的最长公共前缀肯定就是x-1。那么由上面证明的可以知道,这个C与E之间的后缀的len的最小值就是x-1。那么C的len就是至少是x-1了。然后再从第x位往后面比较,一直到不相同才停,然后记作新的x,继续执行上面的操作……
我们发现,如果这样的话,那么从字符串的第1个为开头的后缀开始算(因为这个后缀不是某个后缀去掉开头)的话,那么没去掉一位其实就是第2位为开头,再去掉以为就是第3位为开头……以此类推。这样就节省了很多重复的操作。

RMQ

那么我们怎么来求两个后缀之间的len的最小值呢?这个就能用到RMQ算法了。
RMq是用来求区间最值的,它是类似于倍增算法,先求相邻两个的最小值,然后通过这个最小值求相邻4个最小值,然后8个,16个……
这里写图片描述
比如这个图中第1-2个{3,5}的最小值是3,第3-4个{9,4}的最小值是4,那么第1-4个{3,5,9,4}的最小值就是3和4的最小值。那么通过这个倍增算法,我们建一个RMQ就可以用o(nlogn)的时间完成。
乳沟我们要求第2-4个的最小值,那么我们就求第2-3和第3-4的最小值的最小值。询问就只用o(1)的时间了。
一些公式:
rmq[i][j]就是表示第i-(i+2^j-1)个的最小值。
我们询问第i个到第j个的最小值的话,我们可以先定义一个k=log2(j-i+1).那么就可以球min(rmq[i][k],rmq[i][j-(2^k)+1]);
这里RMQ讲的比较简略,不懂可以看看网上的博客。

时间复杂度

倍增算法与基数排序o(nlogn)
求最长公共前缀的len约为o(n)
RMQ 创建o(nlogn),询问o(1)
所以时间复杂度是o(nlogn)级别的

参考程序

无注释

#include <iostream>#include <fstream>#include <algorithm>#include <cstring>#include <cmath>using namespace std;const int maxN=1000007;int n;char s[maxN];int x[maxN]; int rank[maxN],sa[maxN];int sum[maxN],px[maxN];int r[maxN];int rnum=0;int len[maxN];int rmq[maxN][20];void getrank(){    memset(sum,0,sizeof(sum));    for(int i=0;i<n;i++) x[i]=s[i]-'a';    for(int i=0;i<n;i++) sum[x[i]]++;    for(int i=1;i<26;i++) sum[i]+=sum[i-1];    for(int i=n-1;i>=0;i--) sa[--sum[x[i]]]=i;    for(int i=0;i<n-1;i++)    {        while(x[sa[i]]==x[sa[i+1]] && i<n-1)            rank[sa[i]]=rnum,i++;        rank[sa[i]]=rnum++;    }    int pxnum;    for(int p=1;rnum<n-1;p<<=1)    {        memset(sum,0,sizeof(sum));        pxnum=0;        for(int i=n-p;i<n;i++) px[pxnum++]=i;        for(int i=0;i<n;i++)            if(sa[i]>=p) px[pxnum++]=sa[i]-p;        for(int i=0;i<n;i++) sum[rank[i]]++;        for(int i=1;i<rnum;i++) sum[i]+=sum[i-1];        for(int i=pxnum-1;i>=0;i--) sa[--sum[rank[px[i]]]]=px[i];        for(int i=0;i<n;i++) r[i]=rank[i];        r[n]=-1;        rnum=0;        for(int i=0;i<n;i++)        {            while(r[min(sa[i],n)]==r[min(sa[i+1],n)] &&\                  r[min(sa[i]+p,n)]==r[min(sa[i+1]+p,n)] &&\                  i<n-1)                rank[sa[i]]=rnum,i++;            rank[sa[i]]=rnum++;        }    }}void getlen(){    int k=0;    for(int i=0;i<n;i++)    {        if(rank[i]==0)        {            len[i]=0;            k=0;            continue;        }        if(k>0) k--;        int t=sa[rank[i]-1];        while(s[i+k]==s[t+k] && i+k<n && t+k<n) k++;        len[i]=k;    }}void getRMQ(){    for(int i=0;i<n;i++) rmq[i][0]=len[sa[i]];    for(int j=1;j<19;j++)        for(int i=0;i<n;i++)            if((i+1<<(j-1))<n)                rmq[i][j]=min(rmq[i][j-1],rmq[i+1<<(j-1)][j-1]);}int askRMQ(int i,int j){    if(i==j) return (n-i);    i=rank[i],j=rank[j];     if(i>j) swap(i,j);    i++;    int k=log2(j-i+1);    return min(rmq[i][k],rmq[j-(1<<k)+1][k]);}int main(){    scanf("%s",s);    n=strlen(s);    getrank();    getlen();    getRMQ();    int a,b;    scanf("%d%d",&a,&b);    printf("%d\n",askRMQ(a-1,b-1));    return 0;}
0 0