后缀数组习题集

理论学习参考:

http://www.cnblogs.com/zinthos/p/3899725.html

国家集训队论文   《后缀数组--处理字符串的有力工具》  --罗穗骞 

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


转自http://hi.baidu.com/zfy0701/blog/item/f2278a0928991dca3bc763a0.html

字符串算法通常是非常优雅的,尤其是后缀数组啊,前、后缀自动机啊,TrieKMPBMBOMShift - OrRK ......

我准备写写POJ上一些字符串问题的文章,第一篇是我认为在竞赛中字符串问题用处最大的一个工具:后缀数组。

先帖点后缀数组的背景资料:

然后解决如下三个POJ上的问题:

PKU 2774 - Long Long Message
PKU 1743 - Musical Theme
PKU 3415 - Common Substrings

http://www.lirmm.fr/~rivals/ALGOSEQ/DOC/Manber-Myers-table-des-suffixes.pdf
这是后缀数组的原始论文,作者Manber是个超级大牛,许智磊那篇文章基本是翻译,E文不差的还是看看原文吧。

注意到这篇论文中提出的构造后缀数组的算法是O(nlogn)的,称倍增算法,现在也有线性算法,但多半情况实际性能似乎不如倍增算法。倒是现在有个O(nloglogn)的算法实际效率最高:论文见Linear time construction of suffix arrays,这里面提到了两种算法,其中理论线性的那个不如非线性的。

这篇论文作者是D. K. Kim, J. S. Sim, H. Park, and K. Park,网上没得,要去图书馆下,另外要区别其与DC3的那个线性算法,两者题目很像,作者不同,DC3空间消耗较大,过多基数排序导致常数非常大,据说有1000,远远大于log1000

DC3的论文(Linear Work Suffix Array Construction)的地址:
http://www.cs.helsinki.fi/u/tpkarkka/publications/jacm05-revised.pdf

下面这篇文章对现在的后缀数组算法有个很好的overview,见其intro部分
http://www.brics.dk/~cstorm/courses/StrAlg_e07/papers/SchurmannStoye2005_SuffixArray.pdf

当然,就竞赛而言我觉得以上这些都是废话。

对于后缀数组,最重要的应该就是掌握倍增算法,更要熟练掌握height数组的性质和应用,要理解height数组,最好仔细看看对height数组性质的几个证明。

有些题目需要用RMQ问题的办法进行预处理以便O(1)时间求出任意两后缀的(LCP)Longest common prefix(必须这样才能做出来的题目我现在还没见到,且代码量会巨大)。但大多数情况一个height数组就够了!


后缀数组的用途大概有:匹配,求两串的最长公共子串,两串的公共子串的数量,单串的重复子串数列,单串的最长可重叠/不可重叠子串

好:基本的东西介绍完了,现在来看题:

PKU 2774 - Long Long Message

这个题要求两个串的公共最长子串(不是子序列,即子串是连续的,子序列不连续——可用动态规划解决)。这道题不需要知道任意两个后缀的LCP值,只要连接两个字符串,求后缀数组与height数组,然后只要贪心地选择height数组中最大的就行(唯一约束就是对于一个height[i]sa[i-1], sa[i]必须分属与两个不同串的位置,如只是对于单串,就没有这个限制)。

算法的正确性就是因为后缀数组是按字母顺序排序的,我们假设两个串的最长公共子串在后缀数组中不相临,那么必然有其他的子串插在这两串之间,那么插在其中的串显然拥有更大的LCP,与假设矛盾了。所以相邻是必然的,而height数组刚好记录的是后缀数组中相邻子串的LCP,所以很容易的可以求解了。

这个题也可以用后缀自动机,Oracle Factor自动机求解(非常高效),见http://hi.baidu.com/zfy0701/blog/item/d9fedbd14581113d9b5027ab.html


PKU 1743 - Musical Theme

对于这道,要先把串转换一下,根据题目的性质,应该把输入得到的串前后相减得到方便求解的新的串——设其为s,再求该串s中最长的不重叠重复子串。由于不能重叠,导致height数组的最大值不一定是解,因为相邻两串可能会重叠。

此题用后缀数组也有两种解法,1:二分枚举答案,2:用栈线性扫描,主要说做法1,因为它更以理解些

