[每日一題] 第十六題:n個骰子的點數

DRose發表於2020-07-31

把n個骰子扔在地上,所有骰子朝上一面的點數之和為s。輸入n,列印出s的所有可能的值出現的機率。

你需要用一個浮點數陣列返回答案,其中第 i 個元素代表這 n 個骰子所能擲出的點數集合中第 i 小的那個的機率。

示例 1:

輸入: 1
輸出: [0.16667,0.16667,0.16667,0.16667,0.16667,0.16667]

示例 2:

輸入: 2
輸出: [0.02778,0.05556,0.08333,0.11111,0.13889,0.16667,0.13889,0.11111,0.08333,0.05556,0.02778]

限制:

  • 1 <= n <= 11

方法一:動態規劃

文章可能沒有很清晰的講述程式碼中的一些細節,但這些細節大多數都可以在評論中找到,我都進行了詳細的解釋,希望對大家有幫助。

解題思路

題目需要我們求出所有點數出現的機率,根據機率出現的計算公式,點數 k 出現機率計算公式為:

P(k)=k 出現次數 / 總次數

投擲 n 個骰子,所有點數出現的總次數是 6^n,因為一共有 n 枚骰子,每枚骰子的點數都有 6 種可能出現的情況。我們的目的就是 計算出投擲完 n 枚骰子後每個點數出現的次數。

使用遞迴造成重複計算的問題

單純使用遞迴搜尋解空間的時間複雜度為 6^n,會造成超時錯誤,因為存在重複子結構。解釋如下:

我們使用遞迴函式 getCount(n,k) 來表示投擲 n 枚骰子,點數 k 出現的次數。

為了簡化分析,我們以投擲 2 枚骰子為例。

我們來模擬計算點數 4 和點數 6,這兩種點數各自出現的次數。也就是計算 getCOunt(2,4) 和 getCount(2,6)。

他們的計算公式為:

getCount(2,4) = getCount(1,1) + getCount(1,2) + getCount(1,3)
getCount(2,6) = getCount(1,1) + getCount(1,2) + getCount(1,3) + getCount(1,4) + getCount(1,5)

我們發現遞迴統計這兩種點數的出現次數時,重複計算了 getCount(1,1) + getCount(1,2) + getCount(1,3)

這些子結構,計算其它點數的次數時同樣存在大量的重複計算。

動態規劃

使用動態規劃解決問題一般分為三步:

  1. 表示狀態
  2. 找出狀態轉移方程
  3. 邊界處理

下面我們一步一步分析,相信你一定會有收穫!

表示狀態

分析問題的狀態時,不要分析整體,只分析最後一個階段即可!因為動態規劃問題都是劃分為多個階段的,各個階段的狀態表示都是一樣,而我們的最終答案就是在最後一個階段。

對於這道題,最後一個階段是什麼呢?

透過題目我們知道一共投擲 n 枚骰子,那最後一個階段很顯然就是:當投擲完 n 枚骰子後,各個點數出現的次數。

注意:這裡的點數指的是前 n 枚骰子的點數和,而不是第 n 枚骰子的點數,下文同理。

找出了最後一個階段,那狀態表示就簡單了。

  • 首先用陣列的第一維來表示階段,也就是投擲完了幾枚骰子。
  • 然後用第二維來表示投擲完這些骰子後,可能出現的點數。
  • 陣列的值就表示,該階段各個點數出現的次數。

所以狀態表示就是這樣的 dp[i][j],表示投擲完 i 枚骰子後,點數 j 的出現次數。

找出狀態轉移方程

找狀態轉移方程也就是找各個階段之間的轉化關係,同樣我們還是隻需分析最後一個階段,分析它的狀態是如何做到的。

最後一個階段也就是投擲完 n 枚骰子後的這個階段,我們用 dp[n][j] 來表示最後一個階段點數 j 出現的次數。

單單看第 n 枚骰子,它的點數可能為 1,2,3,4,5,6,因為投擲完 n 枚骰子後點數 j 出現的次數,可以由投擲完 n-1 枚骰子後,對應點數 j-1,j-2,j-3,j-4,j-5,j-6 出現的次數轉化過來。

