后缀数组(Suffix Array)模板及简析——Part 1:构建SA和rank数组

后缀数组(Suffix Array,SA)是处理字符串的有力工具。它比后缀树更易实现,占用空间更少,并且同样可以解决千变万化的字符串问题

首先推荐罗穗骞的论文(网上搜一下就能搜到),里面对后缀数组的定义、实现和应用都做了详细的阐述

然而不幸的是罗神犇的代码简直魔性,蒟蒻表示这代码压的根本看不懂啊……

所以在理解了后缀数组的构建过程之后,我重新写了一份模板代码。虽然啰嗦了点(代码比较大,而且变量名故意拉长了),不过相对比较好懂

而且论文中用到的辅助空间是4N,我的模板用了3N,事实上还可以优化到只需2N。空间优化的核心思想就是:当SA和rank数组“闲置”时,它们就是辅助数组

(如果字符集很大,那么辅助空间就会是4N(优化后为3N))

Template:

  1 #include <cstdio>
  2 #include <cstring>
  3 #include <algorithm>
  4
  5 const int maxN=200005;
  6
  7 int suffixArray[maxN]; //1-based
  8 int suffixRank[maxN]; //1-based
  9 int height[maxN]; //LCP length of SA[i] and SA[i-1]
 10
 11 char objStr[maxN];
 12 int objCoded[maxN]; //begin with objCoded[1]
 13 int objLen;
 14
 15 inline int codeChar(char x)
 16 {
 17     return x+1-‘a‘;
 18 } //range:[1..26]
 19
 20 void input()
 21 {
 22     scanf("%d",&objLen);
 23     for(int i=0;i<objLen;i+=80) scanf("%s",objStr+i);
 24
 25     for(int i=1;i<=objLen;i++)
 26         objCoded[i]=codeChar(objStr[i-1]);
 27     objCoded[objLen+1]=-1;
 28 }
 29
 30 const int alphabetSize=26;
 31
 32 int charCount[alphabetSize+1];
 33
 34 int secondKey[maxN]; //use suffixArray[] itself as firstKey
 35 int radixCount[maxN];
 36 int suffixTemp[maxN];
 37
 38 void buildSuffixArray()
 39 {
 40     memset(charCount,0,sizeof(charCount));
 41
 42     for(int i=1;i<=objLen;i++)
 43         ++charCount[objCoded[i]];
 44
 45     for(int i=2;i<=alphabetSize;i++)
 46         charCount[i]+=charCount[i-1];
 47
 48     for(int i=1;i<=objLen;i++)
 49         suffixRank[i]=charCount[objCoded[i]];
 50
 51     for(int step=1;step<objLen;step<<=1)
 52     {
 53         for(int i=1;i<=objLen;i++)
 54             secondKey[i]=(i+step<=objLen)?suffixRank[i+step]:0;
 55
 56         memset(radixCount,0,sizeof(radixCount));
 57
 58         for(int i=1;i<=objLen;i++)
 59             ++radixCount[secondKey[i]];
 60
 61         for(int i=1;i<=objLen;i++)
 62             radixCount[i]+=radixCount[i-1];
 63
 64         for(int i=objLen;i;i--)
 65         {
 66             int pos=radixCount[secondKey[i]]--;
 67             suffixTemp[pos]=i;
 68         }
 69
 70         memset(radixCount,0,sizeof(radixCount));
 71
 72         for(int i=1;i<=objLen;i++)
 73             ++radixCount[suffixRank[i]];
 74
 75         for(int i=1;i<=objLen;i++)
 76             radixCount[i]+=radixCount[i-1];
 77
 78         for(int i=objLen;i;i--)
 79         {
 80             int& cur=suffixTemp[i];
 81             int pos=radixCount[suffixRank[cur]]--;
 82             suffixArray[pos]=cur;
 83         }
 84
 85         int last=suffixRank[suffixArray[1]];
 86         suffixRank[suffixArray[1]]=1;
 87         bool finished=true;
 88
 89         for(int i=2;i<=objLen;i++)
 90         {
 91             int& curPos=suffixArray[i];
 92             int& lastPos=suffixArray[i-1];
 93             int temp=suffixRank[curPos];
 94             if(temp==last && secondKey[curPos]==secondKey[lastPos])
 95             {
 96                 suffixRank[curPos]=suffixRank[lastPos];
 97                 finished=false;
 98             }
 99             else
100                 suffixRank[curPos]=i;
101             last=temp;
102         }
103
104         if(finished) break;
105     }
106 }
107
108 void buildHeight()
109 {
110     memset(height,0,sizeof(height));
111
112     for(int i=1;i<=objLen;i++)
113     {
114         int& curRank=suffixRank[i];
115         int& lastRank=suffixRank[i-1];
116         if(curRank>1)
117         {
118             height[curRank]=height[lastRank]?height[lastRank]-1:0;
119             int& lastSA=suffixArray[curRank-1];
120             int& curSA=suffixArray[curRank]; //curSA=i
121             for(;;height[curRank]++)
122             {
123                 if(objCoded[lastSA+height[curRank]] !=
124                         objCoded[curSA+height[curRank]] )
125                     break;
126             }
127         }
128     }
129 }
130
131 typedef long long int64;
132
133 #include <iostream>
134
135 int main()
136 {
137     input();
138     buildSuffixArray();
139     buildHeight();
140
141     int64 ans=0;
142
143     for(int i=1;i<=objLen;i++)
144         ans+=(objLen-suffixArray[i]+1)-height[i];
145
146     std::cout<<ans<<"\n";
147     return 0;
148 }

