[剑指-Offer] 60. n个骰子的点数(递归、动态规划、巧妙解法)

1. 题目来源

链接:n个骰子的点数
来源:LeetCode——《剑指-Offer》专项

2. 题目说明

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

3. 题目解析

方法一:递归+剪枝+常规解法

你管这叫简单题???

n 个骰子,记住,这个字念 tou(一声)子… 的点数和的最小值为 n,最大值为 6n。那么所有点数的排列数就是 6 n 6^n 6n 了。所有需要统计点数出现的次数再除以 6 n 6^n 6n,就能求出每个点数出现的概率了。

现在我们考虑如何统计每一个点数出现的次数。要想求出 n 个骰子的点数和,有以下思路:

  • 先把 n 个骰子分为两堆:第一堆只有一个,另一个有 n-1
  • 单独的一个可能出现从 1 到 6 的点数,需要计算从 1 到 6 的每一种点数和剩下的 n-1 个骰子来计算点数和
  • 接下来把剩下的 n-1 个骰子还是分成两堆,第一堆只有一个,第二堆有 n-2
  • 把上一轮那个单独骰子的点数和这一轮单独骰子的点数相加,再和剩下的 n-2 个骰子来计算点数和

分析到这里,我们不难发现这是一种递归的思路,递归结束的条件就是最后只剩下一个骰子。其本质也就是求数列 f ( n ) = f ( n − 1 ) + f ( n − 2 ) + f ( n − 3 ) + f ( n − 4 ) + f ( n − 5 ) + f ( n − 6 ) f(n)=f(n-1)+f(n-2)+f(n-3)+f(n-4)+f(n-5)+f(n-6) f(n)=f(n1)+f(n2)+f(n3)+f(n4)+f(n5)+f(n6)

至此,我们可以定义一个长度为 6n-n+1 的数组,因为和的范围就是 n~6n,和为 s 的点数出现的次数保存到数组的第 s - n 个元素里。

但是参考《剑指-Offer》的写法,会出现 TLE,最后一个样例过不去。其实可以考虑下怎么优化这个问题,使一维递归能够 AC。一开始从控制台单过 n=11 都过不了,想着优化,发现,这个概率是对称的,所以每次只需要算一半即可,经过修改的代码又出了点问题…n=1 过不了…然后看了看发现少了一组 6,直接把 1 的答案打表输出了…然后,提交,还是没有过,TLE 总的时间还是超过了,算了吧,至此了,挖个坑,回看的时候来填。

参见代码如下:

// 控制台单过11都过不了

class Solution {
public:
    vector<double> twoSum(int n) {
        int size = 6 * n;
        vector<double> v;
        vector<int> vt(size - n + 1, 0);
        help(n, vt);
        int total = pow(6, n);
        for (int i = n; i <= size; ++i) {
            double res = (double)vt[i - n] / total;
            v.push_back(res);
        }
        return v;
    }

    void help(int n, vector<int>& vt) {
        for (int i = 1; i <= 6; ++i) help(n, n, i, vt);
    }

    void help(int num, int tmp, int sum, vector<int>& vt) {
        if (tmp == 1) vt[sum - num]++;
        else {
            for (int i = 1; i <= 6; ++i)
                help(num, tmp - 1, i + sum, vt);
        }
    }
};

// 控制台能过11,总时间超TLE
class Solution {
public:
    vector<double> twoSum(int n) {
        if (n == 1) return {0.16667,0.16667,0.16667,0.16667,0.16667,0.16667};
        int size = 6 * n;
        vector<double> v;
        vector<int> vt(size - n + 1, 0);
        help(n, vt);
        int total = pow(6, n);
        for (int i = n; i < size / 2 + 6; ++i) {
            double res = (double)vt[i - n] / total;
            v.push_back(res);
        }
        for (int i = size / 2 - 6; i >= 0; --i) v.push_back(v[i]);
        return v;
    }

    void help(int n, vector<int>& vt) {
        for (int i = 1; i <= 6; ++i) help(n, n, i, vt);
    }

    void help(int num, int tmp, int sum, vector<int>& vt) {
        if (tmp == 1) vt[sum - num]++;
        else {
            for (int i = 1; i <= 6; ++i)
                help(num, tmp - 1, i + sum, vt);
        }
    }
};

方法二:动态规划+迭代+巧妙解法

d p [ i ] [ j ] dp[i][j] dp[i][j] 表示当 n = i n=i n=i 时,和为 j j j 出现的排列情况总数

状态转移方程: d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + d p [ i − 1 ] [ j − 2 ] + d p [ i − 1 ] [ j − 3 ] + d p [ i − 1 ] [ j − 4 ] + d p [ i − 1 ] [ j − 5 ] + d p [ i − 1 ] [ j − 6 ] dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+dp[i-1][j-3]+dp[i-1][j-4]+dp[i-1][j-5]+dp[i-1][j-6] dp[i][j]=dp[i1][j1]+dp[i1][j2]+dp[i1][j3]+dp[i1][j4]+dp[i1][j5]+dp[i1][j6]

初始条件: d p [ 1 ] [ 1 ] = d p [ 1 ] [ 2 ] = d p [ 1 ] [ 3 ] = d p [ 1 ] [ 4 ] = d p [ 1 ] [ 5 ] = d p [ 1 ] [ 6 ] = 1 dp[1][1]=dp[1][2]=dp[1][3]=dp[1][4]=dp[1][5]=dp[1][6]=1 dp[1][1]=dp[1][2]=dp[1][3]=dp[1][4]=dp[1][5]=dp[1][6]=1

参见代码如下:

// 执行用时 :0 ms, 在所有 C++ 提交中击败了100.00%的用户
// 内存消耗 :8 MB, 在所有 C++ 提交中击败了100.00%的用户

class Solution {
public:
	vector<double> twoSum(int n) {
		vector<vector<int>> dp(n + 1, vector<int>(6 * n + 1, 0));
		double num = pow(6, n);
		vector<double> res(5 * n + 1, 1 / (double)6);
		//初始状态
		for (int i = 1; i <= 6; i++) dp[1][i] = 1;
         //从2到n计算dp
		for (int i = 2; i <= n; i++) {     
            //表示当n=i时候的点数和取值从i到6i
			for (int j = i; j <= i * 6; j++) {  
                //dp[i][j]=dp[i-1][j-1]+dp[i-1][j-2]+dp[i-1][j-3]+dp[i-1][j-4]+dp[i-1][j-5]+dp[i-1][j-6];
				for (int k = 1; k <= 6; k++) {  
                    //第i个骰子点数一定比i-1个骰子点数大
					if (j - k > 0) dp[i][j] += dp[i - 1][j - k]; 
					if (i == n) res[j - i] = dp[i][j] / num;
				}
			}
		}
		return res;
	}
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Ypuyu

如果帮助到你,可以请作者喝水~

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

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

打赏作者

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

抵扣说明:

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

余额充值