社会我赞哥
说在前面
在控制疫情的关键时期,位于根结点的军队却始终没有被分配到儿子结点上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
1000⩽a⩽3000,
0.8
⩽
p
⩽
1.0
0.8\leqslant p\leqslant 1.0
0.8⩽p⩽1.0,
600
⩽
k
⩽
1200
600\leqslant k\leqslant 1200
600⩽k⩽1200,
0
⩽
x
⩽
10
0\leqslant x\leqslant 10
0⩽x⩽10。
不得不吐槽,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=k⋅p,great数为
n
g
=
k
−
n
p
−
x
ng=k-np-x
ng=k−np−x。
同时,在讨论过程中不考虑基础分数
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]=max⎩⎪⎨⎪⎧f[n−1][a−1][b][c][t−1]+d1[t]f[n−1][a][b−1][c][t−1]+d2[t]f[n−1][a][b][c−1][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.1⋅combo[t]=0.8⋅combo[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[m−1][a−t1][b−t2]+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][t2−1]+d2[t2]d[t1−1][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[m−1][a−t1][b−t2]+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[n−1][a−h1][b−h2]转移来的。从最终状态向转移到它的状态后退,途径的每个状态的
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
来自NOIP2012提高组D2T3。 ↩︎