现在我们假设-最长,不重叠,重复-子串的长度为len,那么在一个height数组中有一些hgt[i]会小于len,在这个i左右的两个子串,他们LCP是不可能大于或等于len的,这样,就可以吧height数组看做很多LCP >= len的段,我们在每一段中进行扫描,记录这一段中最大和最小的子串串索引(sa[x]),如果两者之和小于len,说明重叠了,否则就找到了一个可行解。

易证:如果该串存在len1的不重叠子串,且len1 > len2,则该串也存在长度len2的不重叠子串,解有连续性,所以我们在上面这个过程外我们可以二分枚举解。

对了,二分查找之前之前可先调用查找len = 4是否成立,不成立就直接return了。

这道题的关键点就是:判断解的时候对height数组进行分段处理,这个分段的思想与其说重要,还不说就是height数组的基本性质。类似的分段法在下题中再次用到。

更详细的过程见解题报告到这去下,上面说的应该比我清楚。

http://www.oibh.org/bbs/viewthread.php?tid=21743

它属于楼天成男人8题中的一道。

核心代码:

bool SearchAns(int *sa, int *hgt, int n, int len) {
     int mi, ma, k, i = 20000, j = 0;
     for (k = 1; k < n; k++) {
         if (hgt[k] < len) {i = 20000; j = 0;}    //复原统计
         else {
              mi = min(sa[k], sa[k-1]);
              ma = max(sa[k], sa[k-1]);
              j = max(j, ma);
              i = min(i, mi);
              if (j - i >= len)   return true;
         }
     }
     return false;

线性扫描的做法:

借助栈扫描,使用栈思路上基本上同3415,但解题的核心思想也和上面的方法所说的一样,所以不详述,根据hgt数组的大小出入栈,扫描时记录每一段的sa[i]最大和最小值,在出栈的时候注意将值更新的上一段。

两种方法我都用了,这题的数据不是很强,后者之比前者快15ms, disscus中还有人说用线性扫描比二分慢的.

PKU 3415 - Common Substrings

求两个字符串不小于k的公共子串的个数,我下面简称k前缀子串(随便编的)。

这道题比较难,是一道非常经典的组合模式匹配问题,官方解题报告我看了一头雾水

后来在google上搜啊搜啊,终于搜到一篇论文,上面略微提到了单个串的不小于k的公共子串的个数的求法,
http://www-hto.usc.edu/people/tingchen/MyPublication/CPM-TrieSearch-1997.pdf
见其3.1的开头部分

然后又得到ImLazy兄的提示,艰难的写出了对于单个串的不小于k的公共子串的个数的程序。我把两个串拼接起来,求k前缀子串数量a,再分别求两个串k前缀子串的数量b, c,然后用a – b – c。其中求完拼接串的后缀数组,两个串的后缀数组可以由品拼接串的后缀数组直接拆分得到,不需要重新O(N * log N)求。

先帖下求单个串的k前缀子串数量核心算法:

假设有m个子串在heigt数组中连续且值等于b,那么他们所产生的k前缀数量为 C(m, 2) * (b – k + 1)(括号内的东西下称贡献量)

用一个栈扫描height(简称hgt)数组

规则1如果hgt[i]大于栈顶元素,入栈,i++,循环continue
规则2如果等于栈顶元素,不入栈,i++,循环continue
规则3如果小于栈顶元素,计算i与栈顶元素之间的长度,按照入栈规则,这一段的hgt值都是一样的,可以容易地按上一段所说的规则进行统计。这里还要分两种情况
1hgt[i]小于栈顶下面的第一个元素,我是先把栈顶元素的hgt成和其下面的一个元素值一样,等于将他们合并,统计这部分的差值(下称相对贡献量),;
2hgt[i]大于栈顶下面的第一个元素,把栈顶元素的hgthgt[i]一样,其他同1

不继续扫描i,循环continue
规则4hgt[i]小于k且栈中有元素,执行统计,且执行出栈操作,不继续扫描i,循环continue
规则5hgt[i]小于k且栈中无元素,i++,循环continue


核心代码   

int top = STACK_BASE;
stack[0] = 0;
hgt[0] = hgt[n + 1] = k - 1;//尾部控制

while (i <= n + 1) {
     hg1 = hgt[stack[top]];
     if (hgt[i] < k && top == STACK_BASE) i++;//如果还未进入统计的状态     
     else if (hgt[i] == hg1) i++;
     else if (hgt[i] > hg1) stack[++top] = i++;
     else {
         interval = i - stack[top] + 1;
         if (hgt[i] >= k && hgt[i] > hgt[stack[top-1]]) { //这种情况小结算,不出栈
              fac = hg1 - hgt[i];
              hgt[stack[top]] = hgt[i];
         } else {//满足消栈情况
              fac = hg1 - hgt[stack[top-1]];
              top--;
         }
         counter += (long long)interval * (interval - 1) / 2 * fac;   //统计
     }
}


这样的做法813MS过了

一个星期后我想到了一次扫描的办法求两串的k前缀子串数量的方法,我的扫描规则还和上面一样,代码也和上面几乎完全一样。只不过主要统计的时候,直接用此段中 串的数量 * b 串的数量 相对贡献量就行了。

改进之后提高60MS,但是对比一下:

1 2766802 crazyb0y 45796K 406MS G++ 1977B
14 3437536(31) zfy0701 4404K 766MS G++ 2964B

差距比较大,一问原来他是用后缀自动机做的。郁闷了,我对后缀自动机的认识仅停留在基于子串的简单串匹配算法上。

 

 

相关习题:

poj 2774 Long Long Message 难度:1

这题求两个字符串的最长公共子串,算是模版题了

POJ 3261 Milk Patterns 难度:1

这题用到最长公共前缀,参考题解

poj 3294 Life Forms 难度:1

这题是上题的加强版吧,参考题解

poj 1226 Substrings 难度:2

这题跟3294差不多,不过这回加上回文,题解同上

poj 1743 Musical Theme 难度:2

楼教主男人八题,最长不重叠重复子串,参考题解

spoj 220 Relevant Phrases of Annihilation 难度:2

n个串的最长公共重复2次子串,参考题解

poj 3415 Common Substrings  难度:4

长度大于k的相同子串对数xian 后缀数组+单调桟统计,参考题解

zoj3199 Longest Repeated Substring
连续重复至少1次的最长串
spoj1811 Longest Common Substring
普通的LCS,PKU2774,不过数据量超大,上边的模板超时,DC3刚好卡过,据说正解是后缀自动机,Orz
hdu2890 Longest Repeated subsequence
不可重叠k次最长重复子串.分层后排序贪心nlog(n)

 

后缀数组经典思想:多串合并+二分答案+最优性--->可行性

例 :最长公共前缀
给定一个字符串,询问某两个后缀的最长公共前缀。   //  直接套用,ans=min( height[ i ] )+rmq    k

例 :可重叠最长重复子串
给定一个字符串,求最长重复子串,这两个子串可以重叠。   //  ans=max( hegiht[ i ] )  0<=i

例 :不可重叠最长重复子串( pku1743       AC
给定一个字符串,求最长重复子串,这两个子串不能重叠。   // 二分转化为判定性问题

例 :可重叠的 次最长重复子串( pku3261        AC
给定一个字符串,求至少出现 次的最长重复子串,这 个子串可以重叠。   //  同上,也是二分

例 :不相同的子串的个数( spoj694,spoj705 
给定一个字符串,求不相同的子串的个数。    
[解法]
对于每一次新加进来的后缀 suffix(sa[k]), 它将产生 n-sa[k]+1 个新的前缀。但是其中有
height[k] 个是和前面的字符串的前缀是相同的。所以 suffix(sa[k]) 将 “ 贡献 
出 n-sa[k]+1- height[k] 个不同的子串。累加后便是原问题的答案。这个做法
的时间复杂度为 O(n) 
例 :最长回文子串( ural1297 
给定一个字符串,求最长回文子串。
[解法]
将整个字 符串反过来写在原字符串后面,中间用一个特殊的字符隔开。这样就把问题变为 了
求这个新的字符串的某两个后缀的最长公共前缀。
egaabebf  ---->   aabebf&fbebaa 

例 :连续重复子串 (pku2406)             AC
给定一个字符串 ,已知这个字符串是由某个字符串 重复 次而得到的,
求 的最大值。
[解法]
做法比较简单,穷举字符串 的长度 ,然后判断是否满足。判断的时候,
先看字符串 的长度能否被 整除,再看 suffix(1) 和 suffix(k+1) 的最长公共
前缀是否等于 n-k 
hit:此题更好的是考察KMPnext
            int k=len-next[len];
            if(len%k==0) fprintf(fout,"%d\n",len/k);
            else fprintf(fout,"1\n");

例 :重复次数最多的连续重复子串 (spoj687,pku3693)   //  还未完成,抽时间好好做,先做SPOJ
给定一个字符串,求重复次数最多的连续重复子串。
[解法]
先穷举长度 ,然后求长度为 的子串最多能连续出现几次。首先连续出 现
次是肯定可以的,所以这里只考虑至少 次的情况。假设在原字符串中连续 出
现 次,记这个子字符串为 ,那么 肯定包括了字符 r[0], r[L], r[L*2],
r[L*3], …… 中的某相邻的两个。所以只须看字符 r[L*i] 和 r[L*(i+1)] 往前和
往后各能匹配到多远,记这个总长度为 ,那么这里连续出现了 K/L+1 次。最 后
看最大值是多少。

例 :最长公共子串 (pku2774,ural1517)     // 发现倍增的O(NlogN) 好慢……2500+ms
给定两个字符串 和 ,求最长公共子串。
连接字符串,O(N)扫描

例 10: 长度不小于 的公共子串的个数 (pku3415)     //  未解决
给定两个字符串 和 ,求长度不小于 的公共子串的个数(可以相同)。
http://hi.baidu.com/zfy0701/blog/item/f2278a0928991dca3bc763a0.html

例 11: 不小于 个字符串中的最长子串 (pku3294)
给定 个字符串,求出现在不小于 个字符串中的最长子串。    //  已经AC,等于没AC,越来越觉得sort慢了……压着时限过的……且写了5K
[解法]
参见例3:扩展到看k个一样
例 12: 每个字符串至少出现两次且不重叠的最长子串 (spoj220)    //  未做,先去学了radix sort再考虑吧,不过SPOJ时空一向很宽……,那次16数独用了100MB……2s
给定 个字符串,求在每个字符串中至少出现两次且不重叠的最长子串。
[解法]
做法和上题大同小异,也是先将 个字符串连起来,中间用不相同的且没 有
出现在字符串中的字符隔开,求后缀数组。然后二分答案,再将后缀分组。判 断
的时候,要看是否有一组后缀在每个原来的字符串中至少出现两次,并且在每 个
原来的字符串中,后缀的起始位置的最大值与最小值之差是否不小于当前答案
(判断能否做到不重叠,如果题目中没有不重叠的要求,那么不用做此判断)。
这个做法的时间复杂度为 O(nlogn) 

例 13: 出现或反转后出现在每个字符串中的最长子串 (PKU1226)   //  AC  数据极弱,比我的算法弱
给定 个字符串,求出现或反转后出现在每个字符串中的最长子串。
[解法]
连接字符串(正反向),二分答案,判断是否都出现过。

后缀数组题目推荐
1412
后缀数组的题目

Uva11475

题目大意 给定一个字符串在字符串的末尾加最少的字符是的该字符串成为回文串。
[这个题目我已经加到OJ上了 题号是1139 这个题目根本不用后缀数组的 而且后缀数组的效率也是不最高的 ——Sempr补充]

Pku2774 : 求两个字符串的最长公共子串长度。                //  AC

Whu1069  求两个字符串的最长公共子串长度。

pku3581 http://acm.pku.edu.cn/JudgeOnline/problem?id=3581    
题目大意:给定一个数组{A1, A2, …, An} 满足A1 > A2, …, An,把该数组分成三段,单独翻转,使得数组的字典序最小。
      
http://acm.pku.edu.cn/JudgeOnline/problem?id=3623              //  AC

题目大意:给定一个字符数组,可以从数组的开头或者结尾取出元素,按取出顺序排成一列,使得他的字典序最小。

http://acm.pku.edu.cn/JudgeOnline/problem?id=1743    //  AC 为什么G++挂,C++AC?肯定是我写错了……而且跑得很慢……
题目大意:求最长不重叠差为定值子串的长度

-》最长不重叠重复子串的长度

(1)二分答案

2)线性扫描

http://acm.pku.edu.cn/JudgeOnline/problem?id=3450      //    未作

题目大意:求多个字符串的最长公共字串。

http://acm.pku.edu.cn/JudgeOnline/problem?id=3080        //  AC

题目大意:求多个字符串的最长公共子串(弱)。

用后缀数组比较麻烦,可以枚举答案,用kmp判定。

Waterloolife formshttp://acm.pku.edu.cn/JudgeOnline/problem?id=3294     //   AC  好题

题目大意:超过一半的串的公共最长子串。

http://acm.pku.edu.cn/JudgeOnline/problem?id=3415            //  未作   好题    

题目大意:求两个字符串的长度大于k公共字串的个数。

Whu1084 http://acm.whu.edu.cn/oak/problem/problem.jsp?problem_id=1084

题目大意:求最长不重叠重复字串的长度和个数。

Toj2171 http://acm.tju.edu.cn/toj/showp2171.html

题目大意:统计每子串可分成可分成多少个重复串
http://acm.pku.edu.cn/JudgeOnline/problem?id=2758
题目大意:给定一个串,要求支持两种操作:插入单个字符或者询问从两个位置开始的最大匹配长度。

 

spoj694(后缀数组
求一个字串的不同子串个数.
rank考虑子串.加入子串S[i],获得了len-Sa[i]个不同子串.但其中height[i]个已经属于S[i-1],所以实际子串数增加了len-Sa[i]-S[i-1]. 顺序扫一遍height数组即得解.

spoj687(后缀数组
求一个串的重复次数最多的连续重复子串.
设周期为L的连续重复子串存在,则点0,L,2L,...,kL必能覆盖到一个完整周期因此对L,考察这些点的字符相等情况,LCP情况,可得到L的解.
枚举L.
复杂度是O(n/1+n/2+...+n/n) = O(nlogn)

pku3693(后缀数组
spoj687,只是结果还要输出字典序最小的满足条件的串.可以借助rank数组直接比较字典序.只是要注意在考察点kL,要把以(k-1)L+1,...,(k+1)L-1为起点的子串都访问一遍找最小rank.

pku1743(后缀数组
找一个串的最长不重叠相同子串.
由于某子串可能整体加上(或减去)相同偏移量,因此不直接对原串操作,而是构造新串b, 其中b[i]=a[i]-a[i-1]. 此时求得最长不重叠相同子串的长度+1便是结果.
可以二分长度,或者栈扫描(*)直接求最大长度.

whu1084(后缀数组
求重复次数最多的不重叠子串长度
spoj687的简单版,不要求循环节连续,直接二分长度即可.

pku2778(多串匹配+DP AC自动机,trie
字符集大小为4, 给出m(m<=10)禁止单词(长度<=10), 求长度为n(n<=2*10^9)的不包含任何禁止单词的串的个数.
对禁止单词建立trie,并计算出图中任意合法结点之间的转移数,这样便求得1步转移矩阵.
n次方后的矩阵,1行中属于合法状态的元素之和即为解.
禁止单词总长度不超过100,因此合法状态亦<100.总复杂度100^3*logN

zju3228() Searching the String 后缀数组,AC自动机,trie
原串长10^5, 现在有10^5次查询每次查询一个长度<=6的模式串在原串中的最大匹配次数.
模式串的匹配方式有可重叠和不可重叠两种需针对查询的类型返回相应值.
后缀数组解法(在线):
对原串建立saheight数组.由于模式串长度最大只有6, 我们可以将height数组分别按L=1..6分组,预处理求出相应长度每组内不重叠子串的最大匹配次数,此过程O(6*nlogn).
另外由于sa数组将所有后缀按字典序排好了,所以对一个询问可以二分找到它在sa中第一次出现的位置p1和最后一次出现的位置p2, p2-p1+1就是可重叠匹配的答案对不可重叠匹配,只需直接返回p1处预处理时的值每次查询O(logn).
trie,AC自动机解法(离线):
把所有查询建trie对图中的每个有效结点维护:该串长度,两类查询的计数,该串上一次被匹配的位置还要用个链表记下这个串属于哪些查询.
剩下的就是经典的自动机多串匹配了.


(*)关于栈扫:
height数组具有区间性,各个不同前缀被相应的极小值隔开,而一个区间中又有多个子区间.各区间值大于区间端点的部分互不影响.因此可以维护一个存放height值不减的栈,栈中每个元素的附属值记录了它在栈中相邻的两个元素为端点的连续区间内所有height值不小于它的必要信息.比如此题要记录height>=k的连续区间内sa[i] 的最大值和最小值.
栈扫描的经典例子移步pku2559.


(**)trie图备忘:
trie树多了个后缀指针psuf. 设当前结点字母为c, psuf指向父亲的后缀的pch[c].
trie树中的后代结点指针pch(已经更名为状态转移指针),当相应后代存在时,指向后代;否则指向当前结点的后缀的相应后代,pch[k]=node[pa].pch[k].
后缀指针在接下来的状态转移中,当前结点与它的后缀结点等价.
后代结点指针在当前状态下,接收到字符ch,转移到pch[ch]指向的结点.

 

你可能感兴趣的:(ACM-训练题单,ACM-字符串处理)