准确地模拟许多没有循环的骰子卷?


14

好的,所以如果您的游戏掷出大量骰子,您可以循环调用一个随机数生成器。但是对于任何一组频繁滚动的骰子,您都会获得分布曲线/直方图。所以我的问题是我可以运行一个简单的简单计算来得出适合该分布的数字吗?

例如2D6-得分-概率百分比

2-2.77%

3-5.55%

4-8.33%

5-11.11%

6-13.88%

7-16.66%

8-13.88%

9-11.11%

10-8.33%

11-5.55%

12-2.77%

因此,了解上述内容后,您可以滚动一个d100并计算出准确的2D6值。但是一旦我们从10D6、50D6、100D6、1000D6开始,这可以节省很多处理时间。因此,必须有一个可以快速完成此任务的教程/方法/算法?对于股票市场,赌场,战略游戏,矮人堡垒等来说,它可能很方便。如果您可以模拟完整的战略战斗的结果,而这需要花费几个小时才能打通,但只需几次调用此功能和一些基本数学运算,该怎么办?


5
即使在1000 d6时,循环在现代PC上也足够快,您不太可能会注意到它,因此这可能是过早的优化。在用不透明的公式替换一个清晰的循环之前,请始终尝试进行概要分析。也就是说,有一些算法选择。您是否对示例中的骰子这样的离散概率感兴趣,或者将它们建模为连续概率分布是否可以接受(因此可能会得到2.5的分数结果)?
DMGregory

DMGregory是正确的,计算1000d6不会占用太多的处理器。但是,有一种叫做二项分布的东西(通过一些巧妙的工作)将获得您感兴趣的结果。此外,如果您想查找任意滚动规则集的概率,请尝试使用语言适中的TRoll设置掷骰子的方式,它将计算每个可能结果的所有概率。
Draco18s不再信任SE 2015年

使用泊松分布:p。
Luis Masuelli 2015年

1
对于任何一组频繁滚动的骰子,您可能都会获得分布曲线/直方图。这是一个重要的区别。骰子可以连续滚动100万个6s,这不太可能,但是它可以
Richard Tingle 2015年

@RichardTingle您能详细说明吗?分布曲线/直方图还将包含“连续6百万个6s”的情况。
2015年

Answers:


16

正如我在上面的评论中提到的那样,建议您在过度复杂化代码之前先对此进行简介。快速for循环求和骰子比复杂的数学公式和表构建/搜索容易得多,而且易于修改。始终先进行概要分析,以确保您正在解决重要问题。;)

就是说,有两种主要方法可以一口气采集复杂的概率分布:


1.累积概率分布

一个巧妙的技巧,可以仅使用一个统一的随机输入从连续概率分布中进行采样。它与累积分布有关,该函数回答“获得不大于 x 的值的概率是多少?”

此功能是不变的,从0开始,在其范围内上升到1。下面显示了两个六边形骰子的总和的示例:

2d6的概率图,累积分布图和逆图

如果您的累积分布函数具有方便计算的逆(或者您可以使用分段函数(如贝塞尔曲线)对其进行近似),则可以使用此函数从原始概率函数中进行采样。

逆函数处理将0和1之间的域分割为映射到原始随机过程的每个输出的间隔,每个区域的捕获区域与其原始概率相匹配。(对于连续分布,这是无限无限的。对于骰子掷骰子等离散分布,我们需要仔细舍入)

这是使用它来模拟2d6的示例:

int SimRoll2d6()
{
    // Get a random input in the half-open interval [0, 1).
    float t = Random.Range(0f, 1f);
    float v;

    // Piecewise inverse calculated by hand. ;)
    if(t <= 0.5f)
    {
         v = (1f + sqrt(1f + 288f * t)) * 0.5f;
    }
    else
    {
         v = (25f - sqrt(289f - 288f * t)) * 0.5f;
    }

    return floor(v + 1);
}

比较一下:

int NaiveRollNd6(int n)
{
    int sum = 0;
    for(int i = 0; i < n; i++)
       sum += Random.Range(1, 7); // I'm used to Range never returning its max
    return sum;
}

明白我在代码清晰度和灵活性方面的不同吗?幼稚的方法可能具有循环的幼稚性,但它简短而简单,其作用显而易见,并且易于缩放至不同的裸片尺寸和数量。更改累积分布代码需要一些不平凡的数学运算,并且很容易破坏并导致意外结果,而不会出现任何明显的错误。(我希望以上没有做过)

