太神了這題,膜拜出題人 orz。
思考一
首先是大家都提到的一點,先抽卡再買。這裡來做個數學分析。
假設我們還剩 \(k\) 種沒有買,其實我們是有式子來算出它的花費期望的。WIKI 上提到,假設一個事件的機率為 \(p\),則遇到它的期望為 \(\frac{1}{p}\),因此,對於這個題,抽到一個新物品的機率顯然為 \(\frac{k}{n}\),那麼期望次數就為 \(\frac{n}{k}\),這裡面除了最後一次之外,其餘的次數的花費都為 \(x-\frac{x}{2}=\frac{x}{2}\),而最後一次的花費恰好為 \(x\),因此可以得出有 \(k\) 種沒買的情況下,抽到新物品的期望花費公式:
可以看出這個式子是關於 \(k\) 單調遞增的,也就是說,越晚抽卡,花費的代價越大,因此才有了上述的結論:先抽卡再買。
思考二
既然我們能直接算出期望了,那我們直接比較一下抽卡和買哪個更賺不就行了嗎。現在抽卡的期望我們有了,還需要一個買東西的期望。其實,因為每個東西的價值都是固定的,所以這個期望實際上就是平均數,那麼我們算出這個平均數,每次都和當前的抽卡期望比較,選個最小的執行不就行了,搞定!紫題也不過如此。
但是不對,有點小問題。因為每個物品的價值都是不一樣的,我們抽到不同的物品,最後計算出來的平均數也不一樣。難道要列舉一遍全部的可能情況,再算一個平均數的平均數?又或者還要分情況討論?那不就變成搜了嗎,暈。
思考三
不對,除了搜還能寫 DP,但是就是不知道狀態怎麼設計。
想想看,我們現在需要的東西有什麼。需要計算抽卡期望,那麼我們需要的東西就是當前剩餘的物品個數,還有一個是平均數,那我們直接用剩餘價值除以剩餘個數不就行了嗎!開一個兩個維度的 DP:剩餘個數,剩餘價值。發現它變成了一個揹包 DP。
現在來想狀態轉移。其實到這一步就和常規揹包 DP 差不多了。定義初始狀態 \(f_{n,sum}=sum\),即不抽卡的期望花費為總價值,然後從剩餘容量由大到小開始列舉所有物品,然後嘗試轉移就行了。
思考四
本來以為都快過了,但是寫一半卡住了。
具體是卡在狀態轉移的時候,突然發現這個轉移根本就沒法實現。因為我們每次轉移可能的來源有兩個:一個是購買,一個是抽卡。買還好說,但是抽卡不行,因為不是整數,而我們沒辦法訪問一個小數下標。
那麼怎麼辦呢,感覺又到了瓶頸了。
首先這個思路是絕對正確的,我們整理一下現在都有什麼了。我們可以求出在某個狀態下的最優代價,那麼,假如我們再維護一個出現機率的話,就能根據期望的定義,直接用機率乘代價去算了。那麼我們不妨把我們設計的 DP 陣列改一下,不維護期望了,改成維護當前狀態的機率,再求一下抽卡期望和平均值的最小值,二者一乘就是答案。又因為這個題的總方案數是一定的(可以用組合數求),那麼我們不妨再轉換一下思路,改成求當前狀態的總方案數,和總方案數一比就是答案。
思考五
我們在思路整理的時候忽略了一些小細節,比如,如果當前的最優策略是抽卡,則若沒抽中,下次的最優策略還是抽卡,直到抽到一個新的。簡單證明一下,假如你抽卡沒抽中,那麼除了錢少了點,此外什麼都沒變,因此 \(E(k)\) 和平均數完全不會有任何變化。知道了這點,我們才能用 \(E(k)\) 這樣一條路走到黑的模型來計算答案。
還有一點,打完提交的時候莫名其妙 RE 了,後來發現是階乘乘炸了(\(100!\) 確實還挺大的),然後把階乘函式改成 Double 就過了,會丟精度,但是不影響答案。
程式碼
#include<bits/stdc++.h>
using namespace std;
#define div *1.0/
int n,x;
int c[101];
double f[101][10001];
long double fac[101];
inline void prework(){
fac[0]=1;
for(int i=1;i<=100;++i){
fac[i]=fac[i-1]*i;
}
}
inline double E(int k){
return (n div k -1)*(x div 2)+x;
}
inline double C(int x,int y){
return fac[x] div (fac[y]*fac[x-y]);
}
int main(){
int sum=0;
cin>>n>>x;
prework();
for(int i=1;i<=n;++i){
cin>>c[i];
sum+=c[i];
}
f[n][sum]=1;
for(int i=1;i<=n;++i){
for(int j=0;j<=n-1;++j){
for(int k=0;k<=sum-c[i];++k){
f[j][k]+=f[j+1][k+c[i]];
}
}
}
long double ans=0;
for(int i=1;i<=n;++i){
for(int j=0;j<=sum;++j){
ans+=(f[i][j] div C(n,n-i)) * min(j div i,E(i));
}
}
printf("%.20Lf",ans);
}