参考题解:CSP202109-4 收集卡牌 题解 - cyx001 - 博客园 (cnblogs.com)
在上述题解的基础上进行了进一步解读和修改
本题采取算法:状态压缩dp
O. 题目
题目描述
小林在玩一个抽卡游戏,其中有 n 种不同的卡牌,编号为 1 到 n。每一次抽卡,她获得第 i 种卡牌的概率为 pi。如果这张卡牌之前已经获得过了,就会转化为一枚硬币。可以用 k 枚硬币交换一张没有获得过的卡。
小林会一直抽卡,直至集齐了所有种类的卡牌为止,求她的期望抽卡次数。如果你给出的答案与标准答案的绝对误差不超过 10−4,则视为正确。
提示:聪明的小林会把硬币攒在手里,等到通过兑换就可以获得剩余所有卡牌时,一次性兑换并停止抽卡。
输入格式
从标准输入读入数据。
输入共两行。第一行包含两个用空格分隔的正整数 n,k,第二行给出 p1,p2,…,pn,用空格分隔。
输出格式
输出到标准输出。
输出共一行,一个实数,即期望抽卡次数。
样例1输入
2 2
0.4 0.6
样例1输出
2.52
样例1解释
共有 2 种卡牌,不妨记为 A 和 B,获得概率分别为 0.4 和 0.6,2 枚硬币可以换一张卡牌。下面给出各种可能出现的情况:
- 第一次抽卡获得 A,第二次抽卡获得 B,抽卡结束,概率为 0.4×0.6=0.24,抽卡次数为 2。
- 第一次抽卡获得 A,第二次抽卡获得 A,第三次抽卡获得 B,抽卡结束,概率为 0.4×0.4×0.6=0.096,抽卡次数为 3。
- 第一次抽卡获得 A,第二次抽卡获得 A,第三次抽卡获得 A,用硬币兑换 B,抽卡结束,概率为 0.4×0.4×0.4=0.064,抽卡次数为 3。
- 第一次抽卡获得 B,第二次抽卡获得 A,抽卡结束,概率为 0.6×0.4=0.24,抽卡次数为 2。
- 第一次抽卡获得 B,第二次抽卡获得 B,第三次抽卡获得 A,抽卡结束,概率为 0.6×0.6×0.4=0.144,抽卡次数为 3。
- 第一次抽卡获得 B,第二次抽卡获得 B,第三次抽卡获得 B,用硬币兑换 A,抽卡结束,概率为 0.6×0.6×0.6=0.216,抽卡次数为 3。
因此答案是 0.24×2+0.096×3+0.064×3+0.24×2+0.144×3+0.216×3=2.52。
样例2输入
4 3
0.006 0.1 0.2 0.694
样例2输出
7.3229920752
子任务
对于 20% 的数据,保证 1≤n,k≤5。
对于另外 20% 的数据,保证所有 pi 是相等的。
对于 100% 的数据,保证 1≤n≤16,1≤k≤5,所有的 pi 满足 pi≥110000,且 ∑i=1npi=1。
一. 解题思路
dp[ i , S ] : i表示当前抽取的次数,S表示状态集合,dp[ i , S ]表示抽取i次,且状态为S时的概率。
p[ j ]:抽到j牌,一共有n种牌
边界条件:dp[0,0]=1,i=0,S=0的概率为1;
状态转移方程:循环枚举
如果当前状态已经集齐卡片,对答案就有当前状态概率*i的贡献,且不会有后继状态;
怎么看是否集齐?如果已经有了x种卡牌,而且一共抽到的i张减去x张,也就是重复的张数再除以k(就是可以得到多少其他卡牌),再加上x是n,说明可以一次兑换,即集齐了。(这边的x是代码中的cnt[S],即状态为S时抽到的卡牌种数)
否则,当前状态需要继续抽取:若抽取到第i种卡片,则对状态贡献了当前状态概率*p[j]
for(int S=0;S<(1<<n);S++){//枚举状态
for(int i=0;i<=100;i++){//枚举j,一定要设的大一点
//集齐了
if(cnt[S]+(i-cnt[S])/k==n){
ans+=i*dp[i][S];//期望
continue;//既然抽完了,就不需要再计算了
}
for(int j=0;j<n;j++){//枚举抽到了哪一张卡牌
if(S&(1<<j)) dp[i+1][S]+=dp[i][S]*p[j];//出现过,状态不改变
else dp[i+1][S+(1<<j)]+=dp[i][S]*p[j];//状态改变
}
}
}
cnt预处理:
如何计算二进制数中1的个数?while(x) x&=x-1,cnt++;
for(int S=0;S<(1<<n);S++){//检查每一个状态
x=S;//会变化
while(x) x&=x-1,cnt[S]++;//二进制里一的个数就是答案
}
二、代码
#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=(1<<17)+17;//防越界
double dp[110][maxn];
double p[100];
int cnt[maxn];
int main(){
double ans=0;
int n,k,x;
cin>>n>>k;//输入
for(int i=0;i<n;i++) cin>>p[i];
for(int i=0;i<(1<<n);i++){//预处理
x=i;
while(x) x&=x-1,cnt[i]++;
}
dp[0][0]=1;
for(int S=0;S<(1<<n);S++){//计算
for(int i=0;i<=100;i++){
if(cnt[S]+(i-cnt[S])/k==n){
ans+=i*dp[i][S];
continue;
}
for(int j=0;j<n;j++){
if(S&(1<<j)) dp[i+1][S]+=dp[i][S]*p[j];
else dp[i+1][S+(1<<j)]+=dp[i][S]*p[j];
}
}
}
printf("%.10lf",ans);//注意输出时要保留5位(好像6位更保险,5位过不了官方数据)
return 0;
}