warning: 本文着重于分析,较为啰嗦冗长,如果您是想通过代码独立理清思路,请直接跳到60.3.3小节

60.1 题目

剑指 Offer 60. n个骰子的点数

把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

60.2 n个色子总共有几种和?

假设我们有5个色子,那么这五个色子能构成的最小值是多少? 对,就是所有色子都为1的时候。那么最大值呢?就是所有色子都为6的时候。

即:
max = 6 * 5
min = 1 * 5

而且由于色子里面存在1这个点数,所以从1*56*5内所有的数字都是连续的正整数,所以总共能产生6*5 - 1*5 + 1 = 26种和。

把5换成n,我们就得到了一个关于n个色子能构成的和的数量的通式:
number = 6*n - 1*n + 1 = 5*n + 1

60.3 正式计算各个和的概率

60.3.1 动态规划,将每一个和进行拆分

比如你有六个色子,排成1 2 3 4 5 6,即结果为21,那么这个可能性如何计算?

现在我们停下来思考一下,要计算这个结果,我们需要哪些必要条件?

  • 总共有多少个色子,即色子数n
  • n个色子构成的和,即结果s

这两个条件缺一不可。那么我们设函数f(n,s)=n个色子组成s的可能性。别害怕,写这个f只是为了表述方便,不会牵扯太多数学公式(几乎没有)。

接下来我们继续上面的问题:六个色子,如果组合成21,可能性有多大?

这里我们要采用一下动态规划的思想:本次的可能性可以使用上一次的可能性来获得。

1 2 3 4 5 6这个色子序列,我们完全可以看成你先用5个色子摇出了15然后又用一个色子摇出了6

  • possibility of 15 = f(5,15)(5个色子摇出15的概率);
  • possibility of 6 = f(1,6) = 1/6(就是你用一个色子摇出六的概率);

那么事实上1 2 3 4 5 6这个序列可以看作六种组合


当然,1 2 3 4 5 6这个序列是一个特例,记住,我们最需要关注的是他们的和,而不是组成这个和的序列

那么这一小节我们懂得了什么?就是n个色子构成的和,可以用n-1个色子的点数构成的和加上一个色子的点数来替代。

60.3.2 现在我们来转换一下问题

上一小节我们把基本的算法思想介绍了一下,正所谓说起来容易做起来难,如果落实到代码上,具体应该怎么做?

先,我们需要一个数组来记录结果,不如就叫res_pro吧。这个数组要多大呢?可以看看之前60.2的证明,总共是5*n + 1个和,那么数组当然也得是这么大。

double[] res_pro = new double[5*n + 1]

下来,我们需要有一个数组,这个数组记录着n-1个色子各个和的可能性(这个怎么得来的咱们一会再说,不要急哈,主要是先把上面的算法逻辑先写出来)。我们就叫这个数组pre_pro吧。

与此同时,我们知道,设n-1个色子构成的和的数组为pre,n个色子构成的和的数组为res,那么如果res[j] = pre[m1] + a1(a为从1到6的某个数) = pre[m2] + a2 = ......,那么则有res_pro[j] = pre_pro[m1]×(1/6) + pre_pro[m2]×(1/6) + ......

所以问题变成了探寻由n个色子构成的每个和与n-1个色子构成的和的关系。

如下图,第一行pre是n=2的所有可能的和,第二行res是n=3时所有可能的和。一般情况下,例如res[6]=9,9可以由pre的38加上16里面的数来表示

那有没有例外呢?有!


  • 第一种例外pre中与res相对应的位置往前不够6位,即从j指针的位置向前走k(k<6)位,发现j-k<0那么我们可以停止往前走了——break
  • 第二种是j-k>=pre.length,比如j=11,对应的pre的相应位置上没有数,那么停止在k=0上磨蹭,继续前进——continue

60.3.3 代码

估计很多人看到这里已经开始要打人了,一个小题干嘛弄那么复杂。

代码上场:


public double[] twoSum2(int n) {
   
    //首先定义一个初始的可能性数组
    //当n=1时,总共有六种可能,每种可能都是1/6
        double[] pre_pro = new double[]{
   1/6d, 1/6d, 1/6d, 1/6d, 1/6d, 1/6d};
        //用到动态规划的思想,用n=1的数据获得n=2的,用n=2获得n=3...用n-1获得n
        for (int i = 2; i <= n; i++) {
   
            //用之前的公式,创建一个确定大小的数组来存储每次循环获得的新的可能性数组
            double[] res_pro = new double[i * 6 - i + 1];
            for (int j = 0; j < res_pro.length; j++) {
   
                for (int k = 0; k < 6; k++) {
   
                    int t = j - k;
                    //这两个条件我前面已经说了
                    if (t < 0) {
   
                        break;
                    }
                    if (t >= pre_pro.length) {
   
                        continue;
                    }
                    //pre[t]/6 其实就是pre[t] * (1/6)
                    res_pro[j] += pre_pro[t]/6;
                }
            }

            pre_pro = res_pro;
        }
        return pre_pro;
    }