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
。那么所有点数的排列数就是
了。所有需要统计点数出现的次数再除以
,就能求出每个点数出现的概率了。
现在我们考虑如何统计每一个点数出现的次数。要想求出 n
个骰子的点数和,有以下思路:
- 先把
n
个骰子分为两堆:第一堆只有一个,另一个有n-1
个 - 单独的一个可能出现从 1 到 6 的点数,需要计算从 1 到 6 的每一种点数和剩下的
n-1
个骰子来计算点数和 - 接下来把剩下的
n-1
个骰子还是分成两堆,第一堆只有一个,第二堆有n-2
个 - 把上一轮那个单独骰子的点数和这一轮单独骰子的点数相加,再和剩下的
n-2
个骰子来计算点数和
分析到这里,我们不难发现这是一种递归的思路,递归结束的条件就是最后只剩下一个骰子。其本质也就是求数列
至此,我们可以定义一个长度为 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);
}
}
};
方法二:动态规划+迭代+巧妙解法
表示当 时,和为 出现的排列情况总数
状态转移方程:
初始条件:
参见代码如下:
// 执行用时 :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;
}
};