问题:
给定一个字符串s[1...n],求它的所有后缀中最小的一个。(本文中字符串之“大”“小”均对字典序而言。)
算法:
定义v[1..n],其中v[i]=k表示s[i..i+k-1]在s的所有长度为k的字串中最小。我们求出每个v[i]最大的取值,再找到所有的v[i]中最大的一个,s[i..n]就是问题的答案。
基本思想是这样的(先不设计实现时的数据结构):先将所有的1..n都保存在一个列表里。每次扫描列表里所有的i,若其v[i+v[i]]>0,则可以将i+v[i]从列表里删去,v[i]+=v[i+v[i]];若其v[i+v[i]==0,则v[i]++当且仅当s[i+v[i]]在所有s[k+v[k]]中最小(其中k是当前列表里的值)。 然后删去所有链表中不等于v[i]最大值的v[i]。这基本上是一个不断合并/扩大区间的过程。当所有v[i]值不再变化时结束。
上述“列表”需要支持按下标查找、按下标删除、顺序枚举的操作。我采用数组模拟的双链表实现。(如果用单链表,在不知道前导节点编号的情况下,似乎无法实现“按下标删除”的功能。)
将完整的算法描述一遍。将1-n串在双链表里(也就是说next[i]=i+1,prev[i]=i-1之类)。每次循环时需要四次扫描链表里的每个i。第一遍,找到所有s[i+v[i]]中最小的。第二遍,考察每个v[i+v[i]],根据其大于0或者等于刚才得出的最小值进行前述赋值与删除的操作。第三遍,找到v[i]的最大值。第四遍,进行前述删除操作。(其中二、三两遍扫描可以一次完成。)当链表仅剩一个元素时就得出了答案。
实现中的一个技巧是在保存s的数组的最后一个元素之后的位置放上一个比所有字符都要小的值,可以防止越界。
算法的时间复杂度是O(n)。可以采用记账方法严格证明。但我现在没时间写下来。
示例程序:
hidden.cpp
(USACO Training Secition 5.5 PROB Hidden Passwords)