n個骰子的點數

異次元的歸來發表於2020-12-19

這題出自LeetCode,題目如下:

把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

首先,容易發現這是一道動態規劃的題,裡面每一種可能性太多了,且為了計算每一種可能性,裡面相互重疊的部分非常多,必然存在著大量重複的計算,而動態規劃本質就是為了避免子可能性的重複計算而提出的演算法。那麼,接下來,我們要思考如何將問題進行拆分成子問題,並且通過解決子問題就可以快速得到問題本身的答案。

我們看到,雖然問題是去求概率,但是投擲n個骰子所能得到的不同排列次數是固定的,即

\[N = 6^n \]

那麼問題轉換成用n個骰子組成和為s的不同排列次數有多少種,可以記為f(n,s)。此時,我們可以想象把這n個骰子分成兩堆,一堆有n-1個,另一堆只有1個。顯然,1個骰子擲出1的可能性只有1種,同樣,擲出2、3、4、5、6的可能性也只有1種。那麼問題轉化成了:

\[f(n,s) = \sum_{i=1}^{6}f(n,s-i) \]

容易知道,f(1,1),f(1,2),...f(1,6) = 1。我們可以先把這些邊界值填上,然後不斷地往上計算,就能得到最終的f(n,s)。程式碼如下:

class Solution {
public:
    vector<double> dicesProbability(int n) {
        int m = n * 6;
        int **a = new int *[m + 1];  
        for(int i = 0; i < m + 1; i++)  
        {  
            a[i] = new int[n + 1];
            for(int j = 0; j < n + 1; j++)  
            {
                a[i][j] = 0;
            }
        } 
        for(int i = 1; i <= m; i++)
        {
            if(i <= 6)
            {
                a[i][1] = 1;
            }
            else
            {
                a[i][1] = 0;
            }
        }

        for(int j = 2; j < n; j++)
        {
            a[1][j] = 0;
        }

        for(int i = 2; i <= m; i++)
        {
            for(int j = 2; j <= n; j++)
            {
                a[i][j] = calc(a, i, j);
            }
        }

        int all = 1;
        for(int j = 1; j <= n; j++)
        {
            all *= 6;
        }

        vector<double> res;
        for(int i = 1; i <= m; i++)
        {
            if(a[i][n] != 0)
            {
                res.push_back(a[i][n] / (all * 1.0));
            }
        }

        return res;
    }

    int calc(int **a, int m, int n)
    {
        int sum = 0;
        for(int i = 1; i <= 6; i++)
        {
            if(m - i > 0)
            {
                sum += a[m - i][n - 1];
            }
        }
        return sum;
    }
};

值得一提的是,可以看出我們的公式計算實際上只依賴前一個狀態,所以其實並不需要二維儲存空間,使用一維陣列就能解決問題。

class Solution {
public:
    vector<double> dicesProbability(int n) {
        int *a = new int[6 * n + 1];
        for(int i = 0; i <= 6 * n; i++)
        {
            a[i] = 0;
        }
        for(int i = 1; i <= 6; i++)
        {
            a[i] = 1;
        }

        for(int i = 2; i <= n; i++)
        {
            for(int j = 6 * i; j >= i; j--)
            {
                int s = j - 6;
                if(s <= i - 1)
                {
                    s = i - 1;
                }
                a[j] = 0;
                for(int k = s; k <= j - 1; k++)
                {
                    a[j] += a[k];
                }
            }
        }

        vector<double> res;
        int all = 1;
        for(int i = 1; i <= n; i++)
        {
            all *= 6;
        }

        for(int i = n; i <= 6 * n; i++)
        {
            if(a[i] != 0)
            {
                res.push_back(a[i] / (all * 1.0));
            }
        }

        return res;
    }
};

注意一點,在計算f(n, s)時,陣列前面6個f(n - 1, s - 1), ..., f(n - 1, s - 6)不一定都是有效的值,因為在計算f(n - 1, s)時,我們是從s = n - 1的情況下開始算的,這也好理解,畢竟n - 1個骰子擲出的最小點數之和必定是n - 1。所以我們在計算f(n, s)時要從第一個有效的值開始算起,即min(n - 1, s - 6)。否則可能會把前幾次計算遺留下的髒值給錯誤計算進去,得到不正確的結果。

相關文章