[Tkey] CodeForces 1267G Game Relics

HaneDaniko發表於2024-06-08

太神了這題,膜拜出題人 orz。

思考一

首先是大家都提到的一點,先抽卡再買。這裡來做個數學分析。

假設我們還剩 \(k\) 種沒有買,其實我們是有式子來算出它的花費期望的。WIKI 上提到,假設一個事件的機率為 \(p\),則遇到它的期望為 \(\frac{1}{p}\),因此,對於這個題,抽到一個新物品的機率顯然為 \(\frac{k}{n}\),那麼期望次數就為 \(\frac{n}{k}\),這裡面除了最後一次之外,其餘的次數的花費都為 \(x-\frac{x}{2}=\frac{x}{2}\),而最後一次的花費恰好為 \(x\),因此可以得出\(k\) 種沒買的情況下,抽到新物品的期望花費公式:

\[E(k)=\frac{x}{2}\times (\frac{n}{k}-1)+x \]

可以看出這個式子是關於 \(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);
}