天天看点

CSP202109-4 收集卡牌 题解

好题,虽然看着像期望DP,但是装压好写。

〇、题目

题目描述

小林在玩一个抽卡游戏,其中有 \(n\) 种不同的卡牌,编号为 \(1\) 到 \(n\)。每一次抽卡,她获得第 \(i\) 种卡牌的概率为 \(p_i\)。如果这张卡牌之前已经获得过了,就会转化为一枚硬币。可以用 \(k\) 枚硬币交换一张没有获得过的卡。

小林会一直抽卡,直至集齐了所有种类的卡牌为止,求她的期望抽卡次数。如果你给出的答案与标准答案的绝对误差不超过 \(10^{-4}\),则视为正确。

提示:聪明的小林会把硬币攒在手里,等到通过兑换就可以获得剩余所有卡牌时,一次性兑换并停止抽卡。

输入格式

从标准输入读入数据。

输入共两行。第一行包含两个用空格分隔的正整数 \(n,k\),第二行给出 \(p_1,p_2,...,p_n\),用空格分隔。

输出格式

输出到标准输出。

输出共一行,一个实数,即期望抽卡次数。

样例1输入

2 2
0.4 0.6
      

样例1输出

2.52
      

样例1解释

共有 \(2\) 种卡牌,不妨记为 A 和 B,获得概率分别为 \(0.4\) 和 \(0.6\),\(2\) 枚硬币可以换一张卡牌。下面给出各种可能出现的情况:

  • 第一次抽卡获得 A,第二次抽卡获得 B,抽卡结束,概率为 \(0.4\times0.6=0.24\),抽卡次数为 \(2\)。
  • 第一次抽卡获得 A,第二次抽卡获得 A,第三次抽卡获得 B,抽卡结束,概率为 \(0.4\times0.4\times0.6=0.096\),抽卡次数为 \(3\)。
  • 第一次抽卡获得 A,第二次抽卡获得 A,第三次抽卡获得 A,用硬币兑换 B,抽卡结束,概率为 \(0.4\times0.4\times0.4=0.064\),抽卡次数为 \(3\)。
  • 第一次抽卡获得 B,第二次抽卡获得 A,抽卡结束,概率为 \(0.6\times0.4=0.24\),抽卡次数为 \(2\)。
  • 第一次抽卡获得 B,第二次抽卡获得 B,第三次抽卡获得 A,抽卡结束,概率为 \(0.6\times0.6\times0.4\),抽卡次数为 \(3\)。
  • 第一次抽卡获得 B,第二次抽卡获得 B,第三次抽卡获得 B,用硬币兑换 A,抽卡结束,概率为 \(0.6\times0.6\times0.6=0.216\),抽卡次数为 \(3\)。

    因此答案是 \(0.24\times2+0.096\times3+0.064\times3+0.24\times2+0.144\times3+0.216\times3=2.52\)。

样例2输入

4 3
0.006 0.1 0.2 0.694
      

样例2输出

7.3229920752
      

子任务

对于 \(20\%\) 的数据,保证 \(1\leq n,k\leq 5\)。

对于另外 \(20\%\) 的数据,保证所有 \(p_i\) 是相等的。

对于 \(100\%\) 的数据,保证 \(1\leq n\leq16,1\leq k\leq5\),所有的 \(p_i\) 满足 \(p_i\geq \frac{1}{1000}\) ,且 \(\sum\limits_{i=1}^np_i=1\)。

一、思路

既然是选或者不选,那就推荐装压DP。

首先定义状态:\(f_{i,j}\)表示状态为\(i\)(二进制,表示抽到或者没有抽到),已经抽了\(j\)次的概率。

边界条件肯定是\(f_{0,0}=1\),没有抽的概率显然是\(100\%\)。

之后是状态转移方程。想要算f,就肯定需要循环枚举,我们跟着代码一起对照。

for(int i=0;i<=(1<<n);i++){//枚举状态
  for(int j=0;j<=100;j++){//枚举j,一定要设的大一点
    //如果已经有了x张卡牌,而且抽到的j张减去x张,也就是重复的张数再除以k(就是可以得到多少其他卡牌),再加上x是n,说明可以一次兑换,就抽了j次,概率dp[i][j]。
    if(cnt[i]+(j-cnt[i])/k==n){
      ans+=j*dp[i][j];//期望
      continue;//既然抽完了,就不需要再计算了
    }
    for(int k=1;k<=n;k++){//枚举抽到了哪一张卡牌
      if(i&(1<<(k-1))) dp[i][j+1]+=dp[i][j]*p[k];//出现过,状态不改变
      else dp[i+(1<<(k-1))][j+1]+=dp[i][j]*p[k];//状态改变
    }
  }
}
      

不过,我们要算的x不可能现场算,就必须预处理,叫做cnt。

预处理方式:

for(int i=1;i<=(1<<n);i++){//检查每一个状态
  x=i;//会变化
  while(x) x&=x-1,cnt[i]++;//二进制里一的个数就是答案
}
      

二、代码

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=(1<<17)+17;//防越界
double dp[maxn][100];
double p[100];
int cnt[maxn];
int main(){
  double ans=0;
  int n,k,x;
  cin>>n>>k;//输入
  for(int i=1;i<=n;i++) cin>>p[i];
  for(int i=1;i<=(1<<n);i++){//预处理
    x=i;
    while(x) x&=x-1,cnt[i]++;
  }
  dp[0][0]=1;
  for(int i=0;i<=(1<<n);i++){//计算
    for(int j=0;j<=100;j++){
      if(cnt[i]+(j-cnt[i])/k==n){
        ans+=j*dp[i][j];
        continue;
      }
      for(int k=1;k<=n;k++){
        if(i&(1<<(k-1))) dp[i][j+1]+=dp[i][j]*p[k];
        else dp[i+(1<<(k-1))][j+1]+=dp[i][j]*p[k];
      }
    }
  }
  printf("%.5lf",ans);//注意输出时要保留5位(好像6位更保险,5位过不了官方数据)
  return 0;
}
      

那么今天就讲到这里了,我们下期再见~