【交替数组】n个骰子的点数
抽象建模即选择合理的数据结构表述问题;分析模型中的内在规律,并用编程语言表述这种规律。
面试题60:n个骰子的点数
解法一(递归)
用递归的方式,每次拿出一个骰子,对1~6点再调用下一层,递归到底更新总数的频数。基本就是一个排列树穷举,有大量的重复计算。
作者递归出口是1,而且在第一步时没有减1,感觉非常难理解。我这里改成了递归出口是0,表示剩下0个骰子没判断过,在第一步时就减1,表示把第一个骰子判断了。
#include<bits/stdc++.h>
using namespace std;
int g_maxValue = 6;//六面骰子
//递归函数:递归调用每次拿出一个骰子,六个分支,更新频数数组
// 参数:
// original: 骰子总数
// current: 当前未计算的骰子数
// sum: 前面层已经计算得的骰子数总和
// pProbabilities: 要更新的频数数组
void Probability(int original, int current, int sum,
int* pProbabilities) {
if(current == 0) {//递归出口:如果骰子已经都计算过
pProbabilities[sum - original]++;//更新频数数组,即对应位置处+1
} else {//如果还剩多个骰子
for(int i = 1; i <= g_maxValue; ++i) {//再拿出一个骰子,对其的每种取值
//递归调用以更新频数数组,每个分支中当前骰子数减去这个骰子
//点数总数是局部自动变量,各自更新为当前点数+之前计算的点数和
Probability(original, current - 1, i + sum, pProbabilities);
}
}
}
//递归外套
//计算number个骰子朝上点数和的所有可能值出现的概率,保存在pProbabilities数组中
void Probability(int number, int* pProbabilities) {
for(int i = 1; i <= g_maxValue; ++i)//对第一个骰子的g_maxValue种取值
Probability(number, number-1, i, pProbabilities);//分别调用递归函数去更新频数数组
}
//打印number个骰子朝上点数和的所有可能值出现的概率
void PrintProbability_Solution1(int number) {
if(number < 1)//至少1个骰子
return;
int maxSum = number * g_maxValue;//可能出现的最大点数
int* pProbabilities = new int[maxSum - number + 1];//点数从number~maxSum,所以数组长n-m+1
for(int i = number; i <= maxSum; ++i)
pProbabilities[i - number] = 0;//总点数为i的频数会保存在i-number里
//计算number个骰子朝上点数和的所有可能值出现的概率,保存在pProbabilities数组中
Probability(number, pProbabilities);
int total = pow((double)g_maxValue, number);//n个骰子的所有点数的排列数为g_maxValue的n次方
for(int i = number; i <= maxSum; ++i) {//对每个可能的点数
double ratio = (double)pProbabilities[i - number] / total;//计算概率=频数/总数
printf("%d: %e\n", i, ratio);//打印
}
delete[] pProbabilities;//销毁堆空间的频数数组
}
int main() {
PrintProbability_Solution1(3);
return 0;
}
解法二(交替数组)
用两个数组交替协同第来做,就可以用循环来实现。每次循环加上一个新的骰子,此时和为n的骰子出现的次数就是上一轮的n-1一直加到n-6的次数的总和:
而没有映射的部分就表示不可能是这个数,比如3个骰子最小是3,下次加了一个骰子,变成4个骰子时候最小就是4了。
#include<bits/stdc++.h>
using namespace std;
int g_maxValue = 6;//六面骰子
void PrintProbability_Solution2(int number) {
if(number < 1)//骰子至少有1个
return;
int* pProbabilities[2];//两个数组
//申请空间
pProbabilities[0] = new int[g_maxValue * number + 1];
pProbabilities[1] = new int[g_maxValue * number + 1];
//全部初始化为0
for(int i = 0; i < g_maxValue * number + 1; ++i) {
pProbabilities[0][i] = 0;
pProbabilities[1][i] = 0;
}
//标识要操作的数组和辅助基准数组,它一改这两个数组的地位就交换了
int flag = 0;
//一开始只有一个骰子,在0号数组中记录抽到1~6的频数都是1
for (int i = 1; i <= g_maxValue; ++i)
pProbabilities[flag][i] = 1;
//接下来从2个骰子到number个骰子
for (int k = 2; k <= number; ++k) {
//k个骰子最小出k,所以小于k的都设置为0
for(int i = 0; i < k; ++i)
pProbabilities[1 - flag][i] = 0;
//接下来从k到6k都是可能出现的
for (int i = k; i <= g_maxValue * k; ++i) {
pProbabilities[1 - flag][i] = 0;//先清空为0
//例如六面骰i=3,这次出现3=上次出现2和上次出现1的
//例如六面骰i=50,这次出现50=上次出现49~44的
//所以这里是要注意i比面数还小的时候其实是受制于i的
for(int j = 1; j <= i && j <= g_maxValue; ++j)//这里其实应该写j<i而不是j<=i,不过p[flag][0]是0也不影响
pProbabilities[1 - flag][i] += pProbabilities[flag][i - j];//都加上来
}
flag = 1 - flag;//标识反转,即下次循环中两个数组角色反转
}
double total = pow((double)g_maxValue, number);//6面骰6^n种组合
for(int i = number; i <= g_maxValue * number; ++i) {//遍历所有可能数的频数
//计算概率,这里直接用flag因为最后一次for最后一句使flag已经反转了
double ratio = (double)pProbabilities[flag][i] / total;
printf("%d: %e\n", i, ratio);
}
delete[] pProbabilities[0];
delete[] pProbabilities[1];
}
int main() {
PrintProbability_Solution2(3);
return 0;
}