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*5
到6*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;
}