因此,在消除明确的循环之前,请绝对确定这确实是一个性能问题,值得为此牺牲。


2.别名方法

当您可以将累积分布函数的反函数表达为简单的数学表达式时,累积分布方法会很好地工作,但这并不总是那么容易甚至不可能。离散分布的可靠替代方法是Alias方法

这样,您仅使用两个独立的,均匀分布的随机输入即可从任意离散的概率分布中进行采样。

它的工作原理是采用类似于左下方的分布(不用担心面积/权重之和不等于1,因为Alias方法我们关心相对权重)并将其转换为表格,例如正确的位置:

  • 每个结果都有一栏。
  • 每列最多分为两个部分,每个部分与原始结果之一相关。
  • 保留每个结果的相对面积/权重。

将分配转换为查找表的别名方法的示例

(图基于这篇关于采样方法的优秀文章的图片)

在代码中,我们用两个表(或具有两个属性的对象表)表示这一点,该表表示从每一列中选择替代结果的可能性,以及该替代结果的标识(或“别名”)。然后我们可以像这样从分布中采样:

int SampleFromTables(float[] probabiltyTable, int[] aliasTable)
{
    int column = Random.Range(0, probabilityTable.Length);
    float p = Random.Range(0f, 1f);
    if(p < probabilityTable[column])
    {
        return column;
    }
    else
    {
        return aliasTable[column];
    }
}

这涉及一些设置:

  1. 计算每个可能结果的相对概率(因此,如果您滚动1000d6,我们需要计算将每个总和从1000转换为6000的方法的数量)

  2. 建立一对表格,每个结果都有一个条目。完整的方法超出了此答案的范围,因此,我强烈建议参考对“别名方法”算法的解释

  3. 存储这些表,并在每次需要通过此发行版进行新的随机模版印刷时参考它们。

这是时空的权衡。预计算步骤有些穷举,我们需要根据结果的数量来分配内存(尽管即使是1000d6,我们也要讲个位数的千字节,所以没有什么可以失去睡眠的),但是作为交换我们的采样无论我们的分布多么复杂,它都是固定时间的。


我希望这些方法中的一种可能有用(或者我已经说服您,朴素的方法的简单性值得花时间循环);)


1
很棒的答案。我喜欢天真的方法。错误的空间要少得多,而且易于理解。
bummzack 2015年

仅供参考,这个问题是关于reddit的随机问题的复制粘贴。
Vaillancourt

出于完整性考虑,我认为这是 @AlexandreVaillancourt在谈论的reddit线程。那里的答案主要建议保留循环版本(有证据表明其时间成本可能是合理的)或使用正态/高斯分布近似大量骰子。
DMGregory

+1是别名方法,似乎很少有人知道它,它的确是许多这类概率选择情况的理想解决方案,而+1是提及高斯解决方案的方法,这可能是“更好的”方法如果我们只关心性能和空间节省,那就回答。
WHN

0

不幸的是,答案是这种方法不会导致性能提高。

我相信在如何生成随机数的问题上可能会有一些误解。以下面的[Java]示例为例:

Random r = new Random();
int n = 20;
int min = 1; //arbitrary
int max = 6; //arbitrary
for(int i = 0; i < n; i++){
    int randomNumber = (r.nextInt(max - min + 1) + min)); //silly maths
    System.out.println("Here's a random number: " + randomNumber);
}

此代码将循环20次,打印出1到6(含)之间的随机数。当我们谈论这段代码的性能时,需要花费一些时间来创建Random对象(这涉及到在创建时基于计算机的内部时钟来创建伪随机整数数组),然后是20个恒定时间在每个nextInt()调用上进行查找。由于每个“滚动”都是恒定的时间操作,因此在时间上滚动非常便宜。还要注意,最小到最大的范围无关紧要(换句话说,计算机滚动d6就像滚动d10000一样容易)。说到时间复杂度,解决方案的性能只是O(n),其中n是骰子数。

或者,我们可以用一个d100(或d10000)来近似任意数量的d6辊。使用此方法,我们首先需要计算s [骰子的面数] * n [骰子数]百分比(技术上为s * n-n + 1百分比,我们应该能够将其大致除以因为它是对称的,所以只有一半;请注意,在模拟2d6掷骰的示例中,您计算​​了11个百分比,而6个是唯一的)。滚动之后,我们可以使用二进制搜索来找出滚动所属于的范围。就时间复杂度而言,此解决方案的评估结果为O(s * n)解,其中s是边数,而n是骰子数。我们可以看到,这比上一段中提出的O(n)解决方案要慢。

从那里推断,假设您创建了这两个程序来模拟1000d20滚动。第一个只会滚动1,000次。第二个程序首先需要确定19,001个百分比(对于1,000到20,000的可能范围),然后再执行其他操作。因此,除非您使用的是一个奇怪的系统,在该系统上内存查找比浮点操作要贵上几倍,否则对每一卷使用nextInt()调用似乎是一种方法。


2
上面的分析不是很正确。如果我们根据Alias方法预先预留一些时间来生成概率表和别名表,则可以在恒定时间内从任意离散概率分布中采样(2个随机数和一个表查找)。因此,一旦准备好桌子,模拟5个骰子或500个骰子的卷就需要进行同样的工作。这比在每个样本上循环大量骰子要快,尽管这不一定使它成为解决问题的更好方法。;)
DMGregory

0

如果您要存储骰子组合,那么好消息是有解决方案,坏的是我们的计算机在某种程度上限制了此类问题。

好消息:

有一个确定性的方法可以解决此问题:

1 /计算骰子组的所有组合

2 /确定每种组合的概率

3 /在此列表中搜索结果,而不是掷骰子

坏消息:

重复的结合数由下式给出

Γñķ=ñ+ķ-1ķ=ñ+ķ-1ķ ñ-1

来自法国维基百科):

与重复结合

这意味着,例如,有150个骰子,您有698'526'906个组合。假设您将概率存储为32位浮点数,将需要2.6 GB的内存,并且仍然需要为索引添加内存要求...

在计算方面,可以通过卷积来计算组合数,这很方便,但不能解决内存限制。

总之,对于大量骰子,我建议您扔骰子并观察结果,而不是预先计算与每种组合相关的概率。

编辑

但是,由于您只对骰子的总和感兴趣,因此可以用更少的资源存储概率。

您可以使用卷积计算每个骰子总和的精确概率。

F一世=ñF1ñF一世-1-ñ

然后从1/6形式的每个结果(含1个骰子)开始,您可以构造任意数目个骰子的所有正确概率。

这是我编写的用于说明的粗略Java代码(尚未真正优化):

public class DiceProba {

private float[][] probas;
private int currentCalc;

public int getCurrentCalc() {
    return currentCalc;
}

public float[][] getProbas() {
    return probas;
}

public void calcProb(int faces, int diceNr) {

    if (diceNr < 0) {
        currentCalc = 0;
        return;
    }

    // Initialize
    float baseProba = 1.0f / ((float) faces);
    probas = new float[diceNr][];
    probas[0] = new float[faces + 1];
    probas[0][0] = 0.0f;
    for (int i = 1; i <= faces; ++i)
        probas[0][i] = baseProba;

    for (int i = 1; i < diceNr; ++i) {

        int maxValue = (i + 1) * faces + 1;
        probas[i] = new float[maxValue];

        for (int j = 0; j < maxValue; ++j) {

            probas[i][j] = 0;
            for (int k = 0; k <= j; ++k) {
                probas[i][j] += probability(faces, k, 0) * probability(faces, j - k, i - 1);
            }

        }

    }

    currentCalc = diceNr;

}

private float probability(int faces, int number, int diceNr) {

    if (number < 0 || number > ((diceNr + 1) * faces))
        return 0.0f;

    return probas[diceNr][number];

}

}

使用所需的参数调用calcProb(),然后访问proba表以获取结果(第一个索引:1个骰子为0,两个骰子为1 ...)。

我在笔记本电脑上用1'000D6进行了检查,花了10秒钟来计算从1到1000的所有概率和所有可能的骰子总和。

借助预计算和有效的存储,您可以快速获得大量骰子的答案。

希望能帮助到你。


3
由于OP仅在寻找骰子总和的值,因此该组合数学不适用,并且概率表条目的数量随骰子的大小和骰子的数量线性增长。
DMGregory

你是对的 !我已经编辑了答案。我们总是很聪明;)
elenfoiro78

我认为您可以使用分治法来稍微提高效率。我们可以通过将10d6的表与自身卷积来计算20d6的概率表。通过将5d6表与自身进行卷积可以找到10d6。通过将2d6和3d6表进行卷积可以找到5d6。通过这种方式减半进行操作,我们可以跳过从1-20生成大多数表大小的工作,而将精力集中在有趣的表上。
DMGregory

1
并使用对称!
elenfoiro78 '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.