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

Posted

技术标签:

【中文标题】大 n (>100) 的掷骰子数学【英文标题】:dice roll math with large n (>100) 【发布时间】:2020-08-12 19:29:42 【问题描述】:

我保证这不是只是另一个掷骰子作业问题。我实现了一个函数来计算在滚动nm 面骰子时获得小于和s 的概率。我的函数适用于n 的小值,但我发现n 的大值的结果很奇怪。见附图。有人知道发生了什么吗?

我的概率函数

从此math stack exchange实现

probability <- function(s, m, n) 

  i <- 0:((s-1-n) / m)
  m^(-n) * sum((-1)^i * choose(n, i) * choose(s - 1 - i * m, n))


从 ~ n > 80 开始中断

n <- 1:90 # number of dice
m <- 6 # number of sides
s <- floor(mean(1:m)*n) # sum of faces
p <- mapply(probability, s = s, m = m, n = n)
plot(n, p, main = paste("probability of rolling less than floor(", mean(1:m),"* n) as sum of n rolls of a", m, "sided dice"))

【问题讨论】:

雪上加霜,我的朋友已经在 Mathematica 中实现了相同的算法,并且对于较大的 n 值没有任何问题 那些choose 数字变得巨大。例如choose(80,40)。您的公式在数值上不稳定。也许尝试在对数尺度上进行计算。 对于大的nchoose 将完全失去精度。也许您可以阅读有关***.com/a/40527881/12158757 的替代方案 谢谢,我正在努力实现 ramanujan 的近似,但在矢量化函数并让它使用 base::choose 来处理较小的 nk 值时遇到了麻烦。我将编辑问题以包含我的进度。 修复了 NaN 问题,但精度仍然下降 ~ n > 80 【参考方案1】:

正如原始问题的 cmets 中所述,问题在于概率函数要求 R 计算非常大的数字 (choose(80,40) = 1.075072e+23),而我们正在达到 R 的数值精度限制。

另一种不涉及大量数字而是使用大量数字的替代方法是运行蒙特卡罗模拟。这会生成掷骰子和的分布,并将观察到的和与分布进行比较。运行时间会更长,但操作起来容易得多,并且不会出现数值精度问题。

mc <- Vectorize(function(s, m, n, reps = 10000) 
  x <- replicate(reps, sum(sample(m, n, replace = TRUE)))
  ecdf(x)(s-1)
)



n <- 1:90 # number of dice
m <- 6 # number of sides
s <- floor(mean(1:m)*n) # sum of faces
analytic_prob <- mapply(probability, s = s, m = m, n = n)
mc_prob <- mapply(mc, s = s, m = m, n = n)


plot(n, analytic_prob, main = paste("probability of rolling less than floor(", mean(1:m),"* n) as sum of n rolls of a", m, "sided dice"),
     sub = "monte carlo in red")
points(n, mc_prob, col = "red")

【讨论】:

【参考方案2】:

问题是由 R 的数值精度限制引起的。正如评论者所指出的,我在上面计算的 n 选择 k 值非常非常大(choose(80,40) = 1.075072e+23)。

我们可以使用日志来尝试将问题控制在 R 的计算范围内。这是Ramanujan方法的实现。不幸的是,近似复合中的误差和精度衰减得更快。概率函数需要对一个非常大的数字序列进行加减运算才能得到一个介于 0 和 1 之间的最终值,并且不能容忍任何不精确性。

0) 改写要分解为步骤的概率函数

probability <- function(s, m, n) 

  # Probability of getting less than s
  i <- 0:((s-1-n) / m)

  c1 <- choose(n, i)
  c2 <- choose(s - 1 - i * m, n)

  seq <- (-1)^i * (c1 * c2)

  m^(-n) * sum(seq)


1) 实现 log(x!) 的近似值

# using the 'ramanujan' method
ramanujan <- function(n)
  n * log(n) - n + log(n * (1 + 4*n * (1 + 2*n))) / 6 + log(pi) / 2


# confirm Ramanujan works correctly
n <- 1:200
diff <- log(factorial(n)) - ramanujan(n)
plot(n, diff) # r returns inf for factorial(171), but up to there the numbers match

2) 使用对数近似重写choose 函数。

#' This function returns log(choose(n,k)) 
log_nck <- Vectorize(function(n, k) 
  if(n <= k | n < 1 | k < 1) return(log(choose(n,k))) # logs don't like 0 or neg numbers

  return((ramanujan(n) - ramanujan(k) - ramanujan(n-k)))
)

# Check that choose function works
n <- seq(10, 100, 10)
k <- seq(5, 50, 5)
c_real <- log(choose(n, k))
c_approx <- log_nck(n, k)
# If we print them, they appear to match
print(c_real)
print(c_approx)
# and the difference shows pretty small errors. 
print(c_real - c_approx)

3) 使用 log choose 重写概率函数。

new_probability <- function(s, m, n) 

  # Probability of getting less than s
  i <- 0:((s-1-n) / m)

  c1 <- log_nck(n, i)
  c2 <- log_nck(s - 1 - i * m, n)

  seq <- (-1)^i * exp(c1 + c2)

  return(m^(-n) * sum(seq))


最终测试

n <- 1:90 # number of dice
m <- 6 # number of sides
s <- floor(mean(1:m)*n) # sum of faces

p <- mapply(probability, s = s, m = m, n = n)
newp <- mapply(new_probability, s = s, m = m, n = n)

plot(n, p, main = "Original in black, approximation in red")
points(n, newp, col = "red")

【讨论】:

以上是关于大 n (>100) 的掷骰子数学的主要内容,如果未能解决你的问题,请参考以下文章

Qt系列文章之二十九(基于QThread的掷骰子线程实例讲解)

Qt系列文章之二十九(基于QThread的掷骰子线程实例讲解)

如何为 N 骰子生成“先行”骰子?

python掷骰子游戏

数学与物理故事相关链接

基础数学问题