剑指Offer60—n个骰子的点数

这篇博客探讨了如何使用动态规划解决计算多个骰子投掷后所有可能点数和出现概率的问题。两种方法被提出:暴力法和动态规划法。暴力法因时间复杂度过高而不适用,而动态规划法通过正向和逆向递推公式实现,解决了越界问题并降低了空间复杂度。博客提供了C++实现代码示例。
摘要由CSDN通过智能技术生成

剑指Offer60

题意

把n个骰子扔在地上,所有骰子朝上一面的点数之和为s。输入n,打印出 s的所有可能的值 出现的概率。你需要用一个浮点数数组返回答案,其中第 i 个元素代表这 n 个骰子所能掷出的点数集合中第 i 小的那个的概率。

解法1—暴力法

给定 n 个骰子,可得:

  • 每个骰子摇到 1 至 6 的概率相等,都为\frac{1}{6}​ 。

  • 将每个骰子的点数看作独立情况,共有6^{n} 种「点数组合」。例如 n = 2时的点数组合为:

    • (1,1),(1,2),⋯,(1,6),

    • (2,1),(2,2),⋯,(2,6),

    • ...........

    • (6,1),(6,2),⋯,(6,6)。

  • n 个骰子「点数和」的范围为[n,6n] ,数量为 6n−n+1=5n+1 种,因此返回的结果中就要有 5*n+1项。


 暴力统计: 每个「点数组合」都对应一个「点数和」,考虑遍历所有点数组合,统计每个点数和的出现次数,最后除以点数组合的总数(即除以 6^{n}),即可得到每个点数和的出现概率

 如下图所示,为输入 n = 2 时,点数组合、点数和、各点数概率的计算过程。

 暴力法需要遍历所有点数组合,因此时间复杂度为 O(6^{n}) ,复杂度太高。

法2—动态规划

设输入 n 个骰子的解(即概率列表)为 f(n),其中「点数和x」出现的概率为 f(n, x) 。

 假设已知 n - 1 个骰子的解 f(n - 1) ,此时添加一枚骰子,求 n 个骰子的点数和为 x 的概率 f(n, x).

  1. 当添加的骰子的点数为 1 时,前 n - 1个骰子的点数和应为 x - 1,方可组成点数和 x;
  2. 同理,当此骰子为 2 时,前 n - 1 个骰子应为 x - 2;
  3. 以此类推,直至此骰子点数为 6

将这 6 种情况的概率相加,即可得到概率 f(n, x), 递推公式如下所示:

 为什么要乘一个\large \frac{1}{6}? 因为多了一个骰子后,每个骰子的点数组合就会变多,n个骰子有\large 6^{n}种组合情况。

根据以上分析,得知通过子问题的解 f(n - 1)可递推计算出 f(n) ,而输入一个骰子的解 f(1) 已知,因此可通过解 f(1) 依次递推出任意解 f(n) 。


如下图所示,为 n = 2,x=7 的递推计算示例。

观察发现,以上递推公式虽然可行,但 f(n - 1, x - i) 中的 x - i 会有越界问题。例如,若希望递推计算 f(2, 2) ,由于一个骰子的点数和范围为 [1, 6] ,因此只应求和 f(1, 1) ,因为只有上一个骰子的点数为1时,且当前骰子也为1时,两个骰子才能组成点数和为2。如果上一个骰子的点数超过1了,两个骰子点数和将会超过2。

即 f(1, 0) , f(1, -1) , ... , f(1, -4) 皆无意义。此越界问题导致代码编写的难度提升。


如下图所示,以上递推公式是 “逆向” 的,即为了计算 f(n, x),将所有与之有关的情况求和;而倘若改换为 “正向” 的递推公式便可解决越界问题。

 具体来看,由于新增骰子的点数只可能为 1 至 6,

因此概率 f(n - 1, x)仅与 f(n, x + 1) , f(n, x + 2), ... , f(n, x + 6)相关。即 (n-1)个骰子组成的点数和为x,那么新增的骰子点数只能为:1~6。然后 (n-1)个骰子与这新加的一个骰子组成的点数和范围为:x+1~x+6。

因而,遍历 f(n - 1) 中各点数和的概率,并将其相加至 f(n) 中所有相关项,即可完成 f(n - 1) 至 f(n) 的递推。

正向递推就不会产生越界问题。

具体实现: 

        通常做法是声明一个二维数组 dp ,dp[i][j] 代表前 i 个骰子的点数和 j 的概率,并执行状态转移。而由于 dp[i] 仅由 dp[i-1] 递推得出,为降低空间复杂度,只建立两个一维数组 dp 和 tmp 交替前进即可。

C++实现(正推)

class Solution 
{
public:
    vector<double> dicesProbability(int n) 
    {
        vector<double> res(6,1.0/6.0);  //当筛子只有一个的时候,每个点数的概率都是一样的,都是 1/6
        for(int i=2;i<=n;i++)
        {
            // i表示当前是共有 i 个筛子

            // 第i个筛子的点数和的范围为:[i,6*i],
            //共有(6i-i+1)=5i+1 个数量,每个数都有对应的概率,因此 i个筛子共要开5*i+1个空间
            vector<double> tmp(5*i+1,0);

            for(int j=0;j<res.size();j++)
            {
                for(int k=0;k<6;k++)
                {
                    // 上(i-1)个筛子的每个点数和x 对应 i个筛子 的 x+1..x+6点数和
                    tmp[j+k]+=(res[j]/6.0);
                }
            }
            res=tmp; //交替前进,迭代
        }
        return res;
    }
};

C++实现(逆推)

class Solution 
{
public:
    vector<double> dicesProbability(int n) 
    {
        vector<double> res(n*5+1);  //n个骰子组成的点数和范围:[n,6n],共5n+1种点数和

        // dp数组
    //一维长度为 n+1,下标范围为;[0,n],可以表示 1个骰子、2个骰子、...、n个骰子组成的点数和概率情况
    //二维长度为 6n+1,下标范围:[0,6n],因为n个骰子的点数和最大值为 6n
        vector<vector<double> > dp(n+1,vector<double>(6*n+1)) ;

        //初始化第一个骰子的所有点数和的概率
        for(int i=1;i<=6;i++)
        {
            dp[1][i]=1.0/6.0;
        }

        //然后分别计算第2个骰子、第三个骰子可以组成的点数和的概率值
        for(int i=2;i<=n;++i)
        {
            for(int j=i;j<=6*i;++j) // i个骰子组成的点数和的范围:[i,6i]
            {
                for(int k=1;k<=6;++k) //第i个骰子的点数为k时候的情况
                {
                    // 当(i-1)个骰子组成的点数和为 (j-k)时
                    // 加上第i个骰子的点数k,即可组成 i个骰子的点数和为 j 的情况。
                    if((j-k)>0) // 边界条件,如果j-k都≤0了,则没有意义
                    {
                        dp[i][j]+=dp[i-1][j-k]/6.0;
                    }
                    else
                    {
                        break;
                    }
                }
            }
        }

        for(int i=0;i<5*n+1;++i)
        {
            res[i]=dp[n][n+i];
        }

        return res;

    }
};

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

心之所向便是光v

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值