zoukankan      html  css  js  c++  java
  • [数据结构-后缀数组小结]

    本文参考《训练指南》P219 、 《IOI2009 国家集训队论文by罗穗骞》

    后缀数组是对文本串进行处理,而非模板串(在文本串中查找模板串),例如搜索引擎。

    而AC自动机是对模板串进行预处理。两者区别。

    后缀trie树:对于字符串banana,可以把它的所有后缀(banana$,anana$,nana$,ana$,na$,a$)插入一颗trie树中。这样查询起来只需要对trie树进行一次遍历就行了。

    后缀树(Suffix tree):在实际应用中,会把后缀trie中没有分支的链合并到一起,得到所谓的后缀树,但由于后缀树构造算法复杂难懂,且容易写错(虽然代码并不长),在算法竞赛中很少使用。

    后缀数组

    定义:为叙述方便,我们直接把"以下标k开头的后缀"叫做后缀k。把所有后缀进行字典序排序,然后将排好序的后缀转化成对应的k,得到的数组就是后缀数组。

    比如banana:a(5) ,ana(3), anana(1), banana(0), na(4), nana(2)

    那么banana的后缀数组就是 5,3,1,0,4,2

    显然,直接对所有后缀进行排序效率是很低的,那么我们可以使用倍增算法(double_algorithm)与三分算法(Difference Cover modulo 3)。

    倍增算法(da):

    对于banana这个字符串:(以下表格体现的是rank数组的值,即当前后缀的排名。表格变化过程体现了da算法的过程)

    1、先对每个后缀的第一个字母进行排序:

    b

    a

    n

    a

    n

    a

    2

    1

    3

    1

    3

    1

    2、对每个后缀的前2个字符进行排序

    b

    a

    n

    a

    n

    a

    21

    13

    31

    13

    31

    10

    3

    2

    4

    2

    4

    1

    3、对每个后缀的前4个字符进行排序

    b

    a

    n

    a

    n

    a

    32

    24

    42

    24

    41

    10

    3

    2

    5

    2

    4

    1

    4、对每个后缀的前8个字符进行排序

    b

    a

    n

    a

    n

    a

    32

    25

    52

    24

    41

    10

    4

    3

    6

    2

    5

    1

    这时候所有名次已经两两不同了,分别是1~6,下面就不用进行排序了。

    显然,如果继续对前16个字符进行排序,那么结果和上面是一样的

    b

    a

    n

    a

    n

    a

    43

    36

    62

    25

    51

    10

    4

    3

    6

    2

    5

    1

    由于每次比较的字符数都翻倍,所以比较次数是log(n),而每次比较的复杂度是多少呢?

    如果使用快速排序,那么每次需要O(nlogn),总共的复杂度就是O(n*logn*logn)。然而这里可以使用基数排序来使每次排序达到O(n)。

    基数排序是很容易理解的。

    //倍增算法--《后缀数组——处理字符串的有力工具》IOI2009 国家集训队论文

    int wa[maxn],wb[maxn],wv[maxn],ws[maxn];

    int cmp(int *r,int a,int b,int l)

    {

    return r[a]==r[b]&&r[a+l]==r[b+l];

    }

    /*

    待排序的字符串放在 r 数组中,从 r[0]到 r[n-1],长度为 n,且最大值小

    于 m。为了函数操作的方便,约定除 r[n-1]外所有的 r[i]都大于 0, r[n-1]=0。

    函数结束后,结果放在 sa 数组中,从 sa[0]到 sa[n-1]。

    */

    void da(int *r, int *sa, int n, int m)

    {

    int i,j,p,*x=wa,*y=wb,*t;

    //第一步:对长度为1的字符串进行排序

    for(i=0; i<m; i++) ws[i]=0;

    for(i=0; i<n; i++) ws[ x[i]=r[i] ]++;

    for(i=1; i<m; i++) ws[i]+=ws[i-1];

    for(i=n-1; i>=0; i--) sa[--ws[x[i]]]=i;

    for(j=1,p=1; p<n; j*=2,m=p)

    {

    for(p=0,i=n-j; i<n; i++) y[p++]=i;

    for(i=0; i<n; i++) if(sa[i]>=j) y[p++]=sa[i]-j;

    for(i=0; i<n; i++) wv[i]=x[ y[i] ];

    for(i=0; i<m; i++) ws[i]=0;

    for(i=0; i<n; i++) ws[wv[i]]++;

    for(i=1; i<m; i++) ws[i]+=ws[i-1];

    for(i=n-1; i>=0; i--) sa[--ws[wv[i]]]=y[i];

    for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i<n; i++)

    x[sa[i]]=cmp(y,sa[i-1],sa[i],j)?p-1:p++;

    }

    }

    代码理解:

    函数的第一步,要对长度为 1 的字符串进行排序。一般来说,在字符串的题目中,r 的最大值不会很大,所以这里使用了基数排序。如果 r 的最大值很大(也就是m的值很大),那么把这段代码改成快速排序。

    代码:

    for(i=0;i<m;i++) ws[i]=0;

    for(i=0;i<n;i++) ws[ x[i]=r[i] ]++;

    for(i=1;i<m;i++) ws[i]+=ws[i-1];

    for(i=n-1;i>=0;i--) sa[ --ws[x[i]] ]=i;

    这里 x 数组保存的值相当于是 rank 值。下面的操作只是用 x 数组来比较字符的大小,所以没有必要求出当前真实的 rank 值。在初始化时,x数组和r数组一样,但这并不意味着x数组的意义和r一样,x数组保存的当前长度为j的子串的排名。因为这些"排名"可能会相同,也可能会断号(只在第一步可能断号),所以这不是严格意义的排名,我觉得更应该理解为优先级,x数值越大,优先级小,排名越靠后。初始化时可以理解为是对长度为1的子串进行排序,所以x可以和r一样,可以把r想象成对每一个长为1的子串的优先级定义。

    接下来进行若干次基数排序,在实现的时候,这里有一个小优化。基数排序要分两次,第一次是对第二关键字排序,第二次是对第一关键字排序。对第二关键字排序的结果实际上可以利用上一次求得的 sa 直接算出, 没有必要再算一次。

    代码:

    for(p=0,i=n-j;i<n;i++) y[p++]=i;//长度不足j的子串,结合下文,这里就是后半部分全是空的情况,前半部分是未知字符。

    for(i=0;i<n;i++) if(sa[i]>=j) y[p++]=sa[i]-j; //这里利用的是上一次的sa数组,不会与上一行重复。

    其中变量j是上一轮排序的子串长度,数组y保存的是对第二关键字排序的结果 。

    上述代码不太好理解,其实,在这段代码执行之前,sa数组存的是上一轮的排序结果,上一轮已经对长度为j的子串排好序了(这也就是为什么j从1开始循环而不从2开始,因为在for循环之前已经初始化求出排序长度为1的sa数组了),这一轮实际上是对长度为2*j的子串进行排序得到新的sa数组。而上述代码的功能就是利用上一轮的sa数组生成y数组,这里的y保存的是对长度为2*j的子串进行第二关键字排序的结果。

    y数组详细解释:y数组保存的是字符串下标,y[i],y[i+1]分别表示排在第i位和排在第i+1位的长度为2*j的子串首下标,之前说过,这里的排序规则是第二关键字的字典序,也就是说它是根据这个2*j子串的后半部分的字典序进行排序的,而前半部分的排序还没有完成。

    假设y[i]表示的字符串是 ????abcd

    假设y[i+1]表示的字符串是 ????bcde

    其中问号字符表示未知字符(不是问号本身),显然这里的j是4,y[i]之所以在y[i+1]的前面,是因为abcd< bcde,而y[i]保存的是????abcd这个子串的首下标,y[i+1]保存的是????bcde这个子串的首下标。

    然后要对第一关键字进行排序,代码:

    for(i=0;i<n;i++) wv[i]=x[y[i]]; //前半部分(第一关键字)的优先级(排名)。

    //仔细看上面这行代码,很容易误解,i从0到n-1,并不是字符串下标从0到n-1,而是第二关键字的排名从第0名循环到第n-1名,wv[i]保存的是第二关键字排名为i的子串的第一关键字排名(优先级)。

    for(i=0;i<m;i++) ws[i]=0;

    for(i=0;i<n;i++) ws[ wv[i] ]++;

    for(i=1;i<m;i++) ws[i]+=ws[i-1];

    for(i=n-1;i>=0;i--) sa[ --ws[wv[i]] ]=y[i];//子串:第二关键字的排名从第n-1名循环到第0名,--ws[wv[i]]是这个子串的新排名,于是把这个子串的首下标赋值给sa[--ws[wv[i]] ]。

    这样便求出了新的 sa 值。在求出 sa 后,下一步是计算 rank 值(优先级),也就是下一轮的x数组。这里要注意的是,可能有多个字符串的 x值是相同的,所以必须比较两个字符串是否完全相同, y 数组的值已经没有必要保存, 为了节省空间, 这里用 y 数组保存 rank值。

    我们之前已经说过,y已经保存了第二关键字的排序结果,y[i]表示第二关键排名第i的子串首下标,那么如何计算下一轮的x数组呢?首先将x和y指针互换,x[sa[0]]=0的作用是根据已经计算好的sa数组初始化x数组的其中一个值,这个位置的rank是0毫无疑问,接下来不断更新x的其他值,p从1开始,如果当前子串和上一个子串完全相同,那么rank是p-1,否则,rank是p,且p++。这个也比较好理解。在比较排名相邻的两个子串是否完全相等的过程中,用到了y数组(原来的x数组),也就是比较这两个子串前后两部分的y值(原来的x,也就是原来的rank值)是否相同。

    这一步完成之后,x数组保存的就是rank值,两个串相等的话,其rank值是相同的。

    这里又有一个小优化,将 x 和 y 定义为指针类型,复制整个数组的操作可以用交换指针的值代替,不必将数组中值一个一个的复制。代码:

    for(t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1 ; i<n ; i++)

    x[sa[i]]=cmp(y,sa[i-1],sa[i],j) ? p-1:p++;

    这里的cmp函数是:

    int cmp(int *r,int a,int b,int l)

    {

    return r[a]==r[b]&&r[a+l]==r[b+l];

    }

    这里可以看到规定 r[n-1]=0 的好处,如果 r[a]=r[b],说明以 r[a]或 r[b]开头的长度为l的字符串肯定不包括字符r[n-1] , 所以调用变量 r[a+l]和r[b+l]不会导致数组下标越界,这样就不需要做特殊判断。执行完上面的代码后,rank值保存在 x 数组中,而变量 p 的结果实际上就是不同的字符串的个数。这里可以加一个小优化, 如果 p 等于 n, 那么函数可以结束。因为在当前长度的字符串中 ,已经没有相同的字符串,接下来的排序不会改变 rank 值。例如图 1 中的第四次排序,实际上是没有必要的。对上面的两段代码,循环的初始赋值和终止条件可以这样写:

    for(j=1,p=1;p<n;j*=2,m=p) {…………}

    在第一次排序以后,rank 数组中的最大值小于 p,所以让 m=p。整个倍增算法基本写好,代码大约 25 行。

    算法分析:

    倍增算法的时间复杂度比较容易分析。每次基数排序的时间复杂度为 O(n) ,排序的次数决定于最长公共子串的长度,最坏情况下,排序次数为 logn 次,所以总的时间复杂度为 O(nlogn)。

    注:关于dc3算法,其具体原理具体请参阅《IOI2009 国家集训队论文by罗穗骞》,该论文还包含两种算法的详细比较,内容太多,暂时不去研究了。

  • 相关阅读:
    日期验证正则表达式
    删除数据表中重复的记录
    转:精选15个国外CSS框架
    WEB打印大全(转)
    document.execCommand()方法使用的语法
    Jquery 1000 demo
    完成 ajax.net 的cross domain ajax功能实现
    "方案改进:直接通过User Control生成HTML", 我的改进意见
    实现Mashup的基本技术手段: cross domain ajax
    你还在用GUID作数据库表的主键吗?
  • 原文地址:https://www.cnblogs.com/lastone/p/5262365.html
Copyright © 2011-2022 走看看