P2178 品酒大会题解
提供一个切入点不同的后缀数组做法。
- 事先声明:由于我需要利用
r 表示区间右端点,所以题目中的r 相似在本文中均表示为k 相似
首先考虑,假设我们知道一些后缀是两两
显然,对于方案数,其贡献为
而找到两两
那么我们可以使用线段树,维护区间最大值、次大值、最小值、次小值。即可在区间已知的情况下
tree merge(tree x,tree y){
if(y.maxn>x.maxn)x.smax=x.maxn,x.maxn=y.maxn;
else if(y.maxn>x.smax)x.smax=y.maxn;
if(y.smax>x.maxn)x.smax=x.maxn,x.maxn=y.smax;
else if(y.smax>x.smax)x.smax=y.smax;
if(y.minn<x.minn)x.smin=x.minn,x.minn=y.minn;
else if(y.minn<x.smin)x.smin=y.minn;
if(y.smin<x.minn)x.smin=x.minn,x.minn=y.smin;
else if(y.smin<x.smin)x.smin=y.smin;
return x;
}
现在考虑第二个问题:找区间+统计答案
后缀数组有一个比较常见的策略,即基于
复杂度:枚举长度
很多题解到这里选择倒序枚举、合并区间,但毕竟咱不一定能想到嘛,所以我们仍然顺序枚举、分裂区间。
考虑我们的复杂度瓶颈在哪里。那么分组和枚举的复杂度至少要抠掉一个。
发现一个比较显然的性质:
- 若区间
[l,r] 是k 相似的,且后缀p 不在其中,那么这个区间的k+1 相似,p 一定不在其中。
很好理解,这个基于后缀数组的定义,或者
这个性质告诉我们:找到一个区间后,不用考虑区间外的数对这个区间分组的影响了。
这是一个很有启发性的结论。那么考虑如何对一个区间重分组。
- 性质:从
k 相似到k+1 相似,区间[l,r] 要么不变,要么被分成多段,且每段起始位置t 满足t=l 或height_t=k 。
这个可以根据分组定义直接证得。
根据这两个性质,我们可以得到一种高效的重分组方式:
-
假设区间
[l,r] 是k 相似的,那么我们找到[l+1,r] 中所有height=k 的位置,外加l 和r ,将整个区间分段,处理每一段的k+1 相似问题。 -
现在考虑如何找到区间内所有
我们对每个
此外,我们舍弃无法切割的继承,每次直接找到下一个会产生新的切割区间的相似度进行转移。这一段的区间相同,贡献一定相同,放在答案区间上等同于区间加和区间取
至于找到下一个切分点,其实就是
这样划分区间的复杂度看似很玄学,但是可以证明是
证明:考虑每个区间。长度上升时,要么新区间直接继承,要么区间被切割成若干段。结束的情况有两种,即
对每个区间处理单次复杂度
对于边界细节,很多题解都有强调,如各类初值
此外,考虑到标记永久化比标记下放好写,所以我对答案线段树采用标记永久化的方式统计答案。
因为同时用了三棵树,所以代码可能略长。在必要的地方添加注释了。
#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<ctime>
const int inf=1<<30;
const long long inff=1ll<<60;
using namespace std;
struct treap{
int son[2],val;
long long key;
}trp[500001];
struct tree{
long long maxn,smax;
long long minn,smin;
//smax/min 表示次大/次小值。
}tre[4000001];
struct ans{
long long tag,rev;
//tag 维护方案数询问,rev 维护最值询问。
}tr[4000001];
int root[400001],size;
//每个 root 代表 height 的一种取值,平衡树内维护下标。
//treap 部分
void rotate(int &k,int fx){
int x=trp[k].son[fx^1];
trp[k].son[fx^1]=trp[x].son[fx];
trp[x].son[fx]=k;
k=x;
}
void insert(int &k,int val){
if(!k){
k=++size;
trp[k].key=rand();
trp[k].val=val;
return;
}
if(trp[k].val==val)return;
else if(trp[k].val>val){
insert(trp[k].son[0],val);
if(trp[k].key<trp[trp[k].son[0]].key)rotate(k,1);
}
else{
insert(trp[k].son[1],val);
if(trp[k].key<trp[trp[k].son[1]].key)rotate(k,0);
}
}
int qlas(int id,int val){
int x=root[id],res=0;
while(x){
if(trp[x].val<=val)x=trp[x].son[1];
else res=trp[x].val,x=trp[x].son[0];
}
return res;
}
//后缀数组全家桶
int n,sa[500001],rk[500001],height[500001];
int tsa[500001],trk[1000001],cnt[500001];
int a[500001],st[500001][20],lg[500001];
char s[500001];
long long max(long long x,long long y){
return x>y?x:y;
}
long long min(long long x,long long y){
return x<y?x:y;
}
int qmin(int l,int r){
int k=lg[r-l+1];
return min(st[l][k],st[r-(1<<k)+1][k]);
}
tree merge(tree x,tree y){
if(y.maxn>x.maxn)x.smax=x.maxn,x.maxn=y.maxn;
else if(y.maxn>x.smax)x.smax=y.maxn;
if(y.smax>x.maxn)x.smax=x.maxn,x.maxn=y.smax;
else if(y.smax>x.smax)x.smax=y.smax;
if(y.minn<x.minn)x.smin=x.minn,x.minn=y.minn;
else if(y.minn<x.smin)x.smin=y.minn;
if(y.smin<x.minn)x.smin=x.minn,x.minn=y.smin;
else if(y.smin<x.smin)x.smin=y.smin;
return x;
}
void build(int k,int l,int r){
tre[k].maxn=tre[k].smax=-inf;
tre[k].minn=tre[k].smin=inf;
tr[k].rev=-inff;
//不处理 0 相似答案很大一个原因就是两个线段树的结构相同,可以少打一个 build
if(l==r){
tre[k].maxn=tre[k].minn=a[sa[l]];
return;
}
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
tre[k]=merge(tre[k<<1],tre[k<<1|1]);
}
tree query(int k,int l,int r,int x,int y){
//这四个查询会相互影响,不方便单独查询。
//所以我们直接把区间对应的线段树提取出来。
if(l>=x&&r<=y)return tre[k];
int mid=l+r>>1;
if(mid>=y)return query(k<<1,l,mid,x,y);
if(x>mid)return query(k<<1|1,mid+1,r,x,y);
return merge(query(k<<1,l,mid,x,y),query(k<<1|1,mid+1,r,x,y));
}
void modify(int k,int l,int r,int x,int y,long long w1,long long w2){
//对答案线段树的更新,w1 是方案,w2 是最值
if(l>=x&&r<=y){
tr[k].tag+=w1;
tr[k].rev=max(tr[k].rev,w2);
return;
}
int mid=l+r>>1;
if(x<=mid)modify(k<<1,l,mid,x,y,w1,w2);
if(mid<y)modify(k<<1|1,mid+1,r,x,y,w1,w2);
}
long long qmax(int k,int l,int r,int x){
if(l==r)return tr[k].rev;
int mid=l+r>>1;
long long res=tr[k].rev;
if(x<=mid)res=max(res,qmax(k<<1,l,mid,x));
else res=max(res,qmax(k<<1|1,mid+1,r,x));
return res;
}
long long qsum(int k,int l,int r,int x){
if(l==r)return tr[k].tag;
int mid=l+r>>1;
long long res=tr[k].tag;
if(x<=mid)res+=qsum(k<<1,l,mid,x);
else res+=qsum(k<<1|1,mid+1,r,x);
return res;
}
void solve(int l,int r,int minx){
//主体部分。表示当前处理区间 [l,r] 是 minx 相似的。
if(l>=r||minx==n)return;
tree rst=query(1,1,n,l,r);
int nxt=qmin(l+1,r);
//直接找到下一个切点长度
long long res=max(rst.maxn*rst.smax,rst.minn*rst.smin),as=1ll*(r-l+1)*(r-l)/2;
if(minx)modify(1,1,n,minx,nxt,as,res);
//对 0 相似单独解决原因见 build 函数。
//0 相似是整个区间,可以 O(1) 解决。
int ps=l;
while(ps<=r){
int t=qlas(nxt,ps);
if(!t||t>r)t=r+1;
solve(ps,t-1,nxt+1);
ps=t;
}
}
int main(){
srand((unsigned)time(NULL));
int i,j;
scanf("%d%s",&n,s+1);
for(i=1;i<=n;i++)scanf("%d",&a[i]);
for(i=1;i<=n;i++)rk[i]=s[i]-'a'+1,cnt[rk[i]]++;;
for(i=1;i<=26;i++)cnt[i]+=cnt[i-1];
for(i=n;i;i--)sa[cnt[rk[i]]--]=i;
for(i=1;i<=n;i++){
if(s[sa[i]]==s[sa[i-1]])rk[sa[i]]=rk[sa[i-1]];
else rk[sa[i]]=rk[sa[i-1]]+1;
}
for(j=1;j<n;j*=2){
memcpy(tsa,sa,sizeof(sa));
memcpy(trk,rk,sizeof(rk));
int tot=0;
for(i=n;i>n-j;i--)sa[++tot]=i;
for(i=1;i<=n;i++)if(tsa[i]>j)sa[++tot]=tsa[i]-j;
for(i=1;i<=n;i++)rk[i]=trk[sa[i]];
memset(cnt,0,sizeof(cnt));
for(i=1;i<=n;i++)cnt[rk[i]]++;
for(i=1;i<=n;i++)cnt[i]+=cnt[i-1];
for(i=n;i;i--)tsa[cnt[rk[i]]--]=sa[i];
memcpy(sa,tsa,sizeof(sa));
for(i=1;i<=n;i++){
if(trk[sa[i]]==trk[sa[i-1]]&&trk[sa[i]+j]==trk[sa[i-1]+j])rk[sa[i]]=rk[sa[i-1]];
else rk[sa[i]]=rk[sa[i-1]]+1;
}
if(rk[sa[n]]==n){
for(i=1;i<=n;i++)sa[rk[i]]=i;
break;
}
}
int len=0;
for(i=1;i<=n;i++){
if(len)len--;
while(s[i+len]==s[sa[rk[i]-1]+len])len++;
height[rk[i]]=len;
}
for(i=1;i<=n;i++)insert(root[height[i]],i);
for(i=1;i<=n;i++)st[i][0]=height[i];
for(i=2;i<=n;i++)lg[i]=lg[i/2]+1;
for(i=1;i<=lg[n];i++)for(j=1;j+(1<<i)-1<=n;j++)st[j][i]=min(st[j][i-1],st[j+(1<<i-1)][i-1]);
build(1,1,n);
solve(1,n,0);
tree ss=query(1,1,n,1,n);
printf("%lld %lld\n",1ll*n*(n-1)/2,max(ss.maxn*ss.smax,ss.minn*ss.smin));
for(i=1;i<n;i++){
long long num=qsum(1,1,n,i);
printf("%lld %lld\n",num,num?qmax(1,1,n,i):0);
//有方案,我们输出方案;没有方案,我们输出约定的 0
}
}