确定骰子投掷中数字出现的频率

6
对于一个游戏,我正在尝试确定在投掷给定数量的骰子时某个数字出现的频率。我知道...这个问题似乎很奇怪。让我用实际数字来解释一下。
因此,对于1个骰子,每个数字的频率都将相同。1-6将出现相同次数。
现在对于2个骰子,情况就不同了。我想象5、6、7将是最常出现的,而谱两端的数字将较少或根本不会出现(在1的情况下)。我想知道如何计算这个列表并按正确顺序显示它们,从最频繁到最不频繁。
有什么想法吗?
@duffymo - 有一个算法来得出这个列表将是非常好的。似乎上述方法需要大量手动选择和排列数字。如果我的骰子数量可以动态增加到10,那么手动完成这项工作将是低效且麻烦的。 :)

你在把数字加起来吗?为什么你得出结论说5、6和7会更多地出现? - JoshFinnie
1
@JoshFinnie - 可能是我错误地假设5、6、7更频繁,但我基于这样一个事实来做出这个假设:掷骰子得到5可能是2+3和4+1,而3只能与2+1一起出现;六是3+3、4+2、5+1等。 - bugfixr
9个回答

11

两个骰子有6*6=36种组合。

2 = 1+1只能出现一次,因此其频率为1/36。 3 = 1+2或2+1,所以其频率为2/36 = 1/18。 4 = 1+3,2+2或3+1,所以其频率为3/36 = 1/12。

您可以继续计算到十二。

任何打三细胞的玩家都非常了解这些。


5

1
不是钟形曲线?你确定吗?我的意思是,它的确像钟形,尽管我承认我的统计学不够强大,可能会被说服它不是“正态”分布,也许不是经典的“钟形曲线”。 - Beska
(顺便说一句,很好的链接……如果你能说服我它不是真正的钟形曲线,我会给它投赞成票。) - Beska
1
这是一个离散分布,因此它不能是真正的正态分布。然而,像大多数分布一样,它在渐近意义下趋向于正态分布 - 对于很多颗骰子,正态分布是更好的近似(且计算上更容易)。 - user57368
没问题...这正是我所想的,也是我在帖子中保留的方式:它近似于正态分布。这就是我对这个答案的问题所在...它暗示了它们是两个不同的东西,而不仅仅是将迭代次数延长到无穷大的问题。 - Beska
骰子的数量越大,逼近正态分布的程度就越高。因此,对于大量的骰子,由于组合数学中的阶乘,可以更快地使用正态分布。 - Cade Roux

5

递归方式的草稿如下:

public static IEnumerable<KeyValuePair<int, int>> GetFrequenciesByOutcome(int nDice, int nSides)
{
    int maxOutcome = (nDice * nSides);
    Dictionary<int, int> outcomeCounts = new Dictionary<int, int>();
    for(int i = 0; i <= maxOutcome; i++)
        outcomeCounts[i] = 0;

    foreach(int possibleOutcome in GetAllOutcomes(0, nDice, nSides))
        outcomeCounts[possibleOutcome] = outcomeCounts[possibleOutcome] + 1;

    return outcomeCounts.Where(kvp => kvp.Value > 0);
}

private static IEnumerable<int> GetAllOutcomes(int currentTotal, int nDice, int nSides)
{
    if (nDice == 0) yield return currentTotal;
    else
    {
        for (int i = 1; i <= nSides; i++)
            foreach(int outcome in GetAllOutcomes(currentTotal + i, nDice - 1, nSides))
                yield return outcome;
    }
}

除非我错了,否则这应该会输出按[key,频率]组织的KeyValuePairs。
编辑:FYI,在运行此代码后,它显示GetFrequenciesByOutcome(2,6)的频率如下:
2:1
3:2
4:3
5:4
6:5
7:6
8:5
9:4
10:3
11:2
12:1

