首页 > 代码库 > [BZOJ]4199 品酒大会(Noi2015)

[BZOJ]4199 品酒大会(Noi2015)

  讲道理是后缀数组裸题吧,虽然知道后缀数组的原理但是小C不会写是什么鬼。。

  小C趁着做这题的当儿,学习了一下后缀数组。

  网络上的后缀数组模板完全看不懂怎么破,全程照着黄学长的代码抄,感觉黄学长写得还是很优雅的。

  求LCP的部分已经崩坏了,小C自己脑补的做法是。。倍增??

  看到正确的写法之后小C内心是绝望的,大致意思是:在原字符串中,设相邻两个后缀为Sx、Sx+1,那么有height[x+1]>=height[x]-1。(height数组就是小C代码里的tp数组)

 

Description

  一年一度的“幻影阁夏日品酒大会”隆重开幕了。大会包含品尝和趣味挑战两个环节,分别向优胜者颁发“首席品酒家”和“首席猎手”两个奖项,吸引了众多品酒师参加。在大会的晚餐上,调酒师 Rainbow 调制了 n 杯鸡尾酒。这 n 杯鸡尾酒排成一行,其中第 n 杯酒 (1 ≤ i ≤ n) 被贴上了一个标签si,每个标签都是 26 个小写 英文字母之一。设 str(l, r)表示第 l 杯酒到第 r 杯酒的 r ? l + 1 个标签顺次连接构成的字符串。若 str(p, po) = str(q, qo),其中 1 ≤ p ≤ po ≤ n, 1 ≤ q ≤ qo ≤ n, p ≠ q, po ? p + 1 = qo ? q + 1 = r ,则称第 p 杯酒与第 q 杯酒是“ r 相似” 的。当然两杯“ r 相似”(r > 1)的酒同时也是“ 1 相似”、“ 2 相似”、……、“ (r ? 1) 相似”的。特别地,对于任意的 1 ≤ p , q ≤ n , p ≠ q ,第 p 杯酒和第 q 杯酒都 是“ 0 相似”的。

  在品尝环节上,品酒师 Freda 轻松地评定了每一杯酒的美味度,凭借其专业的水准和经验成功夺取了“首席品酒家”的称号,其中第 i 杯酒 (1 ≤ i ≤ n) 的 美味度为 ai 。现在 Rainbow 公布了挑战环节的问题:本次大会调制的鸡尾酒有一个特点,如果把第 p 杯酒与第 q 杯酒调兑在一起,将得到一杯美味度为 ap*aq 的 酒。现在请各位品酒师分别对于 r = 0,1,2, ? , n ? 1 ,统计出有多少种方法可以 选出 2 杯“ r 相似”的酒,并回答选择 2 杯“ r 相似”的酒调兑可以得到的美味度的最大值。

 

Input

  第 1 行包含 1 个正整数 n ,表示鸡尾酒的杯数。

  第 2 行包含一个长度为 n 的字符串 S,其中第 i 个字符表示第 i 杯酒的标签。

  第 3 行包含 n 个整数,相邻整数之间用单个空格隔开,其中第 i 个整数表示第 i 杯酒的美味度 ai 。

 

Output

   包括 n 行。第 i 行输出 2 个整数,中间用单个空格隔开。第 1 个整 数表示选出两杯“ (i ? 1) 相似”的酒的方案数,第 2 个整数表示选出两杯 “ (i ? 1) 相似”的酒调兑可以得到的最大美味度。若不存在两杯“ (i ? 1) 相似” 的酒,这两个数均为 0 。
 

Sample Input

  12
  abaabaabaaba
  1 -2 3 -4 5 -6 7 -8 9 -10 11 -12

 

Sample Output

  66 120
  34 120
  15 55
  12 40
  9 27
  7 16
  5 7
  3 -4
  2 -4
  1 -4
  0 0
  0 0

 

HINT

  技术分享

 

