来自yuzan1830的挑战(五)

社会我赞哥

说在前面

在控制疫情的关键时期,位于根结点的军队却始终没有被分配到儿子结点上1,多才多艺的yuzan1830选择了BanG Dream。

题目背后的故事

据yuzan1830描述,这款音游中每个note的得分靠按键准度,分为perfect、great、good、bad、miss。该音游的得分机制见原题目。在只考虑perfect、great、miss,并且技能倍率为 1 1 1、基础分数为常数的情况下,设基础分数为 a a a,note总数为 k k k,玩家准度(perfect数占note总数的比)为 p p p,miss数为 x x x,求理论最高分和最低分,并各输出一种方案。

样例输入
2000 506 0.93 3

样例输出
1142976.000
gr33-p470-m3
1104444.000
p144-m-p108-m-p112-gr4-m-p106-gr29

yuzan1830给出的数据范围:
1000 ⩽ a ⩽ 3000 1000\leqslant a\leqslant 3000 1000a3000 0.8 ⩽ p ⩽ 1.0 0.8\leqslant p\leqslant 1.0 0.8p1.0 600 ⩽ k ⩽ 1200 600\leqslant k\leqslant 1200 600k1200 0 ⩽ x ⩽ 10 0\leqslant x\leqslant 10 0x10
不得不吐槽,yuzan1830给出的数据范围居然是有下限的。
考虑到本蒟蒻的水平,实际数据规模可能会比给定的范围低。

接受挑战

求理论最高分和最低分,容易想到动态规划。

达羌:关于贪心,他死了。

为了方便,记连击 t t t次的combo倍率为 c o m b o [ t ] combo[t] combo[t],perfect数为 n p = k ⋅ p np=k\cdot p np=kp,great数为 n g = k − n p − x ng=k-np-x ng=knpx
同时,在讨论过程中不考虑基础分数 a a a,因为对分数高低没有影响。(所以下文的 a a a不一定指基础分数)

法1: 设状态 f [ n ] [ a ] [ b ] [ c ] [ t ] f[n][a][b][c][t] f[n][a][b][c][t]表示前 n n n个note中有 a a a次perfect、 b b b次great、 c c c次miss,且目前连击 t t t次,此时的理论最高分。最终答案为 max ⁡ i { f [ k ] [ n p ] [ n g ] [ x ] [ i ] } \max\limits_i\{f[k][np][ng][x][i]\} imax{f[k][np][ng][x][i]}
讨论第 n n n个note分别是perfect、great、miss的三种情况,得到状态转移方程:

