面试题60:n个骰子的点数。把n个骰子扔在地上,所有骰子朝上一面的点数之和为s,输入n,打印出s的所有可能的值出现的概率。
n个骰子点数和最小为n,最大为6n,所有点数的排列数为6n,因此我们需要求出每一种点数出现的次数,除以总排列数6n即为该点数出现的概率。
法一:要想求出n个骰子的点数之和,先把骰子分为两堆,一堆只有一个,另一堆有n-1个。我们此时循环第一堆那个骰子的所有点数。第一次循环假设第一堆的那个骰子扔出1点,之后再将第二堆的所有骰子分为两堆,一堆只有一个,另一堆有n-2个,此时再循环第二次分堆时只有一个骰子的所有点数,将该点数与第一次循环的点数相加,直到最后只剩下一个骰子,此时一个全排列就找到了。
我们需要一个长度为所有可能出现的点数之和的数组来保存每个点数之和出现的次数。该数组大小为6n-n+1,将和为s的点数出现的次数保存在数组的下标为s-n的位置,因为最小的和为n:
#include <iostream>
#include <vector>
using namespace std;
void GetSumAppearanceTime(int n, int currN, int currSum, vector<int>& sumAppearanceTime) { // n为骰子数,currN为还要循环几个骰子,currSum为当前投出的点数和,sumAppearanceTime存储每种点数出现的次数
if (currN == 0) {
++sumAppearanceTime[currSum - n];
} else {
for (int i = 1; i <= 6; ++i) {
GetSumAppearanceTime(n, currN - 1, currSum + i, sumAppearanceTime);
}
}
}
void PrintProbability(int n) {
if (n < 1) { // n为骰子数
return;
}
int maxSum = 6 * n; // n个骰子能扔出的最大点数和
vector<int> sumAppearanceTime(5 * n + 1); // 保存每种点数和出现的次数
GetSumAppearanceTime(n, n, 0, sumAppearanceTime);
int base = pow(6, n); // 骰子点数的全排列数
for (int i = n; i <= maxSum; ++i) {
cout << i << "出现次数为:" << sumAppearanceTime[i - n] << ",出现概率为:" << static_cast<double>(sumAppearanceTime[i - n]) / base << endl;
}
}
int main() {
PrintProbability(2);
}
以上代码时间复杂度太高。
法二:定义f(n,k)为投掷n个骰子时,点数k出现的次数,存在公式f(n,k)=f(n−1,k−1)+f(n−1,k−2)+f(n−1,k−3)+f(n−1,k−4)+f(n−1,k−5)+f(n−1,k−6):
#include <iostream>
#include <vector>
using namespace std;
int g_MaxValue = 6; // 将最大点数用全局变量表示,如有其它点数的骰子,直接修改此处即可,程序扩展性好
void PrintProbability(int n) {
if (n < 1) {
return;
}
vector<vector<int>> res(2);
res[0].resize(g_MaxValue * n + 1);
res[1].resize(g_MaxValue * n + 1); // 两个数组下标的含义为点数和,下标表示的值表示该点数和出现的次数,两个向量的大小都为可能出现的最大点数和+1,但和不会为0,因此只需要6*n的大小,但加上0下标书写容易
int flag = 0;
for (int i = 1; i <= g_MaxValue; ++i) { // 初始化为6个1,从下标1开始
res[flag][i] = 1;
}
for (int i = 2; i <= n; ++i) { // i表示当前在计算投i个骰子的情况
for (int j = 0; j < i; ++j) { // i个骰子的可能的和最小为i,将此之前的值初始化,防止下次循环时加到此值之前的值
res[1 - flag][j] = 0;
}
for (int j = i; j <= g_MaxValue * i; ++j) { // j的范围为i到i*6,表示投i个骰子可能出现的所有和
res[1 - flag][j] = 0; // 初始化当前位置出现的次数
for (int k = 1; k <= g_MaxValue && k <= j; ++k) { // k的范围为1~6,表示公式中k减的值,此时k必须还要小于等于j,因为j是从2开始的,这保证了向前找出现次数时不会出界
res[1 - flag][j] += res[flag][j - k]; // k<=j保证了j-k不会小于0
}
}
flag = 1 - flag; // 交换两数组身份
}
int base = pow(g_MaxValue, n);
for (int i = n; i <= n * g_MaxValue; ++i) {
cout << i << "出现次数为:" << res[flag][i] << ",出现概率为:" << static_cast<double>(res[flag][i]) / base << endl;
}
}
int main() {
PrintProbability(2);
}