我不知道为什么这个被踩了,因为没有人提供算法。 - mqp
#1 是一个计算,而不是一个算法。有人建议手动进行计算,然后硬编码表格(痛苦),但我宁愿有一个真正的算法来计算这些数字,而不是手动计算并硬编码它们。 - bugfixr
该算法非常简单,就像冒泡排序一样...并且具有类似的效率问题。这是迄今为止唯一介绍的算法,但对于大量数据来说,它将比所需的更加密集。最好只编写其背后的数学代码。(一些其他帖子中有数学链接)。 - Beska
没错,数学更好。这个算法只是对每一次可能的骰子投掷进行直接模拟,你可以想象,可能会有很多次骰子投掷。 - mqp

3
累加先前卷轴的频率数组,'面数'次移位,然后您将获得每个数字出现的频率数组。请保留 HTML 标记。
1, 1, 1, 1, 1, 1  # 6 sides, 1 roll

1, 1, 1, 1, 1, 1
   1, 1, 1, 1, 1, 1
      1, 1, 1, 1, 1, 1
         1, 1, 1, 1, 1, 1
            1, 1, 1, 1, 1, 1
+              1, 1, 1, 1, 1, 1
_______________________________
1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1  # 6 sides, 2 rolls

1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
   1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
      1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
         1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
            1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
+              1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1
______________________________________________
1, 3, 6,10,15,21,25,27,27,25,21,15,10, 6, 3, 1  # 6 sides, 3 rolls

这比蛮力模拟快得多,因为简单的方程是最好的。 以下是我的Python3实现。

def dice_frequency(sides:int, rolls:int) -> list:
    if rolls == 1:
        return [1]*sides
    prev = dice_frequency(sides, rolls-1)
    return [sum(prev[i-j] for j in range(sides) if 0 <= i-j < len(prev))
            for i in range(rolls*(sides-1)+1)]

例如,
dice_frequency(6,1) == [1, 1, 1, 1, 1, 1]
dice_frequency(6,2) == [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1]
dice_frequency(6,3) == [1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]

请注意,您应该使用“目标数字-掷骰次数”作为列表的索引来获取每个数字的频率。如果您想要得到概率,请使用“面数”^“掷骰次数”作为分母。
sides = 6
rolls = 3
freq = dice_frequency(sides,rolls)
freq_sum = sides**rolls
for target in range(rolls,rolls*sides+1):
    index = target-rolls
    if 0 <= index < len(freq):
        print("%2d : %2d, %f" % (target, freq[index], freq[index]/freq_sum))
    else:
        print("%2d : %2d, %f" % (target, 0, 0.0))

这段代码会产生

 3 :  1, 0.004630
 4 :  3, 0.013889
 5 :  6, 0.027778
 6 : 10, 0.046296
 7 : 15, 0.069444
 8 : 21, 0.097222
 9 : 25, 0.115741
10 : 27, 0.125000
11 : 27, 0.125000
12 : 25, 0.115741
13 : 21, 0.097222
14 : 15, 0.069444
15 : 10, 0.046296
16 :  6, 0.027778
17 :  3, 0.013889
18 :  1, 0.004630

2

1

使用动态函数创建的JavaScript实现:

<script>
var f;
function prob(dice, value)
 {
var f_s = 'f = function(dice, value) {var occur = 0; var a = [];';
for (x = 0; x < dice; x++)
 {
f_s += 'for (a[' + x + '] = 1; a[' + x + '] <= 6; a[' + x + ']++) {';
 }
f_s += 'if (eval(a.join(\'+\')) == value) {occur++;}';
for (x = 0; x < dice; x++)
 {
f_s += '}';
 }
f_s += 'return occur;}';
eval(f_s);
var occ = f(dice, value);
return [occ, occ + '/' + Math.pow(6, dice), occ / Math.pow(6, dice)];
 };

alert(prob(2, 12)); // 2 die, seeking 12
                    // returns array [1, 1/36, 0.027777777777777776]
</script>

编辑:相当失望没有人指出这一点;必须将6 * dice替换为Math.pow(6, dice)。不会再犯这样的错误了...