f [ n ] [ a ] [ b ] [ c ] [ t ] = max ⁡ { f [ n − 1 ] [ a − 1 ] [ b ] [ c ] [ t − 1 ] + d 1 [ t ] f [ n − 1 ] [ a ] [ b − 1 ] [ c ] [ t − 1 ] + d 2 [ t ] f [ n − 1 ] [ a ] [ b ] [ c − 1 ] [ t ′ ] t = 0 f[n][a][b][c][t]=\max\begin{cases} f[n-1][a-1][b][c][t-1]+d_1[t]\\ f[n-1][a][b-1][c][t-1]+d_2[t]\\ f[n-1][a][b][c-1][t']&t=0 \end{cases} f[n][a][b][c][t]=maxf[n1][a1][b][c][t1]+d1[t]f[n1][a][b1][c][t1]+d2[t]f[n1][a][b][c1][t]t=0

其中 d 1 [ t ] d_1[t] d1[t] d 2 [ t ] d_2[t] d2[t]表示连击次数为 t t t时perfect、great的得分。

d 1 [ t ] = 1.1 ⋅ c o m b o [ t ] d 2 [ t ] = 0.8 ⋅ c o m b o [ t ] \begin{aligned} d_1[t]&=1.1\cdot combo[t]\\ d_2[t]&=0.8\cdot combo[t] \end{aligned} d1[t]d2[t]=1.1combo[t]=0.8combo[t]

注意到 n = a + b + c n=a+b+c n=a+b+c,因此可以丢掉一维,又因为miss数和great数都较少,选择丢掉 a a a,状态变成了 f [ n ] [ b ] [ c ] [ t ] f[n][b][c][t] f[n][b][c][t],状态数降至 O ( k 3 x ) O(k^3x) O(k3x)
在状态转移的过程中, n n n这一维可以用滚动数组滚掉,空间复杂度 O ( k 2 x ) O(k^2x) O(k2x)
每次转移需要枚举 t ′ t' t,复杂度 O ( k ) O(k) O(k)。可以用 m f [ n ] [ a ] [ b ] [ c ] mf[n][a][b][c] mf[n][a][b][c]记录 f [ n ] [ a ] [ b ] [ c ] [ t ′ ] f[n][a][b][c][t'] f[n][a][b][c][t]的最大值,每次状态转移就变成 O ( 1 ) O(1) O(1),总时间复杂度为 O ( k 3 x ) O(k^3x) O(k3x)

法2: 所有的note可以看成是若干段连击被若干个miss分隔开。注意到miss是不得分的,因此总得分就是若干段连击的得分之和。
设状态 f [ m ] [ a ] [ b ] f[m][a][b] f[m][a][b]表示总共有 a a a次perfect、 b b b次great的 m m m段连击的理论最高分。最终答案为 max ⁡ i = 1 x + 1 { f [ i ] [ n p ] [ n g ] } \max\limits_{i=1}^{x+1}\{f[i][np][ng]\} i=1maxx+1{f[i][np][ng]}
状态转移方程:

f [ m ] [ a ] [ b ] = max ⁡ { f [ m − 1 ] [ a − t 1 ] [ b − t 2 ] + d [ t 1 ] [ t 2 ] } f[m][a][b]=\max\{f[m-1][a-t_1][b-t_2]+d[t_1][t_2]\} f[m][a][b]=max{f[m1][at1][bt2]+d[t1][t2]}

其中 d [ t 1 ] [ t 2 ] d[t_1][t_2] d[t1][t2]表示一段包含 t 1 t_1 t1个perfect、 t 2 t_2 t2个great的连击的最高得分。由于combo倍率递增,根据排序不等式,perfect应该在great之后。于是 d [ t 1 ] [ t 2 ] d[t_1][t_2] d[t1][t2]可以递推求出:

d [ t 1 ] [ t 2 ] = { d [ 0 ] [ t 2 − 1 ] + d 2 [ t 2 ] t 1 = 0 d [ t 1 − 1 ] [ t 2 ] + d 1 [ t 1 + t 2 ] t 1 ≠ 0 d[t_1][t_2]=\begin{cases} d[0][t_2-1]+d_2[t_2]&t_1=0\\ d[t_1-1][t_2]+d_1[t_1+t_2]&t_1\neq 0 \end{cases} d[t1][t2]={d[0][t21]+d2[t2]d[t11][t2]+d1[t1+t2]t1=0t1=0

状态数为 O ( k 2 x ) O(k^2x) O(k2x),即空间复杂度。每次转移需要枚举 t 1 , t 2 t_1,t_2 t1,t2,复杂度 O ( k 2 ) O(k^2) O(k2)。注意到 f [ m − 1 ] [ a − t 1 ] [ b − t 2 ] + d [ t 1 ] [ t 2 ] f[m-1][a-t_1][b-t_2]+d[t_1][t_2] f[m1][at1][bt2]+d[t1][t2]的值关于 t 1 , t 2 t_1,t_2 t1,t2都是单峰的,因此可以三分 t 1 , t 2 t_1,t_2 t1,t2求出最值,每次转移就变成了 O ( log ⁡ 3 2 k ) O(\log_3^2k) O(log32k),总时间复杂度为 O ( k 2 x log ⁡ 3 2 k ) O(k^2x\log_3^2k) O(k2xlog32k)。比法1优秀,但常数大。

达羌:我才不会告诉你这个“注意到”花了我多少时间。

然后是输出方案。这时候就体现出了法2的优势。因为状态转移过程中 t 1 , t 2 t_1,t_2 t1,t2就记录了每一段连击中perfect和great的个数,避免重新计数;两者的位置又可以根据排序不等式直接确定。miss就穿插在连击之间。
对于每个状态 f [ m ] [ a ] [ b ] f[m][a][b] f[m][a][b],记录 h 1 , h 2 h_1,h_2 h1,h2,表示 f [ m ] [ a ] [ b ] f[m][a][b] f[m][a][b]是由状态 f [ n − 1 ] [ a − h 1 ] [ b − h 2 ] f[n-1][a-h_1][b-h_2] f[n1][ah1][bh2]转移来的。从最终状态向转移到它的状态后退,途径的每个状态的 h 1 , h 2 h_1,h_2 h1,h2作为答案输出。
至于法1……只单独考虑了每一个note。而且法1的状态数本来是爆仓了的,DP的时候用了滚动数组,一滚动之前的note就没了。不过每个note只有3种情况,可以考虑三进制压位。

达羌:……说的像我可以写出来一样。

最低分的求法类似。

其实最高分有一个显然的贪心做法,就是把所有的perfect和great组成一段连击,然后great在前,perfect在后。时间复杂度 O ( k ) O(k) O(k)
也许最低分也有类似的做法,但本蒟蒻没有看出来。
代码中求最高分采用的是贪心,求最低分采用法2。三分套三分真恶心。

#include<cstdio>
const char *str[]={"m","gr","p"};
const int maxk=1200+5,maxx=10+5;
const double EPS=1e-6;
const double INF=1e14;
double f[maxx][maxk][maxk];
double d[maxk][maxk],combo[maxk],p;
int type[maxk],cnt[maxk],tot;
int h1[maxx][maxk][maxk],h2[maxx][maxk][maxk];
int a,k,x,np,ng;
double tsearch2(int m,int a,int b,int t1){
  int l=t1?0:1,r=b;double ans=INF;
  while(r-l>10){
    int lmid=l+(r-l)/3,rmid=l+(r-l)*2/3;
    double f1=f[m-1][a-t1][b-lmid]+d[t1][lmid];
    double f2=f[m-1][a-t1][b-rmid]+d[t1][rmid];
    if(f1>=INF||f1>=f2)l=lmid;
    else r=rmid;
  }
  for(int i=l;i<=r;i++){
    double f0=f[m-1][a-t1][b-i]+d[t1][i];
    if(f0<ans)ans=f0,h2[m][a][b]=i;
  }
  return ans;
}
double tsearch1(int m,int a,int b){
  int l=b?0:1,r=a;double ans=INF;
  while(r-l>10){
    int lmid=l+(r-l)/3,rmid=l+(r-l)*2/3;
    double f1=tsearch2(m,a,b,lmid);
    if(f1>=INF||f1>=tsearch2(m,a,b,rmid))l=lmid;
    else r=rmid;
  }
  for(int i=l;i<=r;i++){
    double f0=tsearch2(m,a,b,i);
    if(f0<ans)ans=f0,h1[m][a][b]=i;
  }
  return ans;
}
int main(){
  scanf("%d%d%lf%d",&a,&k,&p,&x);
  for(int i=1;i<=20;i++)combo[i]=1;
  for(int i=21;i<=50;i++)combo[i]=1.01;
  for(int i=51;i<=100;i++)combo[i]=1.02;
  for(int i=101;i<=150;i++)combo[i]=1.03;
  for(int i=151;i<=200;i++)combo[i]=1.04;
  for(int i=201;i<=250;i++)combo[i]=1.05;
  for(int i=251;i<=300;i++)combo[i]=1.06;
  for(int i=301;i<=400;i++)combo[i]=1.07;
  for(int i=401;i<=500;i++)combo[i]=1.08;
  for(int i=501;i<=600;i++)combo[i]=1.09;
  for(int i=601;i<=700;i++)combo[i]=1.1;
  for(int i=701;i<=k;i++)combo[i]=1.11;
  np=int(k*p+EPS),ng=k-np-x;

  double ans=0;tot=0;
  for(int i=1;i<=k-x;i++){
    if(i<=ng)ans+=0.8*combo[i];
    else ans+=1.1*combo[i];
  }
  printf("%.3lf\n",ans*a);
  if(ng)type[++tot]=1,cnt[tot]=ng;
  if(np)type[++tot]=2,cnt[tot]=np;
  if(x)type[++tot]=0,cnt[tot]=x;
  for(int i=1;i<=tot;i++){
    if(i>1)putchar('-');
    printf("%s",str[type[i]]);
    if(cnt[i]>1)printf("%d",cnt[i]);
  }
  printf("\n");

  for(int a=0;a<=np;a++)
    for(int b=0;b<=ng;b++)
      if(a||b)f[0][a][b]=INF;
  for(int t1=0;t1<=np;t1++)
    for(int t2=0;t2<=ng;t2++)if(t1||t2){
      if(!t2)d[t1][t2]=d[t1-1][0]+1.1*combo[t1];
      else d[t1][t2]=d[t1][t2-1]+0.8*combo[t1+t2];
  }
  for(int m=1;m<=x+1;m++)
    for(int a=0;a<=np;a++)
      for(int b=0;b<=ng;b++)
        if(a||b)f[m][a][b]=tsearch1(m,a,b);
  int c,c1=np,c2=ng;ans=INF,tot=0;
  for(int i=1;i<=x+1;i++)
    if(f[i][np][ng]<ans)ans=f[i][np][ng],c=i;
  printf("%.3lf\n",ans*a);
  for(int i=c;i>=1;i--){
    int t1=h1[i][c1][c2],t2=h2[i][c1][c2];
    if(t1)type[++tot]=2,cnt[tot]=t1;
    if(t2)type[++tot]=1,cnt[tot]=t2;
    if(i>1)type[++tot]=0,cnt[tot]=1,x--;
    else if(x)type[++tot]=0,cnt[tot]=x;
    c1-=t1,c2-=t2;
  }
  for(int i=1;i<=tot;i++){
    if(i>1)putchar('-');
    printf("%s",str[type[i]]);
    if(cnt[i]>1)printf("%d",cnt[i]);
  }
  printf("\n");
  return 0;
}

说在后面

下面是样例欣赏:

样例输入 1
1035 720 0.85 4

样例输出 1
834872.400
gr104-p612-m4
796682.970
p146-m-p143-m-p108-gr29-m-p113-gr23-m-p102-gr52

样例输入 2
2333 233 1 0

样例输出 2
614500.535
p233
614500.535
p233

样例输入 3
3000 1200 0.8 10

样例输出 3
4038390.000
gr230-p960-m10
3771150.000
p93-gr29-m-p91-gr31-m-p91-gr15-m-p96-gr13-m-p96-gr19-m-p93-gr5-m-p95-gr7-m-p93-gr8-m-p98-gr6-m-p57-gr40-m-p57-gr57


  1. 来自NOIP2012提高组D2T3。 ↩︎

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值