字符串的最小表示法及实现

先以一道题目开头:

USACO 5.4 Hidden password

链接:http://www.nocow.cn/index.php/Translate:USACO/hidden

题目大意:找出一个字符串经过循环移位后生成的字符串中字典序最小的串,如果有多个,取移位最少的。原串为s,长度为n.

这里要求的其实就是这个字符串的最小表示。最小表示的思想来源于“序”的概念。比如有两个整数集合,为了判断两个集合是否相等,我们可以把每个集合中的数先排序,如果排序后的数列相等,则集合相等。这里我们认为排序后的数列是“最小序”,用以代表一个集合。同理,最小表示法在判断两个字符串是否循环同构的时候也可以起到类似的作用。更具体的说明,请参考IOI2003周源《浅析“最小表示法”思想在字符串循环同构问题中的应用》一文。在那篇文章中,作者给出了一个基于最小表示的思想O(n)时间判断两个串是否循环同构的算法。注意,这里说只是基于最小表示思想,而实际上,算法最后并不一定能求出最小表示。但其实只要稍微修改一下即可。

首先把原串s复制一份并连接形成新的s。这样问题转化为求一个字典序最小且长度为n的子串。引入i,j指向待比较的两个字串。初始时i=0,j=1。比较以ij开头的字符串大小(记为sisj)。记sisj最长公共前缀长度为k,在k<n的情况下,如果si>sj,i+=(k+1),否则j+=(k+1)。之所以可以这样,简略的说,是因为si的长为(k+1)的前缀的每个后缀都大于sj的相应串,因此从那些位置开始一定不能构成最小表示!(更具体的说明请参考周源的论文)这样可以保证i,j的每次移动一定不会错过最小表示的起始位置(设为t)。再考虑k>=n的情况,这种情况,要么是i等于t,要么是i!=t并且i==j。前一种情况下我们找到了解,后一种情况下,令j++,即回到了之前的情况(仍没有错过t!)。但可能在第一种下,i也等于j,但是i一旦到t位置就不会再移动,因此当作第二种情况处理,只要在j超过n之后终止即可。

举个例子,s=abaab,n=5

过程:先复制,s=abaababaabi=0,j=1

第一轮:s0>s1,k=0 =>j+=1 =>i=0,j=2

第二轮:s0<s2,k=1=>i+=2=>i=2,j=2,因为i==j,所以j++  =>i=2,j=3

注意这时i已经到了最小表示的起始点,最小表示为aabab,因此再往后si始终小于sj了,j>=5后算法终止,此时i=2为所求解。

上面这种算法复杂度显然是O(n)的。注意,这与Nocow上面说的Maigo的那个程序有一点不同的。那个程序不是严格的O(n),99999b+1a的情况下TLE。而官方标程给出了另一种有点像动态规划思想的算法,详见北极天南星的博客。这个算法的基本思想是,把原串复制两份后,再末尾加一个非常大的终结符,设新串为SS。那么问题可以转化为求SS的最小后缀!而最小后缀用后缀数组可以NlogN时间求,而这个算法的时间只用O(N)。但要注意的是,这个算法只有在结尾字符是整个串中惟一最大字符时找出的才是最小后缀!(具体分析见后文)这个问题网上的文章中都没说清楚,搞得我纠结了好久。

算法用一个数组V(1..n),对于一个长度为n的主串s,设v[i]表示子串s[i..i+v[i]-1],该子串的所有前缀在主串中所有相应长度的子串中字典序最小。然后用一个队列记录当前V[i]最大的那几个位置。本质上来说,这个过程是形成了很多个区间,如果两个区间相邻便可以合并,在没有相邻区间的情况下则逐个字符扩展。每一轮都会淘汰一些非最长的区间,最后剩下的则是答案。更详细的叙述请参考北极天南星的博客。我们看一个由na构成的串,则队列中区间的长度每轮依次为1,2,4,8....n,这个级数的求和仍是O(n)。而更一般的情况下的复杂度证明,我和chong_boy讨论了很久,主要是要基于一个特点,就是区间在扩展过程中不会出现交叠,否则就会合并在一起而成为一个新区间了(这一点也没有严格证明,但大致上应该是的)。这样的话,每个字符最多只会被扩展一次,每个区间最多也只会被连接一次,而整个程序在循环中要么扩展要么连接要么出队,每个居间最多出队n次,因此程序的均摊复杂度是O(n)

再回到之前说的最小前缀问题,假设对于一个结尾字符不是最大字符的串,比如aaaa,那么这个算法最终留在队列中的区间是aaaa,而实际上最小后缀是a。这种情况只会出现在一个后缀被另一个后缀包含的情况。而如果加上一个串尾最大字符,则不会出现包含情况。这个算法实际上也是在O(n)的时间给出了最小表示,与上一中算法异曲同工,但编程复杂度要高很多,也难理解很多!但是,添加串尾符的思想还是很值得学习的!

后缀数组能不能做,下篇文章再写,敬请期待,欢迎提出宝贵意见!

没有更多推荐了,返回首页