[LeetCode] 837. New 21 Game 新二十一點游戲


 

Alice plays the following game, loosely based on the card game "21".

Alice starts with 0 points, and draws numbers while she has less than K points.  During each draw, she gains an integer number of points randomly from the range [1, W], where W is an integer.  Each draw is independent and the outcomes have equal probabilities.

Alice stops drawing numbers when she gets K or more points.  What is the probability that she has N or less points?

Example 1:

Input: N = 10, K = 1, W = 10
Output: 1.00000
Explanation:  Alice gets a single card, then stops.

Example 2:

Input: N = 6, K = 1, W = 10
Output: 0.60000
Explanation:  Alice gets a single card, then stops.
In 6 out of W = 10 possibilities, she is at or below N = 6 points.

Example 3:

Input: N = 21, K = 17, W = 10
Output: 0.73278

Note:

  1. 0 <= K <= N <= 10000
  2. 1 <= W <= 10000
  3. Answers will be accepted as correct if they are within 10^-5 of the correct answer.
  4. The judging time limit has been reduced for this question.
 

這道題就是賭桌上經典的 21 點游戲了,想起了當年實習的游輪活動,就有21點游戲的賭桌。我當時還納悶為啥庄家到了 17 點以后就不再要牌了,原來里面大有學問啊,因為再多拿牌,增大了爆的概率,而如果小於 17 就不拿牌的話,會增大玩家贏的概率,估計是經過精心計算,用這個閾值庄家贏的概率最大吧,想着當時庄家每拿一張牌,大家都一起在喊“爆,爆,爆。。”的情景,還是覺得非常搞笑。但當時有一位同期實習的大神,可以根據分析台面上已經出現的牌,來推出最合理的策略,因為庄家的規則是不變的,只要過了 17 就堅決不拿牌,但是大神卻可以根據已出現的牌來制定自己的最優策略,經常能贏庄家。據大神稱他去賭場經常都能贏上個小二百刀,給跪了有木有?!

好,來解題吧。這道題說的是有 [1, W] 范圍內的牌,問我們當拿到不少於K點的時候就停止摸牌,最終點數能不超過的N點的概率。那么我們先來分析下,拿到 [1, W] 范圍內的任意一張牌的概率是 1/W,因為是隨機取的,所以拿到任意張牌的概率都是相等的。那么點數大於W的時候,概率怎么算呢,比如 W = 10, 我們拿到15點的概率是多少呢?這時候肯定不止拿一張牌了,那么我們分析最后一張牌,可以取1到 10,那么能拿到 15 點就有十種情況,之前共拿5點且最后一張拿10,之前共拿6點且最后一張拿9,之前拿共7點且最后一張拿8,...,之前共拿 14 點且最后一張拿1。那么拿 15 點的概率就是把這十種的概率都加起來。這道題給的假設是每次取牌都是等概率的,不管什么時候拿到 [1, 10] 內的任意張牌的概率都是十分之一,但是現實情況肯定不是這樣的,已經取出了的牌,不會放回了,所以現實情況要更加復雜。不用管它,反正我們拿最后一張牌的概率都是 1/W,由於是‘且’的關系,所以是概率相乘,可以將 1/W 提取出來,那么對於拿到x點的概率就可以歸納出下面的等式:

P(x) = 1/W * (P(x-1) + P(x-2) + P(x-W))

       = 1/W * sumP(x-W, x-1)

這里的x是有范圍限制的,必須在 [W, K] 之間,因為小於等於W的點數概率都是 1/W,而大於等於K的時候,就不會再拿牌了。現在回過頭來看看這道題要我們求什么,要求的是拿到不少於K點的時候就停止摸牌,最終點數能不超過的N點的概率,即 P(<=N | >= K)。那么現在我們就要引入強大的條件概率公式了,傳說中的貝葉斯公式就是由其推導出來的:

P(A | B) = P(AB) / P(B)

意思就是在事件B發生的條件下發生事件A的概率,等於事件A和B同時發生的概率除以事件B單獨發生的概率。那么帶入本題的環境,就可以得到下列等式:

P(<=N | >=K) = P(<=N && >=K) / P(>=K)

就是說拿到不小於K點的前提下,還能不超過N點的概率,等於拿到不小於K點且不超過N點的概率除以拿到不小於K點的概率。這樣,我們只要分別求出 P(<=N && >=K) 和 P(>=K) 就可以了:

P(<=N && >=K) = P(K) + P(K+1) + ... + P(N) = sumP(K, N)

P(>=K) = sumP(K, +∞) = sumP(K, K+W-1)

