后缀数组

先贴一点字符串相关算法好了。

学习资料:

http://wenku.baidu.com/link?url=0UsXCx1NUs5Sqpv9c8lx7ewHDMacm7WfjPtdwyqsa3z0NV_m6p_3NUE5h6ben0KCf_zxrQ9TEXkCrHkkWjkPpzALolLqhMJwBcKgFAGKCwy

《后缀数组——处理字符串的有力工具》罗穗骞

http://cxjyxx.me/?p=240

http://cxjyxx.me/?p=241

这是cxjyxx_me神犇的小结

其实上面的材料都说得很清楚了…为了复(骗)习(访)算(问)法(量)就写一写好了。

先说后缀数组本体,我们假设字符串叫s,长度为len,字符串从0开始存储。

首先我们先定义suffix(p)=s[p]…s[len-1]。

我们定义sa和rank。sa是一个0~n-1的排列满足suffix(sa[0])<suffix(sa[1])<…<suffix(sa[n-1]),就是n个后缀从小到大排序后的开头顺序排成一个序列。rank[i]保存的是suffix(i)从小到大的名次(即sa[rank[i]]=i)。

后缀数组是存在O(n)的做法的(当然比O(n)更低复杂度的做法并没有什么卵用),但是太难写了,所以在oi中更常用的是O(nlogn)甚至O(nlog^2n)的。

log?我们考虑倍增。

对于第i次比较,我们只考虑s[x]…s[min(x+2^(i-1)-1,len-1)]的子串,即从x开始往后2^(i-1)这么长,或者到字符串结尾。

例如bdabdc,第一次比较我们考虑b、d、a、b、d、c,第二次考虑bd、da、ab、bd、dc、c,第三次考虑bdab、dabd、abdc、bdc、dc、c,第四次考虑bdabdc、dabdc、abdc、bdc、dc、c。

我们发现”bdab”=”bd”+”ab”,”dabd”=”da”+”bd”,”bdabdc”=”bdab”+”dc”…就是说我们每一次对后缀的排序相当于对上一次的后缀排序进行双关键字排序(如果前半段不同比较前半段,否则比较后半段)!

所以如果我们直接每一次对上一次的结果双关键字sort一下…就可以做到O(nlog^2n)啦!

下面这份代码中rank只用来比较,所以中间过程中可能会有一样的rank,最后的rank才是真正的rank,然后sort时比较rank即可。

//codevs1500
//O(nlog^2n)
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <limits>
#include <set>
#include <map>
using namespace std;
#define SZ 666666
int n,sa[SZ],t[SZ],rank[SZ];
char s[SZ];
int p;
bool cmp(int a,int b) {return (t[a]<t[b])||(t[a]==t[b]&&t[a+p]<t[b+p]);}
bool same(int a,int b) {return t[a]==t[b]&&t[a+p]==t[b+p];}
void getsa()
{
    for(int i=0;i<n;i++) rank[i]=s[i];
    for(int j=0;j<=n;(!j)?(j=1):(j=j+j))
    {
        p=j;
        for(int i=0;i<n;i++) sa[i]=i, t[i]=rank[i];
        sort(sa,sa+n,cmp);
        for(int i=0,p=0;i<n;i++)
            rank[sa[i]]=(i>0&&same(sa[i],sa[i-1]))?p:++p;
    }
}
int main()
{
    scanf("%*d%s",s);
    n=strlen(s); getsa();
    for(int i=0;i<n;i++) printf("%d\n",sa[i]+1);
}

为什么是O(nlog^2n)的呢?废话,有一个sort啊。

嗯好,那么O(nlogn)是什么做法呢?基数排序(又叫计数排序)!

但是我们需要先解决一个问题…基数排序如何双关键字?

在我们的印象中…基数排序就是对于每个数,找到有多少个数不大于它。

怎么找呢?前缀和啊。

但是为了解决相等的情况,每次我们取出一个数的时候都要把前缀和数组-1。

类似这样(摘自维基百科https://zh.wikipedia.org/wiki/%E8%AE%A1%E6%95%B0%E6%8E%92%E5%BA%8F)

image

代码写出来像这样:

//基数排序 
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <limits>
#include <set>
#include <map>
using namespace std;
int n,p[2333333],qzh[2333333],ans[2333333];
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++) scanf("%d",p+i), ++qzh[p[i]];
    for(int i=1;i<=1000000;i++) qzh[i]+=qzh[i-1];
    for(int i=0;i<n;i++) ans[--qzh[p[i]]]=p[i];
    for(int i=0;i<n;i++) printf("%d ",ans[i]);
}

咦我们会发现一个问题,那就是对于相等的元素,位置前面的会放到比较后面的位置!

后缀数组_第1张图片

这是为什么呢?仔细想想你会觉得十分显然,因为顺着循环p[i],所以相等的元素,位于后面的元素此时的qzh[p[i]]因为前面减过会比较小,就会存在前面的位置。

所以我们发现如果我们改成倒着循环,在值一样的时候下标小的就会在前面。

所以假设我们要对a和b双关键字排序,a优先。那么我们先按b排序,然后倒着循环对a排序,就可以实现双关键字排序啦!

我们考虑把上面一段代码的sort改成双关键字的基数排序。当然,一般的字符串题字符集都比较小,如果实在比较大也只能sort…

那我们现在要做的就是先按第二关键字排序,再按第一关键字排序。所以就可以得到一个O(nlogn)的算法啦!

下面的这段代码实现仍然比较奇怪,为了防止比较的时候下标超界,我们必须把n加一,然后令s[n]=0。

//codevs1500
//O(nlogn)
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <vector>
#include <limits>
#include <set>
#include <map>
using namespace std;
#define SZ 666666
int n,sa[SZ],t[SZ],rank[SZ],qzh[SZ],tmpsa[SZ],tmpr[SZ];
char s[SZ];
bool same(int a,int b,int p) {return t[a]==t[b]&&t[a+p]==t[b+p];}
void getsa()
{
    int m=max(n,28);
    for(int i=0;i<n;i++) rank[i]=s[i], ++qzh[rank[i]];
    for(int i=1;i<m;i++) qzh[i]+=qzh[i-1];
    for(int i=n-1;i>=0;i--) sa[--qzh[rank[i]]]=i;
    for(int j=1;j<=n;j<<=1)
    {
        int cur=-1;
        for(int i=n-j;i<n;i++) tmpsa[++cur]=i;
        for(int i=0;i<n;i++) if(sa[i]>=j) tmpsa[++cur]=sa[i]-j;
        for(int i=0;i<n;i++) tmpr[i]=rank[tmpsa[i]];
        for(int i=0;i<m;i++) qzh[i]=0;
        for(int i=0;i<n;i++) ++qzh[tmpr[i]];
        for(int i=1;i<m;i++) qzh[i]+=qzh[i-1];
        for(int i=n-1;i>=0;i--) t[i]=rank[i], sa[--qzh[tmpr[i]]]=tmpsa[i];
        for(int i=0,p=0;i<n;i++)
            rank[sa[i]]=(i>0&&same(sa[i],sa[i-1],j))?p:++p;
    }
}
int main()
{
    scanf("%*d%s",s); n=strlen(s);
    for(int i=0;i<n;i++) s[i]=s[i]-'a'+1;
    ++n; getsa();
    for(int i=1;i<n;i++) printf("%d\n",sa[i]+1);
}

有关应用下一次再写~

你可能感兴趣的:(后缀数组)