剑指offer-6-面试43:n 个骰子的点数()

题目

把 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)考查对递归和循环的性能的理解。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值