题目
把 n 个骰子扔在地上,所有骰子朝上一面的点数之和为 s。 输入 n,打印出 s 的 所有可能的值出现的概率。
骰子对应的点数是1-6之间的一个数字。所以,n 个骰子的点数和的最小值为 n,最大值为 6n。另外根据排列组合的知识,还知道n个骰子的所有点数的排列数为 6^n。要解决这个问题,需要先统计出每一个点数出现的次数,然后把每一个点数出现的次数除以 6^n,就能求出每个点数出现的概率。
分析
解法一:基于递归求骰子点数,时间效率不够高
考虑如何统计每一个点数出现的次数。要想求出n个骰子的点数和,可以先把n个骰子分为两堆:第一堆只有一个,另一个有n-1个。单独的那一个有可能出现从 1到 6的点数。我们需要计算从 1到6的每一种点数和剩下的 n-1个骰子来计算点数和。接下来把剩下的n-1个骰子还是分成两堆,第一堆只有一个,第二堆有 n-2 个。我们把上一轮那个单独骰子的点数和这一轮单独骰子的点数相加,再和剩下的 n-2 个骰子来计算点数和。分析到这里,不难发现是一种递归的思路,递归结束的条件就是最后只剩下一个骰子。
可以定义一个长度为 6n-n+1 的数组,和为 s的点数出现的次数保存到数组第 s-n 个元素里。
int g_maxValue = 6;
void PrintProbability_Solution1(int number)
{
if(number < 1)
return;
int maxSum = number * g_maxValue;
int* pProbabilities = new int[maxSum - number + 1];
for(int i = number; i <= maxSum; ++i)
pProbabilities[i - number] = 0;
Probability(number, pProbabilities);
int total = pow((double)g_maxValue, number);
for(int i = number; i <= maxSum; ++i)
{
double ratio = (double)pProbabilities[i - number] / total;
printf("%d: %e\n", i, ratio);
}
delete[] pProbabilities;
}
void Probability(int number, int* pProbabilities)
{
for(int i = 1; i <= g_maxValue; ++i)
Probability(number, number, i, pProbabilities);
}
void Probability(int original, int current, int sum,
int* pProbabilities)
{
if(current == 1)
{
pProbabilities[sum - original]++;
}
else
{
for(int i = 1; i <= g_maxValue; ++i)
{
Probability(original, current - 1, i + sum, pProbabilities);
}
}
}
上述思路简洁,实现起来也容易。但由于是基于递归的实现,有很多计算是重复的,从而导致当number变大时性能慢得让人不能接受。
基于循环求骰子点数,时间性能好
可以换一种思路来解决这个问题。可以考虑用两个数组来存储骰子点数的每一个总数出现的次数。在一次循环中,第一个数组中的第 n 个数字表示骰子和为 n 出现的次数。在下一循环中,加上一个新的骰子,此时和为n 的骰子出现的次数应该等于上一次循环中骰子点数和为 n-1、n-2、n-3、n-4、n-5 与 n-6的次数的总和,所以我们把另一个数组的第 n 个数字设为前一个数组对应的 第 n-1、n-2、n-3、n-4、n-5 与 n-6之和。
// ====================方法二====================
void PrintProbability_Solution2(int number)
{
if(number < 1)
return;
int* pProbabilities[2];
pProbabilities[0] = new int[g_maxValue * number + 1];
pProbabilities[1] = new int[g_maxValue * number + 1];
for(int i = 0; i < g_maxValue * number + 1; ++i)
{
pProbabilities[0][i] = 0;
pProbabilities[1][i] = 0;
}
int flag = 0;
for (int i = 1; i <= g_maxValue; ++i)
pProbabilities[flag][i] = 1;
for (int k = 2; k <= number; ++k)
{
for(int i = 0; i < k; ++i)
pProbabilities[1 - flag][i] = 0;
for (int i = k; i <= g_maxValue * k; ++i)
{
pProbabilities[1 - flag][i] = 0;
for(int j = 1; j <= i && j <= g_maxValue; ++j)
pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];
}
flag = 1 - flag;
}
double total = pow((double)g_maxValue, number);
for(int i = number; i <= g_maxValue * number; ++i)
{
double ratio = (double)pProbabilities[flag][i] / total;
printf("%d: %e\n", i, ratio);
}
delete[] pProbabilities[0];
delete[] pProbabilities[1];
}
在上述代码中,我们定义了两个数组 pProbabilities[0] 和 pProbabilities[1] 来存储骰子的点数之和。在一轮循环中,一个数组的第 n 项等于另一数组的第 n-1、n-2、n-3、n-4、n-5 以及 n-6项的和。 在下一轮循环中,我们交换这两个数组(通过改变变量 flag 实现)再重复这一计算过程。
值得注意的是,上述代码没有在函数里把一个骰子的最大点数硬编码(hard code)为 6,而是用一个变量 g_maxValue来表示。这样做的好处是,如果某个厂家生产了其他点数的骰子,我们只需要在代码中修改一个地方,扩展起来方便。如果在面试的时候我们能对面试官提起对程序扩展性的考虑,一定能给面试官留下很好的印象。
测试用例&代码
(1)功能测试(1、2、3、4个骰子的各点数的概率)
(2)特殊输入测试(输入0)
(3)性能测试(输入较大的数字,比如 11)
本题考点
(1)数学建模的能力。不管采用哪种思路解决问题,我们都要先想到用数组来存放n个骰子的每一个点数出现的次数,并通过分析点数的规律建立模型并最终找到解决方案。
(2)考查对递归和循环的性能的理解。