求n个骰子的点数和

把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个数组.

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值