Solution

  这种字符串子串比较的题目,应该很容易都能想到后缀数组。

  我们发现如果两杯酒是k相似的,那么k一定小等于 以这两杯酒为开头的后缀 的最长公共前缀。

  我们将原串转化为排序好的后缀数组(原串之后就没用了)显然在后缀数组上相邻的最长公共前缀就是它们之间的height。

  进一步说,在后缀数组上任意两个不相等的后缀s[l]、s[r],那么它们的最长公共前缀就是技术分享

  这样我们得到了n-1个height,设height[i]=height(s[i-1],s[i])。

  height[i]对答案的贡献是存在最长的区间[l,r],使得技术分享,如果height有相同的情况,强制给他们定义大小级别。

  小C举个例子:

    技术分享

  上图中,height[7]影响答案的范围是[6,10],这样左框里的元素3、7,都可以分别和右框里的1、2、4、9配对,每个配对之间的最长公共前缀一定是1。

  (顺带一提,a数组就是题目中所述a数组,小C在此提醒方便读者阅读。)

  因此,height[7]对于为1相似的配对的数目ans1[1]的贡献是2*4=8,对于为1相似的配对的最大美味度ans2[1]的贡献是7*9=63。

  如果像上图中height[7]影响了[6,10],那么同样值等于1的height[9]影响的范围最多只能是[8,10]了,因为我们强制给它们定义了大小级别。

  统计最大值时要注意负负得正的情况,所以要同时记录区间的最大值和最小值。

  以上就基本解决了统计答案一类的问题,那么如何寻找每个height影响的区间呢?

    很容易想到的就是对于每个height分别向左向右利用ST表二分它作为最小值的区间,注意值相等的情况,复杂度O(nlogn)。

    还有就是解决这类问题很经典的区间RMQ数据结构——笛卡尔树,可以实现O(n)解决问题。

 

  简单的说,笛卡尔树长得就像一棵treap,它既满足二分查找树的性质,又满足堆的性质。(假装大家都会treap)

  大约就可以脑补笛卡尔树是干什么用的了吧?

  如果还是不理解或是想知道构建方法的可以自行上网百度,小C这里就不多说了。

  

  建树的时候把每个height看做一个点(本来height很像是两点之间的线段,这里就很像是化边为点?)。

  而存储信息的时候每个节点存储的是后缀数组上的某段区间的信息,注意别混淆了。具体实现可以看小C的代码。

 

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <ctime>
#define l(a) (son[a][0])
#define r(a) (son[a][1])
#define ll long long
#define INF 0x3FFFFFFF
#define MN 300005
#define MS 20
using namespace std;
int mp[MN],a[MN],sa[MS][MN],rak[MS][MN];
ll ad[MN],an[MN],FINF;
int son[MN][2],fa[MN],tp[MN],st[MN],top;
int len[MN],w[MN],mn[MN],mx[MN];
char c[MN];
int n,p;
int T;

inline int read()
{
    int n=0,f=1; char c=getchar();
    while (c<0 || c>9) {if(c==-)f=-1; c=getchar();}
    while (c>=0 && c<=9) {n=n*10+c-0; c=getchar();}
    return n*f;
}

void mul(int* osa,int* ork,int* nsa,int* nrk,int k)
{
    register int i;
    for (i=1;i<=n;++i) mp[ork[osa[i]]]=i;
    for (i=n;i;--i)
        if (osa[i]>k) nsa[mp[ork[osa[i]-k]]--]=osa[i]-k;
    for (i=n-k+1;i<=n;++i) nsa[mp[ork[i]]--]=i;
    for (i=1;i<=n;++i)
        nrk[nsa[i]]=nrk[nsa[i-1]]+(ork[nsa[i]]!=ork[nsa[i-1]]||ork[nsa[i]+k]!=ork[nsa[i-1]+k]);
}

void presa()
{
    register int i,k;
    for (i=1;i<=n;++i) ++mp[a[i]];
    for (i=1;i<=26;++i) mp[i]+=mp[i-1];
    for (i=1;i<=n;++i) sa[p][mp[a[i]]--]=i;
    for (i=1;i<=n;++i) rak[p][sa[p][i]]=rak[p][sa[p][i-1]]+(a[sa[p][i-1]]!=a[sa[p][i]]);
    for (k=1;k<n;k<<=1,++p)    mul(sa[p],rak[p],sa[p+1],rak[p+1],k);
}

void dfs(int x)
{    
    if (!x) return;
    dfs(l(x)); dfs(r(x));
    if (!l(x)) l(x)=MN-1,mn[l(x)]=mx[l(x)]=w[sa[p][rak[p][x]-1]],len[l(x)]=1;
    if (!r(x)) r(x)=MN-2,mn[r(x)]=mx[r(x)]=w[sa[p][rak[p][x]  ]],len[r(x)]=1;
    mn[x]=min(mn[l(x)],mn[r(x)]);
    mx[x]=max(mx[l(x)],mx[r(x)]);
    len[x]=len[l(x)]+len[r(x)];
    ad[tp[x]]+=1LL*len[l(x)]*len[r(x)];
    an[tp[x]]=max(an[tp[x]],1LL*mn[l(x)]*mn[r(x)]);
    an[tp[x]]=max(an[tp[x]],1LL*mn[l(x)]*mx[r(x)]);
    an[tp[x]]=max(an[tp[x]],1LL*mx[l(x)]*mn[r(x)]);
    an[tp[x]]=max(an[tp[x]],1LL*mx[l(x)]*mx[r(x)]);
}

