[Luogu P2473] 奖励关
题意简述:共有 \(K\) 轮,有 \(n\) 种物品,每一轮出现每一种物品的概率 \(\frac{1}{n}\),物品可选可不选,对于选每一种物品,必须要在前面的轮先选给定的部分物品,每一种物品的价格可正可负。求 \(k\) 轮后按最优方案选择的期望价格。
数据范围:\(1\leq K \leq 100\) , \(1≤n≤15\)。
思路:
首先看题,概率 \(dp\) 没得跑。
再看数据范围,哦豁!状压 \(dp\) !
这道题不同的是需要倒推,因为正推的话有些情况在转移时选择宝物的概率并不是平均的(有宝物合集的限制),这样就会导致结果出现问题,且最终答案的状态表示十分麻烦 不要问我是怎么知道的!,因此选择倒推。
另外,因为 \(dp\) 的转移至于当前局和前一局有关,所以可以用滚动数组优化一下。
还有就是,这里用到了一个状压小技巧。很多时候题目会给定条件限制,即枚举状态的前提条件。这里就再次用到了二进制小技巧。
这里就以这道题为例:
while(~scanf("%d",&x) && x)
num[i] += 1 << (x - 1);//用二进制数存储 宝物集合
如果得到第 \(i\) 个宝物之前需要先得到第 \(x\) 个宝物,我们就在 \(num[i]\) 的值加上 \(1 << (x - 1)\),最后\(num[i]\)的值就是要得到第 \(i\) 个宝物的先决条件,枚举时的状态必须满足 \(num[i]\) 的要求。
其实这里就是在模拟二进制的形成,计算出满足题意的状态值,是不是很简单啊。
解题步骤:
- 先循环游戏轮数 \(i\);
- 然后每轮枚举当前宝物的状态 \(j\)(第 \(x\) 位为 \(0\) 则未得到该宝物,反之得到);
- 再枚举当局游戏抽到的宝物编号 \(k\),判断是否满足条件(满足该宝物的先决条件)在拿 \(k\) 宝物和不拿之间选最大值加到当前状态;
- 最终期望要乘上随机选择一个宝物的概率,因为是随机选择的啦。
- 因为是倒推的,所以最后答案为游戏开始时,未得到任何一个宝物的情况
坑:因为用的滚动数组,所以每次记得清空 不要问我是怎么知道的!
其他细节讲解都在代码里啦~
完整代码:
#include<cstdio>
#include<algorithm>
using namespace std;
int K,n,x,w[20],num[20];
double p,dp[2][1 << 15];
int main() {
scanf("%d %d",&K,&n);
p = 1.0 / n;//选择宝物的平均概率
for(int i = 1; i <= n; i ++) {
scanf("%d",&w[i]);//分值
while(~scanf("%d",&x) && x)
num[i] += 1 << (x - 1);//用二进制数存储 宝物集合
}
int maxn = 1 << n;
for(int i = K; i >= 1; i --) { //游戏轮数
for(int j = 0; j < maxn; j ++) {
dp[i & 1][j] = 0;//初始化(清空上一次循环的值)
for(int k = 1; k <= n; k ++) {//枚举这轮抽到的宝物
if((j & num[k]) == num[k])//如果满足k宝物的 宝物集合要求,
dp[i & 1][j] += max(dp[(i + 1) & 1][j | (1 << (k - 1))] + w[k],dp[(i + 1) & 1][j]);
//满足最优策略,在拿k宝物和不拿之间选最大值
else dp[i & 1][j] += dp[(i + 1) & 1][j];//不满足的话,该状态及为上一次的状态,没有变化
}
dp[i & 1][j] *= p;//最终期望要乘上随机选择一个宝物的概率
}
}
printf("%.6lf",dp[1][0]);
//因为是倒推的,所以最后答案为游戏开始时,未得到任何一个宝物的情况
return 0;
}