把n个骰子仍在地上,所有骰子朝上一面的点数之和为s。输入骰子个数n,求所有s出现的概率。
最直接的方法, 把所有的情况都列出来, 然后统计出现和的次数, 用 (s出现的次数/6^n) 就是对应的概率.
用一个数组装所有的可能, 递归调用装满数组, 当数组的大小等于n的时候说明装满了, 对数组计算一次和, 并存入到字典. 然后在处理字典, 用value/6^n 就是对应的概率了.
- (void)viewDidLoad {
[super viewDidLoad];
[self probability];
}
/// n个骰子的点数概率,枚举法
- (void)probability {
// 每个骰子可能的结果是6个
// 共有n个骰子
int size = 6;
int n = 3;
NSDictionary * dic = [self __sumAllPossibilityWithSize:size count:n];
NSLog(@"%@",dic);
// 升序排列, 方便下面打印
NSArray * allKeys = [dic.allKeys sortedArrayUsingComparator:^NSComparisonResult(NSNumber * _Nonnull obj1, NSNumber * _Nonnull obj2) {
return obj1.intValue>obj2.intValue;
}];
for (NSNumber * key in allKeys) {
NSNumber * time = dic[key];
NSLog(@"和为%@的概率为%lf",key,time.intValue/pow(size, n));
}
}
/// 递归算出每种可能, 返回一个字典, key是和, value是对应和的数量
/// @param size 可能的结果数量,此处恒等于6
/// @param n 几个骰子
- (NSDictionary *)__sumAllPossibilityWithSize:(int)size count:(int)n {
// 用这个数组来装每种可能的结果
static NSMutableArray * array = nil ;
if (array == nil) {
array = [NSMutableArray arrayWithCapacity:3];
}
// key是骰子和, value是对应和的数量
static NSMutableDictionary * dic = nil;
if (dic == nil) {
dic = [NSMutableDictionary dictionary];
}
if (array.count == n) {
int sum = [[array valueForKeyPath:@"@sum.self"] intValue];
dic[@(sum)] = @( [dic[@(sum)] intValue] + 1 );
return dic;
}
for (int i = 1; i<=size; i++) {
[array addObject:@(i)];
[self __sumAllPossibilityWithSize:size count:n];
[array removeLastObject];
}
return dic;
}
在实际测试中, 当n等于7以上的时候就可以感觉到明显的延迟了, 从开始算法到结果需要7s左右. 6^7=27936, 大约有28W种结果, 计算起来效率确实不好.
n个骰子的问题还可以看出是一个动态规划问题。首先该问题具备DP的两个特征:最优子结构性质和子问题的重叠性。具体的表现在:
(1)n个骰子的点数和依赖于n-1个骰子的点数和,相当于在n-1个骰子点数的基础上再进行投掷一次。
(2)求父问题的同时,需要多次利用子问题。由此定义状态转移方程为f(n,k), 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)
其中 n>0且k<=6n。其中f(n−1,k−i)表示的是第n-1次掷骰子时,骰子的点数为k-i对应的情况,所以从k−1到k−6分别对应第n次掷骰子时骰子正面为1到6的情况。而初始状态可以定义为:
f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1.
- (void)viewDidLoad {
[super viewDidLoad];
[self probability2];
}
/// n个骰子的点数概率,
- (void)probability2 {
// 每个骰子可能的结果是6个
// 共有n个骰子
int n = 12;
int size = 6;
for (int i = n; i<=n*size; i++) {
int time = [self __getPossibilityWithNum:i size:size count:n];
NSLog(@"和为%d的概率为%lf,次数为%d",i,time/pow(size, n),time);
}
}
// [n+1][6*n+1], 由于c语言不太熟练, 就在这里假设有20个骰子, 用来做缓存, 防止递归的重复计算
int possibility[21][121]={};
- (int)__getPossibilityWithNum:(int)num size:(int)size count:(int)n {
if (num<=0) {
return 0;
}
if (n==1&&num>size) {
return 0;
}
// 这里就是 f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1.
if (n==1&&num<=size) {
return 1;
}
// 如果已经有缓存数据了, 取缓存数据, 防止重复计算
if (possibility[n][num] > 0) {
return possibility[n][num];
}
// 这里对应 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)
int sum = 0;
for (int i=1; i<=size; i++) {
sum += [self __getPossibilityWithNum:(num-i) size:size count:n-1];
}
possibility[n][num] = sum;
return sum;
}
这个算法就是处理起来就已经快多了,
没有使用缓存的情况下,计算7个骰子在0.1s以内,计算10个骰子的时间在14s左右, 计算12个骰子大约需要14*36=504秒
使用缓存的情况下, 计算7个骰子在0.1s以内, 计算10个骰子的时间在0.5s左右, 计算12个骰子在13s左右,
动态规划 有递归的版本, 自然也有循环的版本, 目的是求n个骰子, 我不管求几个, 都从1个骰子开始计算, 从1计算到n即可.
声明一个数组, result[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)
初始值: f(1,1)=f(1,2)=f(1,3)=f(1,4)=f(1,5)=f(1,6)=1. 其他值都设为0.
以n=3,画图
- (void)viewDidLoad {
[super viewDidLoad];
[self probability3];
}
/// n个骰子的点数概率, 动态规划,循环
- (void)probability3 {
// 每个骰子可能的结果是6个
// 共有n个骰子
int n = 20;
int size = 6;
// [n+1][6*n+1],
// result[m][k],表示m个骰子出现和为k的数量
NSInteger result[n+1][size*n+1];
// 初始值都设置为0
memset(result, 0, sizeof(result));
// 1个骰子的话,每个都是1
for (int i = 1; i<=size; i++) {
result[1][i] = 1;
}
// 2个骰子以上,开始计算
for (int i = 2; i<=n; i++) {
// 计算i个骰子出现和为j的数量
for (int j = i; j<=size*i; j++) {
// 可以放心使用j-6,不需要判断和0的关系,比如result[1][-4], 实际就是result[0][9],那个值也是0
result[i][j] = result[i-1][j-1] + result[i-1][j-2] + result[i-1][j-3] +
result[i-1][j-4] + result[i-1][j-5] + result[i-1][j-6];
}
}
double totle = pow(size, n);
for (int i = n; i<=size*n; i++) {
NSInteger time = result[n][i];
NSLog(@"和为%d的概率为%lf,次数为%ld",i,time/totle,(long)time);
}
}
即使n=20 , 计算也是在0.1s内出完所有结果, 时间复杂度在O(n^2)级别, 比起上面2种方式实在是太优秀了.
但是要说优化吗? 其实还有有余地的, 上面使用了一个[n+1]行[6*n+1]列的数组, 其实可以优化成2个[6*n+1]的数组, 2个数组循环相加得到最后的n个骰子.
但是我觉得在学习理解阶段还是用n+1行6*n+1列比较好理解, 可读性更好. 如果有机会在生产环境中使用, 那就把多维数组优化成2个数组.