int main()
{
    register int i,k,x,y;
    n=read();
    scanf("%s",c+1);
    for (i=1;i<=n;++i) a[i]=c[i]-a+1;
    presa();
    for (i=1;i<=n;++i) w[i]=read();
    mn[0]=INF; mx[0]=-INF;
    memset(an,128,sizeof(an)); FINF=an[0]; tp[0]=-1;
    for (i=2;i<=n;++i)
        for (tp[sa[p][i]]=0,k=p,x=sa[p][i],y=sa[p][i-1];k>=0;--k)
            if (rak[k][x]==rak[k][y]) tp[sa[p][i]]+=(1<<k),x+=(1<<k),y+=(1<<k);
    for (i=2;i<=n;++i)
    {
        for (x=0;top&&tp[sa[p][i]]<=tp[st[top]];x=st[top--]);
        if (x) fa[x]=sa[p][i];
        l(sa[p][i])=x; r(st[top])=sa[p][i];  fa[sa[p][i]]=st[top]; st[++top]=sa[p][i];
    }
    dfs(st[1]);
    for (i=n-1;i>=0;--i) an[i]=max(an[i],an[i+1]),ad[i]+=ad[i+1];
    for (i=0;i<n;++i) printf("%lld %lld\n",ad[i],an[i]==FINF?0:an[i]);
}

 

  这个做法是小C之后才知道的,小C在这之前还自己YY了一个奇怪的线段树做法,思路好像完全不一样,洛谷上卡过了,BZOJ(时限0.5s)和自家OJ(未开O2)死活卡过不去几个点。代码姑且放出来留念吧。

技术分享
#include <cstdio>
#include <cstring>
#include <algorithm>
#define l(a) (a<<1)
#define r(a) (a<<1|1)
#define ll long long
#define MM 1200005
#define MN 300005
#define MS 20
using namespace std;
int mp[MN],a[MN],sa[MS][MN],rak[MS][MN];
ll ad[MM],an[MM],FINF;
int len[MM],w[MN],mn[MM],mx[MM],inf,Finf;
bool tg[MM];
char c[MN];
int n,p;

inline int read()
{
    int n=0,f=1; char c=getchar();
    while (c<0 || c>9) {if(c==-)f=-1; c=getchar();}
    while (c>=0 && c<=9) {n=n*10+c-0; c=getchar();}
    return n*f;
}

void mul(int* osa,int* ork,int* nsa,int* nrk,int k)
{
    register int i;
    for (i=1;i<=n;++i) mp[ork[osa[i]]]=i;
    for (i=n;i;--i)
        if (osa[i]>k) nsa[mp[ork[osa[i]-k]]--]=osa[i]-k;
    for (i=n-k+1;i<=n;++i) nsa[mp[ork[i]]--]=i;
    for (i=1;i<=n;++i)
        nrk[nsa[i]]=nrk[nsa[i-1]]+(ork[nsa[i]]!=ork[nsa[i-1]]||ork[nsa[i]+k]!=ork[nsa[i-1]+k]);
}

void presa()
{
    register int i,k;
    for (i=1;i<=n;++i) ++mp[a[i]];
    for (i=1;i<=26;++i) mp[i]+=mp[i-1];
    for (i=1;i<=n;++i) sa[p][mp[a[i]]--]=i;
    for (i=1;i<=n;++i) rak[p][sa[p][i]]=rak[p][sa[p][i-1]]+(a[sa[p][i-1]]!=a[sa[p][i]]);
    for (k=1;k<n;k<<=1,++p)    mul(sa[p],rak[p],sa[p+1],rak[p+1],k);
}

