CSP 202109-4 收集卡牌

参考题解: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时抽到的卡牌种数)

 {if \; cnt[S]+(j-cnt[S])/k==n \: \; then\; \; }ans+=i*dp[i][S];

否则,当前状态需要继续抽取:若抽取到第i种卡片,则对状态贡献了当前状态概率*p[j]

\begin{cases} & \text{if } S\&(1<<j)\neq 0 ; dp[i+1][S]+=dp[i][S]*p[j]; \\ & \text{else } dp[i+1][S+(1<<j)]+=dp[i][S]*p[j]; \end{cases}

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;
}

 

  • 11
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值