ACM-ICPC 2017 Asia Qingdao Suffix
题目链接:
https://vjudge.net/problem/计蒜客-A1428
给定\(n\)个非空字符串\(s_1,s_2,...,s_n\),你需要给每个字符串截取一个非空的后缀\(suf_1,suf_2,...,suf_n\),并且按照顺序拼接,求字典序最小的拼接方案。其中,所有的字符串长度之和不超过\(5e5\)
本着非多项式复杂度化多项式复杂度,高次多项式复杂度化\(O(n)\)或者\(O(nlogn)\)的思想,先看看这个题有没有非指数级搜索的做法。
字典序是从前往后比较的,但是我们的贪心法必须从后往前,从前往后选会出现一些很尴尬的情况,可以看看样例。在一个字符串前缀固定的前提下,后缀的字典序越小,则整个字符串的字典序越小。假设我们确定了\(suf_1,suf_2,...,suf_{n-1}\),需要选择\(suf_n\),我们可以果断的挑选一个\(s_n\)字典序最小的后缀。这个意思是说我们不需要知道\(suf_1,suf_2,...,suf_{n-1}\),就可以确定\(suf_n\)。枚举即可得出\(suf_n\)。
然后我们希望\(suf_{n-1}+suf_n\)的字典序最小,这个也可以通过枚举\(s_{n-1}\)的后缀得出。
以此类推,我们可以求出最终的答案。
这个方法的复杂度是\(O(n^2)\),它复杂在已经确定\(suf_{k+1},suf_{k+2},...,suf_{n}\)的基础上,枚举\(s_k\)的后缀时,一共有\(|s_k|\)个可选项,而且两个字符串比较字典序大小的复杂度是\(O(n)\)的,故总共是\(O(n^2)\)的。
回想上C语言课的时候,老师大概率会讲从\(n\)个不同的数\(a_0,a_2,...,a_{n-1}\)中找到最小的那个数的角标的做法:
int target=0;
for(int i=1;i<n;i++)
{
if(a[target]>a[i])target=i;
}
//target即为所求
由于字典序是线序关系,和数字是一样的,因此求这个最小后缀的时候,也是同样的做法
代码如图所示:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
string s[500010];
int main()
{
int T;
cin>>T;
while(T--)
{
int n;
cin>>n;
for(int i=1;i<=n;i++)cin>>s[i];
string ans;
for(int k=n;k>0;k--)
{
string str=s[k]+ans;
int len=str.size();
int target=0;
for(int i=1;i<s[k].size();i++)
{
string s1=str.substr(target,len-target);
string s2=str.substr(i,len-i);
if(s1>s2)target=i;
}
ans=str.substr(target,len-target);
}
cout<<ans<<endl;
}
}
当然,这个代码按理来说是不能过的,但是测评网站上的数据可以过,有兴趣的话可以交一发试试。
用
1
500000
a
a
...
a
这个数据应该就可以卡掉,但是出题人好像没有这么想过。
这个程序的瓶颈在于
1.频繁拼接字符串,这很慢
2.字符串比较很慢
由于我们是已知后缀,把前缀拼接在后缀的前面,我们有两种简单的方法
1.开一个长度\(5e5\)的数组,把目前得到的后缀尽量靠后放,拼接的时候后缀的前面一定是有空间的
2.倒着存放后缀,这样在内存中是按照顺序依次存放的
比较字符串字典序是一个比较麻烦的地方,我们需要\(LCP\)的思想
给定字符串\(s1\),\(s2\),我们可以先求出它们的最长公共前缀(\(LCP\)),在前缀的后面的那个字符决定了它们的大小(结尾可以是'\0',没有比'\0'大的字符)
如\(abababbaabbababba\),比较角标从\(0\)开始的后缀和角标从2开始的后缀的字典序大小
它们的\(LCP\)为\(abab\),前者\(LCP\)后面的字母是\(a\),后者是\(b\),所以前者小于后者。
比较两个子串是否相等可以用字符串哈希的方式解决,哈希方法可以参考代码。
没有接触过哈希的同学一般会觉得这个东西玄学,但是一般来说,哈希不容易被卡掉,可以放心使用。
哈希的基本操作是把字符串的'a,‘b',...,'z'替换成\(1,2,...,26\)(不可以是\(0,1,2,...,25\)),然后设定一个进制,比如\(131\)(一般是质数),然后模一个大数,比如\(2^{64}\)。哈希可以正着做,也可以倒着做,因为本题是从后往前进行贪心,所以我的哈希都是倒着的。
计算一个子串哈希值的复杂度为\(O(1)\),我们可以二分这两个串的\(LCP\)的大小,用\(O(logn)\)次比较算出\(LCP\)
同理,计算字符串匹配、后缀排序的问题在时间复杂度允许的前提下都可以通过字符串哈希加\(LCP\)的办法实现。
附上代码:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<vector>
using namespace std;
int add[500010],len[500010];
char s[1000010];
typedef unsigned long long ull;
ull power[500010];
ull has[500010],base=131;
int total=0;
char ans[500010];
ull cal_has(int L,int R)
{
return has[R]-has[L-1]*power[R-L+1];
}
int lcp(char *st,int x,int y)
{
int L=0,R=min(x,y);
while(L+1<R)
{
int mid=(L+R)/2;
if(cal_has(x-mid+1,x)==cal_has(y-mid+1,y))L=mid;
else R=mid;
}
if(cal_has(x-R+1,x)==cal_has(y-R+1,y))return R;
else return L;
}
int main()
{
power[0]=1;
for(int i=1;i<=500000;i++)power[i]=power[i-1]*base;
int T;
scanf("%d",&T);
while(T--)
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
add[i]=add[i-1]+len[i-1]+1;
scanf("%s",s+add[i]);
len[i]=strlen(s+add[i]);
}
total=0;//已经确定的字符串后缀,初始化
for(int i=n;i>0;i--)
{
char *st=s+add[i];
int temp=total;
for(int j=len[i]-1;j>=0;j--,temp++)
{
has[temp+1]=has[temp]*base+(st[j]-'a'+1);
ans[temp+1]=st[j];
}
int tar=0;//最小的后缀的角标
for(int j=1;j<len[i];j++)
{
int maxn=lcp(st,total+len[i]-tar,total+len[i]-j);
if(ans[total+len[i]-(tar+maxn)]>ans[total+len[i]-(j+maxn)])tar=j;
}
total+=len[i]-tar;
}
for(int i=total;i>0;i--)printf("%c",ans[i]);
printf("\n");
}
}