需要注意的是,一旦大於等於 K+W了,那么概率就為0了,所以邊界就從正無窮降到 K+W-1 了。既然說到了邊界,那么就來處理一下 corner case 吧,當 K=0 時,由於題目中說當前點數大於等於K,不能摸牌,那么一開始就不能摸牌了,而 K <= N,所以永遠不會超過N,概率返回1。還有就是當 N >= K+W 的時候,當我們大於等於K的時候,不能摸牌,此時不會超過N。當剛好為 K-1 的時候,此時還有一次摸牌機會,但最大也就摸個W,總共為 K-1+W,還是小於N,所以返回概率為1。

根據上面的條件概率公式推導,P(>=K) 的邊界降到了 K+W-1, 所以我們只要更新到這個邊界就都用了,因為 P(<=N && >=K) 的范圍是 [K, N],而 N 是要小於 K+W 的。我們新建一個大小為 K+W 的一維數組 sums,其中 sum[i] 表示獲得范圍 [0, i] 內的點數的概率綜合,初始化 sum[0] 為 1.0。下面來推導狀態轉移方程吧 ,通常來說,我們要更新 sum[i],那么只要知道了 sum[i-1],就只要算出 P[i],就行了,因為 sum[i] = sum[i-1] + P[i]。但這道題的更新其實比較復雜,要考慮兩個關鍵的位置,K和W,我們還是用經典的21點游戲來舉例說明吧,N=21, K=17, W=10。先來說一下當點數不超過 10 的更新方法,這個其實比較簡單,比如拿到七點的概率 P[7],根據我們上面對於 P(x) 的求法,我們知道可以拆分為下列多種情況:先拿到六點的概率 (P[6]) 乘以再拿一個1點的概率 (1/W),先拿到五點的概率 (P[5]) 乘以再拿一個2點的概率 (1/W),...,先拿到一點的概率 (P[1]) 乘以再拿一個六點的概率 (1/W),直接拿個七點的概率 (1/W),那么統統加起來,就是:

P[7] = 1/W * (P[6] + p[5] + ... + P[1] + P[0]) = 1/W * sum[6]

那么歸納一下,對於 i <= W 的情況下:

P[i] = 1/W * sum[i-1]

sum[i] = sum[i-1] + P[i] = sum[i-1] + sum[i-1] / W     (when i <= W)

那么當 i > W 的時候,情況是不一樣的,比如要求得到 15 點的概率 P[15],那么還是根據上面求 P(x) 的方法,拆分為下面多種情況:先拿到 14 點的概率 (P[14]) 乘以再拿一個1點的概率 (1/W),先拿到 13 點的概率 (P[13]) 乘以再拿一個2點的概率 (1/W),...,先拿到五點的概率 (P[5]) 乘以再拿一個 10 點的概率 (1/W),那么統統加起來就是:

P[15] = 1/W * (P[14] + P[13] + ... + P[5]) = 1/W * (sum[14] - sum[4])

那么歸納一下,對於 i > W 的情況下:

P[i] = 1/W * (sum[i-1] - sum[i-W-1])

sum[i] = sum[i-1] + P[i] = sum[i-1] + (sum[i-1] - sum[i-W-1]) / W     (when i > W)

到這里,你以為就大功告成了嗎?圖樣圖森破,嘛噠得斯。還有一個K呢,更新K以內的P值,和更新大於K的P值是稍有不同的,比如當 K=17 時,我們要更新 P[15],那么跟上面分析的一樣,同時還得考慮W的情況,歸納一下:

P[i] = 1/W * sum[i-1]     (when i <= K && i <= W)

P[i] = 1/W * (sum[i-1] - sum[i-W-1])    (when i <= K && i > W)

但是對於大於K的值,比如 P[20] 的更新方法就有所不同了,為啥呢?這要分析 20 點是怎么得來的,由於超過了 17 點就不能再摸牌了,所以 20 點只能由下列情況組成:先拿到 16 點的概率 (P[16]) 再拿到一個4點的概率 (1/W),先拿到 15 點的概率 (P[15]) 再拿到一個5點的概率 (1/W),...,先拿到 10 點的概率 (P[10]) 再拿到一個 10 點的概率 (1/W),那么統統加起來就是:

P[20] = 1/W * (P[16] + P[15] + P[14] + ... + P[10]) = 1/W * (sum[16] - sum[9])

那么我們歸納一下,就有:

P[i] = 1/W * sum[K-1]     (when i > K && i <= W)

P[i] = 1/W * (sum[K-1] - sum[i-W-1])    (when i > K && i > W)

