您的位置:首页 > 其它

BZOJ 3230 相似子串 后缀数组

2016-03-24 09:04 387 查看
之前觉得后缀数组谜一样的豆腐块代码很难理解,于是先去学了后缀自动机,然而这道题让我发现后缀数组还是很好用的工具QaQ。

关于这道题的每个询问首先要求出第i小和第j小的子串在原串的位置,然后快速地求出这两个子串的最长公共前缀和后缀。

第一个步骤我们可以用二分搞定,通过原串的height数组我们可以求出一个sum数组,sum[i]表示按照字典序排序后,前i个后缀中共有多少个本质不同的子串。在n个后缀中,每个子串都可以被表示为某个后缀的前缀,所以我们在sum数组中二分查找出第一个不小于k的sum[i]来求原串第k小的子串。那么第i个后缀的某个前缀就是所需的子串了,这个稍微计算一下得出。

我们得出了两个子串[la,ra]和[lb,rb],要求它们的最长公共前缀以及后缀,最长公共前缀就是height数组上的区间最小值问题了,那么后缀我们按照原串的反串建立后缀数组也就变成了height数组上的区间最小值问题了。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
#define SZ 100005
using namespace std;

int n, q, s1[SZ], s2[SZ];
int sa1[SZ], r1[SZ], h1[SZ], rmq1[SZ][18];
int sa2[SZ], r2[SZ], h2[SZ], rmq2[SZ][18];
long long sum[SZ];

void Read ()
{
scanf ("%d %d", &n, &q);
char c = getchar();
while (c < 'a' || c > 'z') c = getchar();
for (int i = 1; i <= n; i++)
{
s1[i-1] = s2[n-i] = c-'a'+1;
c = getchar();
}
}

int cmp (int *s, int i, int j, int l)
{return s[i]==s[j] && s[i+l]==s[j+l];}

void buildsa (int *s, int *sa, int *rank, int *height, int m)
{
n++;
int a[SZ], b[SZ], c[SZ], d[SZ];
int i, j, p, k = 0, *x = a, *y = b, *t;
for (i = 0; i < m; i++) c[i] = 0;
for (i = 0; i < n; i++) c[x[i]=s[i]]++;
for (i = 1; i < m; i++) c[i] += c[i-1];
for (i = n-1; i >= 0; i--) sa[--c[x[i]]] = i;

for (j = p = 1; p < n; j<<=1, m=p)
{
for (p = 0, i = n-j; i < n; i++) y[p++] = i;
for (i = 0; i < n; i++)
if (sa[i] >= j) y[p++] = sa[i]-j;
for (i = 0; i < n; i++) d[i] = x[y[i]];
for (i = 0; i < m; i++) c[i] = 0;
for (i = 0; i < n; i++) c[d[i]]++;
for (i = 1; i < m; i++) c[i] += c[i-1];
for (i = n-1; i >= 0; i--) sa[--c[d[i]]] = y[i];

for (t=x,x=y,y=t,p=1,x[sa[0]]=0,i=1; i < n; i++)
x[sa[i]] = cmp(y,sa[i-1],sa[i],j) ? p-1 : p++;
}

n--;
for (i = 1; i <= n; i++) rank[sa[i]] = i;
for (i = 0; i <  n; height[rank[i++]] = k)
for (k?k--:0, j=sa[rank[i]-1]; s[i+k]==s[j+k]; k++);
}

void BuildSa ()
{
buildsa(s1,sa1,r1,h1,27);
buildsa(s2,sa2,r2,h2,27);
sum[1] = n-sa1[1];
for (int i = 2; i <= n; i++)
sum[i] = sum[i-1]+(long long)(n-sa1[i]-h1[i]);
}

void BuildRmq ()
{
for (int i = 1; i <= n; i++)
rmq1[i][0] = h1[i], rmq2[i][0] = h2[i];
for (int i = 1; (1<<i) <= n; i++)
for (int j = 1; j+(1<<i)-1 <= n; j++)
rmq1[j][i] = min(rmq1[j][i-1],rmq1[j+(1<<i-1)][i-1]),
rmq2[j][i] = min(rmq2[j][i-1],rmq2[j+(1<<i-1)][i-1]);
}

void Query (long long k, int &lk, int &rk)
{
if (sum
< k) {lk = -1; return;}
int l = 1, r = n, mid;
while (l < r)
{
mid = (l+r)/2;
if (sum[mid] >= k) r = mid;
else l = mid + 1;
}
lk = sa1[l];
rk = sa1[l]+k+h1[l]-(l?sum[l-1]:0)-1;
}

int lcp (int l, int r, bool flag)
{
if (l == r) return 9999999;
if (flag)
{
l = r1[l], r= r1[r];
if (l > r) swap(l,r);
int k = 0; l++;
if (l == r) return rmq1[l][0];
while ((1<<k+1) <= r-l+1) k++;
return min(rmq1[l][k],rmq1[r-(1<<k)+1][k]);
}
else
{
l = r2[l], r= r2[r];
if (l > r) swap(l,r);
int k = 0; l++;
if (l == r) return rmq2[l][0];
while ((1<<k+1) <= r-l+1) k++;
return min(rmq2[l][k],rmq2[r-(1<<k)+1][k]);
}
}

void SolveQue ()
{
#define SKIP_THIS puts("-1");continue;
long long a, b, ansa, ansb, mxans;
int la, lb, ra, rb;
while (q--)
{
scanf ("%lld %lld", &a, &b);
Query (a, la, ra);
Query (b, lb, rb);
if (la < 0 || lb < 0) {SKIP_THIS}
mxans = (long long)(min(ra-la,rb-lb) + 1);
ansa = min((long long)lcp(la,lb,1), mxans);
ansb = min((long long)lcp(n-ra-1,n-rb-1,0), mxans);
printf ("%lld\n", ansa*ansa+ansb*ansb);
}
}

int main ()
{
Read();
BuildSa();
BuildRmq();
SolveQue();
return 0;
}



【蒟蒻的自述】

然而蒟蒻不理解后缀数组的豆腐块代码,所以一开始出了好多错误。

在前一部分计算sa数组的时候,为了防止出错所以在原串后加入一个原串没有的字符,并且它小于任何一个原串内的字符,所以传入的n实际是原串长度+1,并且原串s转换后最小字符不能是0,因为被加在末尾的字符是最小的。这也导致sa数组的有效位置是[1,n],而rank仍然是[0,n-1],不过我们仍然可以按照字面意思来理解它们,sa[i]表示排名第i的后缀的开始位置,rank[i]表示开始位置为i的后缀的排名,height[i]表示排名第i的后缀与排名第i-1的后缀的LCP。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: