最小表示法使得一个环形字符串有唯一的读法,这个读法是所有读法中字典序最小的。
本文讲解的是最小表示法的O(n)求法。
对于这种环形问题,一个常规的做法是把它自己复制一遍,接到原串之后。问题就转换成了求在字符串s中长度为n的最小子串。
先思考暴力做法。找到所有同构串,两两间比较一下,找到最小的。
优一点的做法,存下一个最小的同构串,逐一比较,如果有更小的则替换之。即便是这样时间复杂度O(n*|S|)。
想到这,其实已经命中了正解的一半。想要提速,最根本的办法就是减少无用状态空间的遍历。
再仔细思考暴力的过程。假设在比较s[i~i+len]和s[j~j+len]中s[i+k]与s[j+k]不同(假设s[i+k]<s[j+k]),按暴力,我们仅仅得到s[i~i+len]小于s[j~j+len],然后舍弃j,选择i。深入地再想想,s[i+1~i+1+len]与s[j+1~j+1+len]的大小关系呢?因为s[i+1~i+k-1]与s[j+1~j+k-1]是相同的,而s[i+k]<s[j+k],所以s[i+1~i+1+len]小于s[j+1~j+1+len]。所以s[j+1~j+1+len]这个状态我们可以直接跳过。
像这样可以跳过的从s[j+1~j+1+len]一直到s[j+k~j+k+len]。我们可以对应的从s[i+1~i+1+len]到s[i+k~i+k+len]中找到比前者小的同构串。
举个例子,看图。
假设出现了(实际并不会这样匹配)i=1,j=4,k=2。现在s[i+k]>s[j+k],那么①>④。按照上面所说的,②、③都是可以直接跳过的,因为分别有⑤<②、⑥<③,②、③绝不肯能为最小表示法。所以i可以直接跳到i+k+1的位置。
实现时i、j就成了两个指针,表示同构串的起始位置。还要注意几处特殊的地方,看代码注释。
例题(bzoj2882 工艺)
代码
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=300010;
int n;
int a[2*maxn];//2倍空间
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
a[i+n]=a[i];
}
int i,j,k;
for(i=1,j=2;i<=n && j<=n;)
{
for(k=0;k<n && a[i+k]==a[j+k];k++);//寻找不相同的位置
if(k==n) break;//由一个相同元素构成
if(a[i+k]<a[j+k])
{
j=j+k+1;
if(i==j) j++;//i和j相同时有一个数要+1
}
if(a[i+k]>a[j+k])
{
i=i+k+1;
if(i==j) i++;//i和j相同时有一个数要+1
}
}
int begin=i>n?j:i;//未超出n的是最终胜者
for(i=0;i<n;i++) printf("%d ",a[begin+i]);
return 0;
}