我很好奇你为什么选择了这个特定的感叹符号。:) - Hexagon Theory
因为这是一种比我通常倾向的编写代码方式更加愉快有趣的方式 :) - mqp
我也这样认为。最近才发现JavaScript中的动态函数创建/修改,很高兴找到了它的用途。 - Hexagon Theory

1

有趣的事实...

你知道帕斯卡三角形是N个2面骰子之和的概率分布吗?

   1 1    - 1 die, 1 chance at 1, 1 chance at 2
  1 2 1   - 2 dice, 1 chance at 2, 2 chances at 3, 1 chance at 4
 1 3 3 1  - 3 dice, 1 chance at 3, 3 chances at 4, 3 chances at 5, 1 chance at 6 
1 4 6 4 1 - etc.

这很不错!现在我在思考如何将这种迭代思维过程(如何在帕斯卡三角形中创建下一行)推广到M面骰子。 - mqp

0

似乎有一些神秘的事情围绕着这个问题,虽然duffymo已经解释了其中的一部分,但我看到另一篇帖子说:

没有理由认为5、6和7应该比2更多次地出现,因为掷骰子的第一次和第二次是独立事件,它们都有相等的1-6的概率。

这种观点确实很有吸引力。但是它是不正确的......因为第一次掷骰子会影响后面的概率。通过一个例子,我们可以最容易地说明这个推理过程。

假设我想知道在两个骰子上掷出2或7的概率哪个更高。如果我掷第一个骰子得到3,那么现在我掷出总数为7的概率是多少?显然是6分之1。而掷出总数为2的概率是多少呢?是0,因为我无论如何都无法在第二个骰子上掷出2来使我的总数为2。

由于无论我在第一颗骰子上掷出什么点数,只要在第二颗骰子上掷出正确的点数,就可以达到正确的总点数,因此数字7最有可能(也是最可能)被掷出。数字6和8同样略微不太可能,数字5和9更不太可能,直到我们到达数字2和12,它们的概率相等,每次为36分之1。
如果你绘制这个(总和 vs 概率),你会得到一个漂亮的钟形曲线(或者更精确地说,由于实验的离散性质,你会得到一个块状的近似曲线)。

0
在互联网和stackoverflow上搜索了很多后,我发现Dr. Math在一个工作函数中很好地解释了它(另一个答案中的链接有一个错误的公式)。我将Dr. Math的公式转换为C#并且我的nUnit测试(之前尝试代码失败)全部通过了。
首先,我必须编写一些辅助函数:
  public static int Factorial(this int x)
  {
     if (x < 0)
     {
        throw new ArgumentOutOfRangeException("Factorial is undefined on negative numbers");
     }
     return x <= 1 ? 1 : x * (x-1).Factorial();
  }

由于选择法在数学中的工作方式,我意识到如果我有一个带有下限的重载阶乘函数,就可以减少计算量。当达到下限时,该函数可以退出。
  public static int Factorial(this int x, int lower)
  {
     if (x < 0)
     {
        throw new ArgumentOutOfRangeException("Factorial is undefined on negative numbers");
     }
     if ((x <= 1) || (x <= lower))
     {
        return 1;
     }
     else
     {
        return x * (x - 1).Factorial(lower);
     }
  }

  public static int Choose(this int n, int r)
  {
     return (int)((double)(n.Factorial(Math.Max(n - r, r))) / ((Math.Min(n - r, r)).Factorial()));
  }

当这些都就位后,我就能够编写代码了。
  public static int WaysToGivenSum(int total, int numDice, int sides)
  {
     int ways = 0;
     int upper = (total - numDice) / sides; //we stop on the largest integer less than or equal to this
     for (int r = 0; r <= upper; r++)
     {
        int posNeg = Convert.ToInt32(Math.Pow(-1.0, r)); //even times through the loop are added while odd times through are subtracted
        int top = total - (r * sides) - 1;
        ways += (posNeg * numDice.Choose(r) * top.Choose(numDice - 1));
     }
     return ways;
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接