講到這里,是不是頭暈又眼花,哈哈,博主也快繞暈了,最重要的四個式子已經加粗顯示了,K和W的大小關系其實是不知道的,不過我們可以把二者揉在一起,我們每次使用 i-1 和 K-1 中的較小值來算 P[i] 即可,這樣就完美把K融到了W的分類情況中,當 sum 數組計算完成之后,我們就直接按照上面的條件概率公式來算 P(<=N | >=K) = P(<=N && >=K) / P(>=K) = sumP(K, N) / sumP(K, K+W-1) 就行了,寫的累s博主了,聽個《青鳥》緩解一下吧,跟博主一起唱~阿歐伊,阿歐伊,阿弄嗖啦~

 

解法一:

class Solution {
public:
    double new21Game(int N, int K, int W) {
        if (K == 0 || N >= K + W) return 1.0;
        vector<double> sum(K + W);
        sum[0] = 1.0;
        for (int i = 1; i < K + W; ++i) {
            int t = min(i - 1, K - 1);
            if (i <= W) sum[i] = sum[i - 1] + sum[t] / W;
            else sum[i] = sum[i - 1] + (sum[t] - sum[i - W - 1]) / W;
        }
        return (sum[N] - sum[K - 1]) / (sum[K + W - 1] - sum[K - 1]);
    }
};

 

下面這種解法跟上面的解法沒有啥本質的區別,這里的 dp 數組跟上面的 sum 數組表達的意思是完全一樣的,dp[i] 表示獲得范圍 [0, i] 內的點數的概率綜合,初始化 dp[0] 為 1.0。希望博主在上面已經解釋清楚了,我們可以看到,這里並沒有將K融合到W的分類中,而是多加了 (K, i] 區間的部分,所以當 i > K 時就要將這部分多加的減去,從而符合題意。還有一點讓博主驚奇的地方是,這道題的條件概率和聯合概率是相同的,根據之前的條件概率公式:

P(<=N | >=K) = P(<=N && >=K) / P(>=K)

就是說拿到不小於K點的前提下,還能不超過N點的概率,等於拿到不小於K點且不超過N點的概率除以拿到不小於K點的概率。但是實際上這道題 P(<=N | >=K) = P(<=N && >=K),即拿到不小於K點的前提下,還能不超過N點的概率,等於拿到不小於K點且不超過N點的概率。那么就是說拿到不小於K點的概率的總是為1,想想也是阿,只有在拿到不少K點的時候才停止摸牌,這樣肯定點數不少於K點阿,單獨計算這個概率簡直是多此一舉啊,參見代碼如下:

 

解法二:

class Solution {
public:
    double new21Game(int N, int K, int W) {
        if (K == 0 || N >= K + W) return 1.0;
        vector<double> dp(K + W);
        dp[0] = 1.0;
        for (int i = 1; i < K + W; ++i) {
            dp[i] = dp[i - 1];
            if (i <= W) dp[i] += dp[i - 1] / W;
            else dp[i] += (dp[i - 1] - dp[i - W - 1]) / W;
            if (i > K) dp[i] -= (dp[i - 1] - dp[K - 1]) / W;
        }
        return dp[N] - dp[K - 1];
    }
};

 

下面這種解法還是大同小異,吃透了解法一的講解,看這些變形基本都比較好理解。這里的 dp 數組意義跟上面的一樣,但是並沒有初始化大小為 K+W,而是只初始化為了 N+1,為啥呢,根據解法二的講解,我們知道了這道題的條件概率和聯合概率是相等的,所以只要求出 P(<=N && >=K),即 dp[N] - dp[K-1],而這題不是更新完整個dp數組后再求聯合概率,而是在更新的過程中就累加到了結果 res,當 i >= K 的時候,正好可以將概率加入到結果 res,而且此時不用再累加 sumW,這里的 sumW 是保存的到目前為止的概率和,相當於之前的 dp[i-1],還需要判斷的是當 i >= W 時,要減去多加的概率 dp[i-W],參見代碼如下:

 

解法三:

class Solution {
public:
    double new21Game(int N, int K, int W) {
        if (K == 0 || N >= K + W) return 1.0;
        vector<double> dp(N + 1);
        dp[0] = 1.0;
        double sumW = 1.0, res = 0.0;
        for (int i = 1; i <= N; ++i) {
            dp[i] = sumW / W;
            if (i < K) sumW += dp[i];
            else res += dp[i];
            if (i - W >= 0) sumW -= dp[i - W];
        }
        return res;
    }
};

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/837

 

參考資料:

https://leetcode.com/problems/new-21-game/

https://leetcode.com/problems/new-21-game/discuss/132334/One-Pass-DP-O(N)

https://leetcode.com/problems/new-21-game/discuss/132478/C%2B%2B-12ms-O(K%2BW)-solution-with-explanation

https://leetcode.com/problems/new-21-game/discuss/132358/Java-O(K-%2B-W)-DP-solution-with-explanation

 

LeetCode All in One 題目講解匯總(持續更新中...)


免責聲明!

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



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