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

那么今天就講到這里了,我們下期再見~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM