计算公平掷骰子的概率(在非指数时间内)

Posted

技术标签:

【中文标题】计算公平掷骰子的概率(在非指数时间内)【英文标题】:Calculate probability of a fair dice roll (in non-exponential time) 【发布时间】:2018-11-14 08:46:23 【问题描述】:

这方面的变化是很常见的问题,但我所有的 google-fu 都让我感到困惑。我想计算掷骰子公平的几率,但我想有效地做到这一点。有很多例子可以说明如何做到这一点,但我发现的所有算法在计算上都过于昂贵(指数时间),无法处理大量具有多面的骰子。

简单问题:计算在 x y 面骰子上掷出 n 的几率。

简单的解决方案:创建滚动的 n 元笛卡尔积,对每个积求和,计算和为目标的次数,做一点除法,瞧。

Go 中的简单解决方案示例: https://play.golang.org/p/KNUS4YBQC0g

简单的解决方案完美运行。我对其进行了扩展,以允许删除最高/最低 n 面等情况,结果经得起现场测试。

但是考虑Count: 20,Sides: 20,DropHighest: 0,DropLowest:0, Target: 200

如果我使用之前的解决方案进行评估,我的“表”将有 104 个奇怪的 septillion 单元,并且很容易使 CPU 最大化。

有没有更有效的方法来计算大量多面骰子的概率?如果是这样,它能否解释更复杂的“成功”条件选择,例如丢掉一些骰子?

我相信这是可能的,因为这个漂亮的网站的存在:https://anydice.com/program/969

编辑:

最适合我的解决方案是 David Eisenstat 的回答,我将其移植到:https://play.golang.org/p/cpD51opQf5h

【问题讨论】:

The dice roll sum problem 很好地解释了 N 个 6 面骰子的问题。如果您理解那篇文章,您应该能够将其推广到 n 面骰子。另请参阅第一篇文章引用的mathworld.wolfram.com/Dice.html @AaronSmall 独立骰子和的分布是每个骰子上的数字分布的卷积。在这种情况下,多项式乘法给出与卷积相同的结果,但卷积是更一般的概念(并且不再复杂)。见:en.wikipedia.org/wiki/Convolution @JimMischel 我不知道lucamoroni.it/the-dice-roll-sum-problem ,它似乎不必要地复杂。仅仅为了得到骰子的总和并不需要所有的组合。 @RobertDodier 这是对该主题的全面回顾,重点是理解其背后的数学原理。是的,如果你想要的只是一个公式,那就太复杂了。 @AaronSmall 你能帮我理解如何打算使用 DropHighest 和 DropLowest。您的意思是排除高于 DropHighest 和低于 DropLowest 的面孔吗?或者,在所有骰子中,无论它们显示的是什么,都排除了 DropHighest 最高的骰子,而 DropLowest 最低的骰子被排除在外? (如果是后者,如何处理关系?)感谢您提供任何信息。 【参考方案1】:

这里有一些代码可以处理低掷和高掷。很抱歉切换到 Python,但我需要简单的 bignums 和一个记忆库来保持理智。我认为复杂性类似于O(count^3 sides^2 drop_highest)

此代码的工作方式是将count 骰子每个与sides 面的可能性空间除以显示最大数量的骰子数量(count_showing_max)。有binomial(count, count_showing_max) 方法可以在唯一标记的骰子上实现这种滚动,因此multiplier。给定count_showing_max,我们可以计算出有多少骰子因为高而被丢弃,有多少骰子因为低而被丢弃(它确实发生了),并将这个总和添加到剩余骰子的结果中。

#!/usr/bin/env python3
import collections
import functools
import math


@functools.lru_cache(maxsize=None)
def binomial(n, k):
    return math.factorial(n) // (math.factorial(k) * math.factorial(n - k))


@functools.lru_cache(maxsize=None)
def outcomes(count, sides, drop_highest, drop_lowest):
    d = collections.Counter()
    if count == 0:
        d[0] = 1
    elif sides == 0:
        pass
    else:
        for count_showing_max in range(count + 1):  # 0..count
            d1 = outcomes(count - count_showing_max, sides - 1,
                          max(drop_highest - count_showing_max, 0),
                          drop_lowest)
            count_showing_max_not_dropped = max(
                min(count_showing_max - drop_highest,
                    count - drop_highest - drop_lowest), 0)
            sum_showing_max = count_showing_max_not_dropped * sides
            multiplier = binomial(count, count_showing_max)
            for k, v in d1.items():
                d[sum_showing_max + k] += multiplier * v
    return d


def main(*args):
    d = outcomes(*args)
    denominator = sum(d.values()) / 100
    for k, v in sorted(d.items()):
        print(k, v / denominator)


if __name__ == '__main__':
    main(5, 6, 2, 2)

【讨论】:

这太棒了,谢谢。我今天将深入挖掘并尝试移植。我想我必须建立自己的动态编程库...... 您的实现是一件美丽的事情。移植到 Go 真的让我想念 Python :P play.golang.org/p/cpD51opQf5h @david-eisenstat 我不得不说我对您的解决方案印象深刻。我断断续续地修补了这个问题一个月,最终想出了一个稍微不同的解决方案,这很好,但我看到你能够很快地整理出一个简短的通用解决方案。我向你致敬,继续努力。【参考方案2】:

您可以通过在变量Z 中乘以以下多项式来计算x y 面骰子之和的分布:

(Z + Z^2 + ... + Z^x)^y / x^y.

例如,对于两个六面骰子:

(Z + Z^2 + ... + Z^6)^2 / 6^2
  = (Z + Z^2 + ... + Z^6) * (Z + Z^2 + ... + Z^6) / 36
  = (Z^2 + 2Z^3 + 3Z^4 + 4Z^5 + 5Z^6 + 6Z^7 + 5Z^8 + 4Z^9 + 3Z^10 + 2Z^11 + Z^12) / 36,

所以你可以读出得到和6 的概率作为Z^6 (5/36) 的系数。

对于三个“双面”骰子:

(Z + Z^2)^3 / 2^3 = (Z + Z^2) * (Z + Z^2) * (Z + Z^2) / 8
                  = (Z^2 + 2Z^3 + Z^4) (Z + Z^2) / 8
                  = (Z^3 + 3Z^4 + 3Z^5 + Z^6) / 8,

所以得到和4的概率是Z^43/8)的系数。

你可以使用学校算法来得到这个问题的多项式算法。轻度测试的 Go 代码:

package main

import "fmt"

func dieRolls(x, y int) map[int]float64 
    d := map[int]float640: 1.0
    for i := 0; i < x; i++ 
        d1 := make(map[int]float64)
        for j := 1; j <= y; j++ 
            for k, v := range d 
                d1[k+j] += v / float64(y)
            
        
        d = d1
    
    return d


func main() 
    for k, v := range dieRolls(2, 6) 
        fmt.Printf("%d %g\n", k, v)
    
    fmt.Printf("\n")
    for k, v := range dieRolls(3, 2) 
        fmt.Printf("%d %g\n", k, v)
    

运行代码:https://play.golang.org/p/O9fsWy6RZKL

【讨论】:

这似乎是我所要求的大部分内容。我仍然对它的工作原理有点困惑,所以我积极地重命名变量以试图理解。我想,如果我将目标和最大值之间的概率相加,我将得到超过目标的概率,并且对于低于目标的情况,反之亦然。我仍然对如何更改分布以说明“最高跌幅”和“最低跌幅”感到困惑 @AaronSmall 我们正在计算 1 个 y 面骰子,然后是 2 个 y 面骰子,然后是 3 个等的概率分布。我们可以将滚动 x y 面骰子想象成滚动 y面骰子和代表 (x-1) 个 y 面骰子之和的怪异复合骰子。如果复合掷出(比如说)4,那么它是 1+3 还是 2+2 还是 3+1 都没有关系——这些可能性在最终核算中都是等价的。 这似乎完全正确,除了我想支持组合确实很重要的情况,以便我只能选择掷骰子的一个子集。一个常见的 RPG 场景是“掷 4d6-L”或“掷 4 个 6 面骰子,丢弃最低的面,将剩余的相加”。这是一个示例:anydice.com/program/1033e 在这种情况下,我不确定如何继续。 @AaronSmall 组合数学变得更加复杂。让我想想怎么写。

以上是关于计算公平掷骰子的概率(在非指数时间内)的主要内容,如果未能解决你的问题,请参考以下文章

大 n (>100) 的掷骰子数学

使用概率分布扩展整数的随机范围

上帝掷骰子吗? 计算机程序构造解释 奇思妙想-摘要

骰子游戏

09骰子游戏

掷骰子 dp