Suffix Array(Demo:Vijos P1567)

接下来,我们将对SA和rank的构造过程进行解析。

 1 const int maxN=200005;
 2
 3 int suffixArray[maxN]; //1-based
 4 int suffixRank[maxN]; //1-based
 5 int height[maxN]; //LCP length of SA[i] and SA[i-1]
 6
 7 char objStr[maxN];
 8 int objCoded[maxN]; //begin with objCoded[1]
 9 int objLen;
10
11 inline int codeChar(char x)
12 {
13     return x+1-‘a‘;
14 } //range:[1..26]
15
16 void input()
17 {
18     scanf("%s",objStr);
19     objLen=strlen(objStr);
20
21     for(int i=1;i<=objLen;i++)
22         objCoded[i]=codeChar(objStr[i-1]);
23     objCoded[objLen+1]=-1;
24 }

suffixArray(SA)就是后缀数组本身,suffixArray[i]=x,表示从小到大(字典序)第i个后缀从第x个字符开始,也就是“排第几的是谁”

以下为了叙述方便,我们用“后缀u”(u为一个整数)这样的说法代指“以第u个字符开始的后缀”

suffixRank(SR)是后缀数组的逆映射,若suffixRank[i]=x,则suffixArray[x]=i,表示以第i个字符开头的后缀能排第x位,也就是”你排第几“

height[i]记录后缀SA[i]和后缀SA[i-1]的LCP(Longest Common Prefix,最长公共前缀)长度。即排第i的后缀和排第i-1的后缀的LCP长度

objStr是我们要操作的目标字符串,objCoded则是”编码“后的串(我们假定字符集为小写英文字母)

当然如果不需要再对objStr本身进行操作(比如输出某个字串本身)的话,objCoded数组其实也可以省略掉

在objCoded的结尾,我们用-1代指‘/0‘(字符串结束符)

为了方便,我们将目标串的长度objLen存放到全局变量中,并且规定objCoded的下标从1开始

1 const int alphabetSize=26;
2
3 int charCount[alphabetSize+1];
4
5 int secondKey[maxN]; //use suffixArray[] itself as firstKey
6 int radixCount[maxN];
7 int suffixTemp[maxN];

