[NOI2016]品酒大会(后缀数组+并查集)
题面
给出一个 长度为 n 的字符串,每一位有一个权值 val。定义两个位字符为 r 相似,是指分别从这两个字符开始,到后面的 r 个字符都相等。两个 r 相似的字符还有一个权值为这两个字符权值的乘积。问对于 (r in [0,n-1]),统计出有多少种方法可以选出 2 个“r 相似”的字符,并回答选择 2 个”r 相似”的字符可以得到的权值的最大值。
分析
先求出这个串的height数组。对于某个(r),height数组上值(geq r)的位置一定会形成若干个连续区间。这些区间里面任意选两个后缀的LCP都(geq r),即它们是"(r)相似的"。
那么我们从大到小枚举(r),对于每个(r),我们把height值为r的位置与相邻位置合并。那么这些新合并的位置与原来位置的LCP都是r。我们可以用并查集维护集合大小,集合中后缀对应的最小值与最大值,合并的时候就可以更新方案数(加上集合大小乘积),以及乘积最大值(由于可能存在负数,所以要维护最小值,可能两个负的最小值之积更大)。
细节见代码。
代码
//https://www.luogu.com.cn/problem/P2178
#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define maxs 128
#define maxn 300000
using namespace std;
typedef long long ll;
void rsort(int *ans,int *fi,int *se,int n,int m){
static int buck[maxn+5];
for(int i=0;i<=m;i++) buck[i]=0;
for(int i=1;i<=n;i++) buck[fi[i]]++;
for(int i=1;i<=m;i++) buck[i]+=buck[i-1];
for(int i=n;i>=1;i--) ans[buck[fi[se[i]]]--]=se[i];
}
int sa[maxn+5],rk[maxn+5],height[maxn+5];
void suffix_sort(char *s,int n,int m){
static int se[maxn+5];
for(int i=1;i<=n;i++){
rk[i]=s[i];
se[i]=i;
}
rsort(sa,rk,se,n,m);
for(int k=1;k<=n;k*=2){
int p=0;
for(int i=n-k+1;i<=n;i++) se[++p]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) se[++p]=sa[i]-k;
rsort(sa,rk,se,n,m);
swap(rk,se);
rk[sa[1]]=1;
p=1;
for(int i=2;i<=n;i++){
if(se[sa[i-1]]==se[sa[i]]&&se[sa[i-1]+k]==se[sa[i]+k]) rk[sa[i]]=p;
else rk[sa[i]]=++p;
}
if(p==n) break;
m=p;
}
}
void get_height(char *s,int n,int m){
suffix_sort(s,n,m);
for(int i=1;i<=n;i++) rk[sa[i]]=i;
int k=0;
for(int i=1;i<=n;i++){
if(k) k--;
int j=sa[rk[i]-1];
while(s[i+k]==s[j+k]) k++;
height[rk[i]]=k;
}
}
int n;
char str[maxn+5];
int val[maxn+5];
ll cnt[maxn+5];
ll mul[maxn+5];//i相似的方案数和最大乘积
int fa[maxn+5];
ll sz[maxn+5],maxv[maxn+5],minv[maxn+5];
//集合大小,最大值,最小值
int find(int x){
if(fa[x]==x) return x;
else return fa[x]=find(fa[x]);
}
void merge(int x,int y){
int fx=find(x),fy=find(y);
if(fx==fy) return;
int r=height[y];//按照height从大到小合并,lcp一定是当前height值
cnt[r]+=sz[fx]*sz[fy];
mul[r]=max(mul[r],max(maxv[fx]*maxv[fy],minv[fx]*minv[fy]));//可能有负数,所以两个最小值相乘也可以很大
maxv[fy]=max(maxv[fx],maxv[fy]);
minv[fy]=min(minv[fy],minv[fx]);
sz[fy]+=sz[fx];
fa[fx]=fy;
}
vector<int>pos[maxn+5];//每个height值在height数组上对应的下标(rank)
int main(){
scanf("%d",&n);
scanf("%s",str+1);
for(int i=1;i<=n;i++) scanf("%d",&val[i]);
get_height(str,n,maxs);
for(int i=1;i<=n;i++){
fa[i]=i;
sz[i]=1;
maxv[i]=minv[i]=val[sa[i]];//按照height数组的顺序,即排名合并
}
memset(cnt,0,sizeof(cnt));
memset(mul,0xcf,sizeof(mul));
for(int i=1;i<=n;i++) pos[height[i]].push_back(i);
for(int i=n-1;i>=0;i--){//按照height从大到小合并
for(int j=0;j<(int)pos[i].size();j++){
int x=pos[i][j];
merge(x-1,x);//把height>=i的相邻值并成连续区间
}
}
for(int i=n-2;i>=0;i--){
cnt[i]+=cnt[i+1];
mul[i]=max(mul[i],mul[i+1]);
//求后缀和
//我们合并时求出的是lcp恰好为i的答案,而题目求的是lcp>=i的答案
}
for(int i=0;i<=n-1;i++){
printf("%lld ",cnt[i]);
if(cnt[i]!=0) printf("%lld
",mul[i]);
else printf("0
");
}
}