void mark0(int x)
{
    void down(int);
    if (tg[x]) {ad[x]+=1LL*len[x]*(len[x]-1)/2; len[x]=0; mn[x]=inf; mx[x]=Finf;}
    else {down(x); tg[x]=1;}
}
void enlen(int x,int z) {len[x]+=z;}
void upmx(int x,int z)
{
    if (mx[x]!=Finf) an[x]=max(an[x],1LL*mx[x]*z);
    if (mn[x]!=inf)  an[x]=max(an[x],1LL*mn[x]*z);
    mn[x]=min(mn[x],z); mx[x]=max(mx[x],z);
}
void down(int x)
{
    if (tg[x]) {mark0(l(x)); mark0(r(x)); tg[x]=0;}
    if (len[x])
    {
        enlen(l(x),len[x]); enlen(r(x),len[x]);
        upmx(l(x),mx[x]); upmx(r(x),mx[x]);
        if (len[x]>1) {upmx(l(x),mn[x]); upmx(r(x),mn[x]);}
        len[x]=0; mn[x]=inf; mx[x]=Finf;
    }
    if (ad[x]) {ad[l(x)]+=ad[x]; ad[r(x)]+=ad[x]; ad[x]=0;}
    if (an[x]!=FINF) {an[l(x)]=max(an[l(x)],an[x]); an[r(x)]=max(an[r(x)],an[x]); an[x]=FINF;}
}

void write(int x,int L,int R)
{
    if (L==R) {if (L<n) printf("%lld %lld\n",ad[x],an[x]==FINF?0:an[x]); return;}
    down(x);
    int mid=L+R>>1;
    write(l(x),L,mid); write(r(x),mid+1,R);
}

void chzero(int x,int L,int R,int ql,int qr)
{
    if (ql==L&&qr==R) {mark0(x); return;}
    down(x);
    int mid=L+R>>1;
    if (qr<=mid) chzero(l(x),L,mid,ql,qr);
    else if (ql>mid) chzero(r(x),mid+1,R,ql,qr);
    else {chzero(l(x),L,mid,ql,mid); chzero(r(x),mid+1,R,mid+1,qr);}
}

void modify(int x,int L,int R,int ql,int qr,int z)
{
    if (ql==L&&qr==R) {enlen(x,1); upmx(x,z); return;}
    down(x);
    int mid=L+R>>1;
    if (qr<=mid) modify(l(x),L,mid,ql,qr,z);
    else if (ql>mid) modify(r(x),mid+1,R,ql,qr,z);
    else {modify(l(x),L,mid,ql,mid,z); modify(r(x),mid+1,R,mid+1,qr,z);}
}

int main()
{
    register int i,tp,k,x,y;
    n=read();
    scanf("%s",c+1);
    for (i=1;i<=n;++i) a[i]=c[i]-a+1;
    for (i=1;i<=n;++i) w[i]=read();
    presa(); 
    memset(an,128,sizeof(an)); FINF=an[0];
    memset(mn,127,sizeof(mn)); inf=mn[0];
    memset(mx,128,sizeof(mx)); Finf=mx[0];
    chzero(1,0,n,0,n);
    for (i=1;i<=n;++i)
    {
        for (tp=0,k=p,x=sa[p][i],y=sa[p][i-1];k>=0;--k)
            if (rak[k][x]==rak[k][y]) tp+=(1<<k),x+=(1<<k),y+=(1<<k);
        if (tp<n) chzero(1,0,n,tp+1,n);
        modify(1,0,n,0,n,w[sa[p][i]]);
    }
    chzero(1,0,n,0,n);
    write(1,0,n);
}
View Code

 

Last Word

  看到最后你知道小C一定又要吐槽了。(废话,小C发哪篇文章不是为了吐槽?)

  做这题之前小C还从来没有写过后缀数组,上网找了一篇“公认”很优秀的后缀数组模板(这里不是指黄学长的)讲解,理解了半天,愣是没理解。

  奇怪,这大概是小C唯一一个看着模板还依然没有思路的算法了吧。(明明是你智商比较低)

  然后点开黄学长的博客,发现了画风迥异的后缀数组模板,小C当机立断就写这种风格的了。

  虽然黄学长写得十分优雅,但感觉还是无法拯救自己的智商,全程抄代码,至今好像还有点蒙。(CTSC考试之前一直都在看后缀数组模板QAQ,还好没考

  这是其一。然后就是自己脑子一抽写了个线段树(败给了常数),结果学写了正解之后发现自己想不出原来的线段树做法了有木有?

  写正解的时候也处处碰壁,直到遇到小D和小F两位大神的指(yi)点(tong)迷(nu)津(D),才理解可以把height理解为连接后缀数组的线段,思路瞬间清晰。

  蒟 蒻 求 轻 D

[BZOJ]4199 品酒大会(Noi2015)