1031: [JSOI2007]字符加密Cipher
Time Limit: 10 Sec
Memory Limit: 162 MB
Submit: 5473
Solved: 2261
[ Submit][ Status][ Discuss]
Description
喜欢钻研问题的JS同学,最近又迷上了对加密方法的思考。一天,他突然想出了一种他认为是终极的加密办法
:把需要加密的信息排成一圈,显然,它们有很多种不同的读法。例如下图,可以读作:
JSOI07 SOI07J OI07JS I07JSO 07JSOI 7JSOI0把它们按照字符串的大小排序:07JSOI 7JSOI0 I07JSO JSOI07
OI07JS SOI07J读出最后一列字符:I0O7SJ,就是加密后的字符串(其实这个加密手段实在很容易破解,鉴于这是
突然想出来的,那就^^)。但是,如果想加密的字符串实在太长,你能写一个程序完成这个任务吗?
Input
输入文件包含一行,欲加密的字符串。注意字符串的内容不一定是字母、数字,也可以是符号等。
Output
Sample Input
JSOI07
Sample Output
I0O7SJ
HINT
对于100%的数据字符串的长度不超过100000。
Source
[ Submit][ Status][ Discuss]
题解:后缀数组。
后缀:后缀是指从某个位置i开始到整个字符串末尾结束的一个特殊子串。字符串S的从i开关的后缀表示为Suffix(S,i),也就是Suffix(S,i)=S[i…len(S)]。
所谓后缀数组,主要就是sa,rank
sa[i]表示排名为i的后缀的起点下标
rank[i]表示以第i位为起点的后缀的优先级
所以可知两个互为逆运算,知道一个后就可以o(n)的时间求出另一个。
要了解后缀数组就有先了解计数排序 http://baike.baidu.com/view/1209480.htm
下面进入正题: 倍增法构造后缀数组
例如一个长为2^k的后缀,可以把它分成前2^k-1,和后2^k-1两部分,我们可以把这两部分当成第一关键字和第二关键字来排序。
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define N 200003
using namespace std;
char ch[200003];
int a[N],v[N],sa[2][N],rk[2][N];//sa[i]表示排名为i的后缀的起点下标,rk[i]表示第i位的后缀的优先级
//之所以有两维是因为2^k是由两段2^k-1推来的,所以两维分别表示的是两段
int n,m,k;
void calcsa(int sa[N],int rank[N],int SA[N],int Rank[N])
{
for(int i=1;i<=n;++i)v[rank[sa[i]]]=i;//v[i]表示排名为i的后缀,最靠后的位置在哪一位
for(int i=n;i>=1;--i)
if(sa[i]>k)
SA[v[rank[sa[i]-k]]--]=sa[i]-k;
for(int i=n-k+1;i<=n;++i)
SA[v[rank[i]]--]=i;
for(int i=1;i<=n;++i)
Rank[SA[i]]=Rank[SA[i-1]]+(rank[SA[i-1]]!=rank[SA[i]]||rank[SA[i-1]+k]!=rank[SA[i]+k]);//双关键字排序
}
void work()
{
int p=0,q=1;
for (int i=1;i<=n;i++) v[a[i]]++;//统计每个字符出现了多少次
for (int i=1;i<=256;i++) v[i]+=v[i-1];//统计出前缀和即可知道每个字符在串中的排名
for (int i=1;i<=n;i++)
sa[p][v[a[i]]--]=i;//因为每个字符不一定只出现了一次,但是不同的后缀不能用同样的排名,所以每次--,有效的防止了重复,并且保证了后面长度短的后缀排名更靠前
for (int i=1;i<=n;i++)//rk 与sa 互为逆运算,知道另一个就可以O(n)求出另一个
rk[p][sa[p][i]]=rk[p][sa[p][i-1]]+(a[sa[p][i-1]]!=a[sa[p][i]]);//如果相邻两个相同,那么他们的优先级也是相同的
k=1;//字符串的初始长度,因为是双关键字计数排序,所以刚开始sa[q]为0,相当于只看第一个关键字
while (k<n)
{
calcsa(sa[p],rk[p],sa[q],rk[q]);
p^=1; q^=1; k<<=1;
}
for (int i=1;i<=n;i++)
{
printf("%d ",sa[p][i]);
}
printf("\n");
for (int i=1;i<=n;i++)//sa[p]中存储的就是最终后缀数组的排名
{
if (sa[p][i]<=n/2)
printf("%c",ch[sa[p][i]+n/2-1]);//这里是为什么呢?以样例为例展环为链后为JSOI07JSOI07 ,那么根据排名sa[p]最终会变成 0077IIJJOOSS,因为是读最后一列,所以是sa[p][i]+n/2-1,即当前点的前一位,因为是环嘛,划一划就明白了。
}
}
int main()
{
scanf("%s",ch+1);
int len=strlen(ch+1);
for (int i=1;i<=len;i++)//展环为链
{
a[i]=int(ch[i]);
a[i+len]=a[i];
ch[i+len]=ch[i];
}
n=len<<1;
work();
}