这些变量的含义我们会在讲解buildSuffixArray()过程中予以说明(现在说了反正也没用(*´∇`*))

构建SA的过程只需4个辅助数组,而且与字符串规模相当的只有3个,另一个非常小,空间开销几乎可以忽略

“另一个非常小”的前提是字符集很小,不过在OI/ACM范畴内,它通常不会超过ASCII的值域(除非在某些问题中把数列当成串处理,那样的话“字符集”就会与objLen同阶)

在这3个“大”数组中,有一个实际上是可以省略的,所以真正必需的辅助空间实际上只有2N加上一点零头

我们将使用倍增算法构造SA数组。它的时间复杂度是O(n log n)

SA的O(n)构造算法(DC3)在这里不再赘述,它的常数因子巨大,所以时间效率实际上优势不是很大,而且其空间开销也比倍增算法大的多

图为利用倍增算法求suffixRank数组的过程。

倍增算法的基本思路是:对于每一个后缀,第i次比较长度为step=2^i的前缀。如果该后缀长度小于step,用0作为第二关键字,直到:

(1)step>=objLen

(2)step<objLen,但排序的结果已经两两互异,这样的话再进行下去也没有意义了

0比charCoded的最小值1还要小,所以用0作为“填充物”,可以使末尾较短的(后缀的)前缀字典序较小。

每次比较时,把suffixRank[i]作为第一关键字,把suffixRank[i+step]作为第二关键字,然后进行排序

排序可以用快排实现,时间复杂度为O(n log n),乘以倍增的O(log n),总的时间复杂度就是O(n log^2 n)

更好的做法是基数排序,时间复杂度为O(n)

算法流程(伪代码):

首先把字符本身的排名作为suffixRank的初值

for(int step=1;step<objLen;step<<=1)

  构建二元组P[i]={suffixRank[i],suffixRank[i+step]}(i+step>objLen时用0补充),对P[]进行基数排序

    先按第二关键字得到suffixTemp序列,使得:

      若suffixTemp[i]=x,则以第二关键字升序排序后,P[x]排在第i位

    然后按第一关键字得到suffixArray(中间结果)

      for(int i=objLen;i;i--)

        记cur=suffixTemp[i],按基数排序的规则分配P[cur],使得第一关键字相同时,第二关键字大的放在后面

  根据当前的suffixArray求出下一步的suffixRank

    for(int i=1;i<=objLen;i++)

      记cur=suffixArray[i],last=suffixArray[i-1]

      若i==1,则令suffixRank[cur]=1,否则:

        若P[cur]==P[last],则令suffixRank[cur]=suffixRank[last]

        否则,令suffixRank[cur]=i,使得suffixRank[cur]>suffixRank[last]

 1     memset(charCount,0,sizeof(charCount));
 2
 3     for(int i=1;i<=objLen;i++)
 4         ++charCount[objCoded[i]];
 5
 6     for(int i=2;i<=alphabetSize;i++)
 7         charCount[i]+=charCount[i-1];
 8
 9     for(int i=1;i<=objLen;i++)
10         suffixRank[i]=charCount[objCoded[i]];

在”正式“开始基数排序之前,我们需要一个倍增的”基石“,即预先处理step=0的情形(也就是对字符本身排序)

这里则是用了计数排序(和基数排序有一些差异,尽管都是分配式排序而且汉语读音都完全一样)的思想

细心的话会发现按上面代码排出来的SR初值和图中给的不太一样:

事实上这无关紧要,因为此时我们只需要知道一个相对的大小关系,所以两种标号方式是等价的

 1     for(int step=1;step<objLen;step<<=1)
 2     {
 3         for(int i=1;i<=objLen;i++)
 4             secondKey[i]=(i+step<=objLen)?suffixRank[i+step]:0;
 5
 6         memset(radixCount,0,sizeof(radixCount));
 7
 8         for(int i=1;i<=objLen;i++)
 9             ++radixCount[secondKey[i]];
10
11         for(int i=1;i<=objLen;i++)
12             radixCount[i]+=radixCount[i-1];
13
14         for(int i=objLen;i;i--)
15         {
16             int pos=radixCount[secondKey[i]]--;
17             suffixTemp[pos]=i;
18         }

我们把基数排序的过程分成了两部分。

第一个循环用来比较secondKey(也就是第二关键字)。

这个数组可以省掉,secondKey可以即时计算出来,如果省掉的话辅助空间就成了2N

之后3个循环就是第一趟基数排序的过程,结果暂时放在suffixTemp数组内

这个suffixTemp就是第二趟基数排序的处理序列(仍然是倒序遍历),suffixTemp[i]=x表示第二关键字排第i位的二元组位于第x个位置

是不是和suffixArray的含义很像?实际上,我们求的这个suffixTemp数组就是suffixArray的中间结果

根据基数排序的实现原理,由于我们是对suffixTemp倒序处理的,所以(第一关键字相同时)第二关键字较大的会被放到相对靠后的位置

这样在第一关键字排好序的同时,第二关键字也会处于合适的位置

 1         memset(radixCount,0,sizeof(radixCount));
 2
 3         for(int i=1;i<=objLen;i++)
 4             ++radixCount[suffixRank[i]];
 5
 6         for(int i=1;i<=objLen;i++)
 7             radixCount[i]+=radixCount[i-1];
 8
 9         for(int i=objLen;i;i--)
10         {
11             int& cur=suffixTemp[i];
12             int pos=radixCount[suffixRank[cur]]--;
13             suffixArray[pos]=cur;
14         }

第二趟基数排序,依照第一关键字(就是上一步的suffixRank)

最后一次循环,我们利用了suffixTemp序列,使得第二关键字较大的尽可能先被处理,其位置就会相对靠后

这里我们将第二趟基数排序的结果放到了suffixArray中。suffixArray[pos]=cur的含义就是”排第pos个的后缀可能从cur开始“

此时的suffixArray只是一个中间结果

        int last=suffixRank[suffixArray[1]];
        suffixRank[suffixArray[1]]=1;
        bool finished=true;

        for(int i=2;i<=objLen;i++)
        {
            int& curPos=suffixArray[i];
            int& lastPos=suffixArray[i-1];
            int temp=suffixRank[curPos];
            if(temp==last && secondKey[curPos]==secondKey[lastPos])
            {
                suffixRank[curPos]=suffixRank[lastPos];
                finished=false;
            }
            else
                suffixRank[curPos]=i;
            last=temp;
        }

        if(finished) break;
    }

最后就是计算新的suffixRank数组。

这里实际上略微用到了SA和SR互为反函数的关系。但由于此时的suffixArray不一定是真正的SA,所以还需要进行比较

这一步得到的suffixRank可能会有相等的值(表示后缀i和后缀i+step在当前比较的范围内是相等的),所以此时的SR可能只是中间结果

这里用到了一个小优化:如果suffixRank没有相等的值,就说明我们已经得到了suffixRank(顺便也有suffixArray)的最终结果,此时直接跳出循环即可

小细节(节省空间需要)留给读者推敲

至此suffixArray和suffixRank的构造工作已经完成。它们已经可以用来处理简单的字符串问题

但是只有SA和rank数组,我们能处理的问题十分有限,所以我们还需要一个强大的工具——height数组

在下一篇博文中,我们不仅会对height[]的构造过程进行解析,而且会通过一些例题,初步展现height数组的威力

————To be continued————

时间: 2024-10-11 04:13:40

后缀数组(Suffix Array)模板及简析——Part 1:构建SA和rank数组的相关文章

后缀数组(suffix array)

参考: Suffix array - Wiki 后缀数组(suffix array)详解 6.3   Suffix Arrays - 算法红宝书 Suffix Array 后缀数组 基本概念 应用:字符串处理.生物信息序列处理 后缀:学过英语的都知道什么叫后缀,就是从某个位置开始到字符串结尾的特殊子串,记住 Suffix(i)=S[i...len(S)-1],i就是后缀起始位置 后缀数组:就是将后缀排序好后放到一个一维数组里,SA[i]存放排名第i大的后缀首字符下标,并且保证 Suffix(SA

后缀数组(suffix array)详解

后缀数组(suffix array)详解 转载请注明:http://www.cnblogs.com/acmer-jsb/p/3988683.html 一.What  Is  Suffix Array? 用我的理解,后缀数组是一种功能强大的字符串处理工具,堪称字符串处理神奇,尤其是在字符串匹配方面更是有着出色的处理能力. 其实后缀数组是后缀树的一个非常精巧的替代品,它比后缀树容易编程实现,能够实现后缀树的很多功能而时间复杂度也不太逊色,并且,它比后缀树所占用的空间小很多.可以说,在信息学竞赛中后缀

利用后缀数组(suffix array)求最长公共子串(longest common substring)

摘要:本文讨论了最长公共子串的的相关算法的时间复杂度,然后在后缀数组的基础上提出了一个时间复杂度为o(n^2*logn),空间复杂度为o(n)的算法.该算法虽然不及动态规划和后缀树算法的复杂度低,但其重要的优势在于可以编码简单,代码易于理解,适合快速实现. 首先,来说明一下,LCS通常指的是公共最长子序列(Longest Common Subsequence,名称来源参见<算法导论>原书第3版p223),而不是公共最长子串(也称为最长公共子串). 最长公共子串问题是在文本串.模式串中寻找共有的

用倍增法构造后缀数组中的SA及RANK数组

感觉后缀数组很难学的说= = 不过总算是啃下来了 首先 我们需要理解一下倍增法构造的原理 设原串的长度为n 对于每个子串 我们将它用'\0'补成长度为2^k的串(2^k-1<n<=2^k) 比如串aba的子串就有 aba'\0'    ba'\0''\0'  a'\0''\0''\0' 每次操作我们可以排出所有长度为 2^x的子串的大小 比如串aba的排序过程 第一遍 a                   a             b 第二遍 a'\0'             ab  

后缀数组suffix array

倍增算法,时间复杂度O(nlogn) sa从小到大保存相对大小的下标 理解LSD,x数组,sa数组 char s[maxn]; int sa[maxn],t[maxn],t2[maxn],c[maxn],n; void build_sa(int m) { //LSD基数排序 int *x=t,*y=t2;//x数组保存rank //字串长度为1,即对每一个元素的大小排序 for(int i=0;i<m;++i) c[i]=0;//计数数组清空 for(int i=0;i<n;++i) c[x[

后缀数组详解+模板

后缀数组 注 SA[] 第几名是谁 后缀数组:后缀数组 SA 是一个一维数组, 它保存 1..n 的某个排列 SA[1] ,SA[2],……,SA[n],并且保证 Suffix(SA[i]) < Suffix(SA[i+1]),1≤i<n .也就是将 S 的 n 个后缀从小到大进行排序之后把排好序的后缀的开头位置顺次放入 SA 中. Rank[] 谁是第几名名次数组:名次数组 Rank[i]保存的是 Suffix(i)在所有后缀中从小到大排列的“名次 ” . r[]:原始数据j当前字符串的长度

UVA11107 后缀数组(new模板)

IT 要走多久,要怎么走. IT 要走多久,要怎么走.这些问题,在我已经快毕业了一个年头的现在,又重新浮现在我的脑海里.一边是工作的了了模块,一边是可以自己无聊打发的时间.这不是我当初要的路,现在的路是一条没有激情,没有波澜,没有变革,没有无论是技术方向,还是职业规划此时此刻又都摆在了我的眼前.工作是工作,职业是职业. 我一直这么觉得,我不想把IT仅仅当为一种工作一样继续这样做下去,我不喜欢把IT当成一种生存计生这样的做下去.它应该是我的一种爱好,一种职业,一种诉求,一种偏执,一种倔强的追求.而

Linux网络性能优化方法简析

Linux网络性能优化方法简析 2010-12-20 10:56 赵军 IBMDW 字号:T | T 性能问题永远是永恒的主题之一,而Linux在网络性能方面的优势则显而易见,这篇文章是对于Linux内核中提升网络性能的一些优化方法的简析,以让我们去后台看看魔术师表演用的盒子,同时也看看内核极客们是怎样灵活的,渐进的去解决这些实际的问题. AD:2014WOT全球软件技术峰会北京站 课程视频发布 对于网络的行为,可以简单划分为 3 条路径:1) 发送路径,2) 转发路径,3) 接收路径,而网络性

GLib库安装与简析

GLib是GTK+和GNOME工程的基础底层核心程序库,是一个综合用途的实用的轻量级的C程序库, 它提供C语言的常用的数据结构的定义.相关的处理函数,有趣而实用的宏, 可移植的封装和一些运行时机能,如事件循环.线程.动态调用.对象系统等的API. 它能够在类UNIX的操作系统平台(如LINUX, HP-UNIX等),WINDOWS,OS2和BeOS等操作系统台上运行. 一.GLib在CentOS上的安装 检查系统当前的版本 # rpm -qi glibc Name        : glibc