CSP——收集卡牌

题目描述

小林在玩一个抽卡游戏,其中有 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≥1/10000,且 ∑i=1n pi=1。


题目思路:

题目不难理解,乍一看,以为是概率论的知识,实则是动态规划dp。对动态规划还不理解的同学可以浅浅看一下这篇博客:(90条消息) 动态规划—入门篇_Wu_L7的博客-CSDN博客_动态规划入门讲解。简单来说,动态规划就是把大问题化成小问题,在求解小问题的过程中,把所得的结果存储起来,以便后续使用,一步一步来求解。在这个题目中,我们可以一张一张地抽,把每一张的可能性都算出来,一步一步得出最终结果。

AC代码:

#include<iostream>
using namespace std;
double p[21];//概率 
double dp[70001][81];//dp,抽第j张获得二进制状态i的概率 
int cnt[70001];//该二进制状态下有几个1 
int main() 
{
	int n=0, k=0;
	scanf("%d%d", &n, &k);
	for(int i=1; i<=n; i++)
	{
		scanf("%lf", &p[i]);
		dp[1<<(i-1)][1]=p[i];//第一张卡是i的概率 
	}
	for(int i=1; i<(1<<n); i++)
	{
		int x=i;
		while(x)
		{
			cnt[i]++;
			x&=x-1;//每次和(自身-1)作与运算 都会消除一个1 
		}
	}
	double ans; 
	for(int i=1; i<(1<<n); i++)//二进制状态 
	{
		for(int j=1; j<=(k*(n-1)+1); j++)//抽第j张卡 
		{
			if((cnt[i]+(j-cnt[i])/k)==n)
			{
				ans+=dp[i][j]*j;
				continue;
			}
			for(int w=1; w<=n; w++)
			{
				if(i&(1<<(w-1)))dp[i][j+1]+=dp[i][j]*p[w];//抽中已有的 
				else dp[i+(1<<(w-1))][j+1]+=dp[i][j]*p[w];//抽中未有的 
			}
		}
	}
	printf("%.10lf", ans); 
	return 0;
}

下面就来讲解一下代码以及几个疑惑点:

1、为什么设dp[70001][81]:数组dp,抽第 j 张获得二进制状态 i 的概率。我们采用二进制去表达当前已拥有的卡牌状态,举个例子,dp[ 5 ][ 2 ],表示的是已经抽了两张卡牌,并且拥有第一张和第三张,因为5的二进制表示为101,同理,dp[ 6 ][ 4 ],表示的是已经抽了四张卡牌,并且拥有第二张和第三张,因为6的二进制表示为110。为什么设70001和81?因为n<=16,即最多有16张卡牌,所以有2^16=65536个状态,当有16张卡牌时,最坏一种情况就是一直抽到相同的,并且k最大为5,则这个游戏最多需要抽1+5*15=76张就能获得全部卡牌,70001和81都是满足最大要求并稍微大点的范围,当然,如果想节约那该死的内存,设65536和76也是完全没有问题的。

2、在我们访问dp的时候,得先知道它目前状态已经有几张卡牌,我们才知道至少抽多少张就能够集齐所有或者最多再抽多少张就能兑换。该状态已有的卡牌数,即cnt的预处理。一个数和它的减一做与运算,就能消除一个1。

图解:

3、该算法的执行过程如下:假设n==2,k==2,第一张卡牌为a,第二张为b

相信有些同学对集齐卡牌的满足条件后的执行语句有些疑惑,为什么用continue,而不是用break?对照着上面的例子,如果用break的话,(ab+ba)和(aab+bba)这种情况就会算不到,因为当j==1时就已经满足条件,执行并退出了,所有要continue一直算下去,当然,算到后面也没关系,因为后面的dp[ i ][ j ]都是0,不影响结果。

 4、为什么用dp[70001][81],而不用dp[81][70001],会不会用dp[81][70001]更好一点,时间上更有效?看懂代码并理解上面图解的同学应该也知道,该算法执行的时候是一列一列执行的,在计算机的二维数组执行逻辑里,一行一行执行的效率比一列一列执行的效率高的多,为此,我也去分别提交了两个模式的代码,看看其中的时间效率。很奇怪的是,在这道题中,一列一列执行所用的时间竟然比一行一行执行所用的时间还少。(第一次是用dp[70001][81])

当时看到这样的结果很是惊讶,理论上dp[81][70001]要比dp[70001][81]更有效,时间的使用要缩短好几倍才是,现在反而耗费的时间更长了,然后我又提交了好几组,都是一样的结果,dp[70001][81]所用的时间更短。然后浅浅思考过后,发觉背后还是有其中的道理。

即每次都把一行跑完,一行就有70001,完完整整全部跑完。

貌似这样获取复杂度还是很高,但是越往后执行,它执行的次数就越少,例如执行到最后两行时: 

这时它只需执行81*2=162次,所以它是越来越少,而dp[81][70001]是固定(81*70001),所以这其中无法直接比较,这个得看具体的数据量和计算机的性能了,如果数据量是比70001更少一点,比81更多一点,可能就是dp[81][70001]更有效了。

5、最后一点,题目说绝对误差不超过 10^−4,则视为正确,但是输出格式用%.6lf、%.8lf都过不了,最后改成%.10lf就行了,这个真不理解了哈哈哈哈(玄学),知道原因的同学欢迎底下评论。

谢谢大家!

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值