[Tkey] CodeForces 1267G Game Relics
太神了这题,膜拜出题人 orz。
思考一
首先是大家都提到的一点,先抽卡再买。这里来做个数学分析。
假设我们还剩 \(k\) 种没有买,其实我们是有式子来算出它的花费期望的。WIKI 上提到,假设一个事件的概率为 \(p\),则遇到它的期望为 \(\frac{1}{p}\),因此,对于这个题,抽到一个新物品的概率显然为 \(\frac{k}{n}\),那么期望次数就为 \(\frac{n}{k}\),这里面除了最后一次之外,其余的次数的花费都为 \(x-\frac{x}{2}=\frac{x}{2}\),而最后一次的花费恰好为 \(x\),因此可以得出有 \(k\) 种没买的情况下,抽到新物品的期望花费公式:
可以看出这个式子是关于 \(k\) 单调递增的,也就是说,越晚抽卡,花费的代价越大,因此才有了上述的结论:先抽卡再买。
思考二
既然我们能直接算出期望了,那我们直接比较一下抽卡和买哪个更赚不就行了吗。现在抽卡的期望我们有了,还需要一个买东西的期望。其实,因为每个东西的价值都是固定的,所以这个期望实际上就是平均数,那么我们算出这个平均数,每次都和当前的抽卡期望比较,选个最小的执行不就行了,搞定!紫题也不过如此。
但是不对,有点小问题。因为每个物品的价值都是不一样的,我们抽到不同的物品,最后计算出来的平均数也不一样。难道要枚举一遍全部的可能情况,再算一个平均数的平均数?又或者还要分情况讨论?那不就变成搜了吗,晕。
思考三
不对,除了搜还能写 DP,但是就是不知道状态怎么设计。
想想看,我们现在需要的东西有什么。需要计算抽卡期望,那么我们需要的东西就是当前剩余的物品个数,还有一个是平均数,那我们直接用剩余价值除以剩余个数不就行了吗!开一个两个维度的 DP:剩余个数,剩余价值。发现它变成了一个背包 DP。
现在来想状态转移。其实到这一步就和常规背包 DP 差不多了。定义初始状态 \(f_{n,sum}=sum\),即不抽卡的期望花费为总价值,然后从剩余容量由大到小开始枚举所有物品,然后尝试转移就行了。
思考四
本来以为都快过了,但是写一半卡住了。
具体是卡在状态转移的时候,突然发现这个转移根本就没法实现。因为我们每次转移可能的来源有两个:一个是购买,一个是抽卡。买还好说,但是抽卡不行,因为不是整数,而我们没办法访问一个小数下标。
那么怎么办呢,感觉又到了瓶颈了。
首先这个思路是绝对正确的,我们整理一下现在都有什么了。我们可以求出在某个状态下的最优代价,那么,假如我们再维护一个出现概率的话,就能根据期望的定义,直接用概率乘代价去算了。那么我们不妨把我们设计的 DP 数组改一下,不维护期望了,改成维护当前状态的概率,再求一下抽卡期望和平均值的最小值,二者一乘就是答案。又因为这个题的总方案数是一定的(可以用组合数求),那么我们不妨再转换一下思路,改成求当前状态的总方案数,和总方案数一比就是答案。
思考五
我们在思路整理的时候忽略了一些小细节,比如,如果当前的最优策略是抽卡,则若没抽中,下次的最优策略还是抽卡,直到抽到一个新的。简单证明一下,假如你抽卡没抽中,那么除了钱少了点,此外什么都没变,因此 \(E(k)\) 和平均数完全不会有任何变化。知道了这点,我们才能用 \(E(k)\) 这样一条路走到黑的模型来计算答案。
还有一点,打完提交的时候莫名其妙 RE 了,后来发现是阶乘乘炸了(\(100!\) 确实还挺大的),然后把阶乘函数改成 Double 就过了,会丢精度,但是不影响答案。
代码
#include<bits/stdc++.h>
using namespace std;
#define div *1.0/
int n,x;
int c[101];
double f[101][10001];
long double fac[101];
inline void prework(){
fac[0]=1;
for(int i=1;i<=100;++i){
fac[i]=fac[i-1]*i;
}
}
inline double E(int k){
return (n div k -1)*(x div 2)+x;
}
inline double C(int x,int y){
return fac[x] div (fac[y]*fac[x-y]);
}
int main(){
int sum=0;
cin>>n>>x;
prework();
for(int i=1;i<=n;++i){
cin>>c[i];
sum+=c[i];
}
f[n][sum]=1;
for(int i=1;i<=n;++i){
for(int j=0;j<=n-1;++j){
for(int k=0;k<=sum-c[i];++k){
f[j][k]+=f[j+1][k+c[i]];
}
}
}
long double ans=0;
for(int i=1;i<=n;++i){
for(int j=0;j<=sum;++j){
ans+=(f[i][j] div C(n,n-i)) * min(j div i,E(i));
}
}
printf("%.20Lf",ans);
}