for (第n枚骰子的點數 i = 1; i <= 6; i ++) {
    dp[n][j] += dp[n-1][j - i]
}

寫成數學公式是這樣的:

          6
dp[n][j]= ∑  dp[n−1][j−i]
         i=1    

n 表示階段,j 表示投擲完 n 枚骰子後的點數和,i 表示第 n 枚骰子會出現的六個點數。

邊界處理

這裡的邊界處理很簡單,只要我們把可以直接知道的狀態初始化就好了。

我們可以直接指導的狀態是啥,就是第一階段的狀態:投擲完 1 枚骰子後,它的可能點數分別為 1,2,3,4,5,6,並且每個點數出現的次數都是 1.

for (int i = 1; i <= 6; i ++) {
    dp[1][i] = 1;
}

程式碼

class Solution {
public:
   vector<double> twoSum(int n) {
       int dp[15][70];
       memset(dp, 0, sizeof(dp));
       for (int i = 1; i <= 6; i ++) {
           dp[1][i] = 1;
       }
       for (int i = 2; i <= n; i ++) {
           for (int j = i; j <= 6*i; j ++) {
               for (int cur = 1; cur <= 6; cur ++) {
                   if (j - cur <= 0) {
                       break;
                   }
                   dp[i][j] += dp[i-1][j-cur];
               }
           }
       }
       int all = pow(6, n);
       vector<double> ret;
       for (int i = n; i <= 6 * n; i ++) {
           ret.push_back(dp[n][i] * 1.0 / all);
       }
       return ret;
   }
}; 

空間最佳化

我們知道,每個階段的狀態都只和它前一階段的狀態最佳化,因此我們不需要額外的一維來儲存所有階段。

用一維陣列來儲存一個階段的狀態,然後對下一階段可能出現的點數 j 從大到小遍歷,實現一個階段一個階段的轉換。

最佳化程式碼如下:

class Solution {
public:
    vector<double> twoSum(int n) {
        int dp[70];
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= 6; i ++) {
            dp[i] = 1;
        }
        for (int i = 2; i <= n; i ++) {
            for (int j = 6*i; j >= i; j --) {
                dp[j] = 0;
                for (int cur = 1; cur <= 6; cur ++) {
                    if (j - cur < i-1) {
                        break;
                    }
                    dp[j] += dp[j-cur];
                }
            }
        }
        int all = pow(6, n);
        vector<double> ret;
        for (int i = n; i <= 6 * n; i ++) {
            ret.push_back(dp[i] * 1.0 / all);
        }
        return ret;
    }
};

個人理解

  1. 感覺用遞迴的題都可以用動態規劃來實現。
  2. 使用動態規劃的話,就得實現動態規劃的三步:狀態狀態轉移方程邊界值
  3. 當我們確定了上述三步之後,其實我們已經解決了這個題。
  4. 各個陣列迴圈起始、陣列大小一定要搞清楚。

附上一個我寫的 java 版本的程式碼

class Solution {
    public double[] twoSum(int n) {
        double[] result = new double[5*n+1];
        int total = (int) Math.pow(6,n);
        int[][] dp = new int[n+1][6*n+1];
        for (int i = 1 ; i <= 6; i++) {
            dp[1][i] = 1;
        }
        for (int i = 2; i <= n; i++) {
            for (int j = i; j <= 6*i; j++) {
                for (int current = 1; current <= 6; current ++) {
                    if (j <= current) {
                        break;
                    }
                    dp[i][j] += dp[i-1][j-current];
                }
            }
        }

        for (int i = n; i <= 6*n; i++) {
            result[i-n] = (double) dp[n][i] / total;
        }

        return result;
    }
}

題解來源

作者:huwt
連結:leetcode-cn.com/problems/nge-tou-z...
來源:力扣(LeetCode)

來源:力扣(LeetCode)
連結:leetcode-cn.com/problems/nge-tou-z...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章