滚动骰子后得到特定总和的概率。红宝石

时间:2015-08-07 17:09:20

标签: ruby math probability standard-deviation

找到n个骰子的滚动概率的最佳解决方案是什么? 我通过找到

来解决它
  1. 的意思。
  2. 标准差。
  3. z_score代表x
  4. 以下的数字
  5. z_score代表x
  6. 以上的数字
  7. 将两者都转换为概率
  8. 从另一个中减去一个
  9. 这是我迄今为止所做的。

    # sides - number of sides on one die
    def get_mean(sides)
      (1..sides).inject(:+) / sides.to_f
    end
    
    def get_variance(sides)
      mean_of_squares = ((1..sides).inject {|sum, side| sum + side ** 2}) / sides.to_f
      square_mean = get_mean(sides) ** 2
    
      mean_of_squares - square_mean
    end
    
    def get_sigma(variance)
      variance ** 0.5
    end
    
    # x - the number of points in question
    def get_z_score(x, mean, sigma)
      (x - mean) / sigma.to_f
    end
    
    # Converts z_score to probability
    def z_to_probability(z)
      return 0 if z < -6.5
      return 1 if z > 6.5
    
      fact_k = 1
      sum = 0
      term = 1
      k = 0
    
      loop_stop = Math.exp(-23)
      while term.abs > loop_stop do
        term = 0.3989422804 * ((-1)**k) * (z**k) / (2*k+1) / (2**k) * (z**(k+1)) / fact_k
        sum += term
        k += 1
        fact_k *= k
      end
    
      sum += 0.5
      1 - sum
    end
    
    # Calculate probability of getting 'х' total points by rolling 'n' dice with 'sides' number of sides.
    def probability_of_sum(x, n, sides=6)
    
      mean = n * get_mean(sides)
      variance = get_variance(sides)
      sigma = get_sigma(n * variance)
    
      # Rolling below the sum
      z1 = get_z_score(x, mean, sigma)
      prob_1 = z_to_probability(z1)
    
      # Rolling above the sum
      z2 = get_z_score(x+1, mean, sigma)
      prob_2 = z_to_probability(z2)
    
      prob_1 - prob_2
    end
    
    # Run probability for 100 dice
    puts probability_of_sum(400, 100)
    

    我担心的是,当我选择x = 200时,概率为0。 这是正确的解决方案吗?

4 个答案:

答案 0 :(得分:6)

添加两个独立概率分布的结果与两个分布的convolving相同。如果分布是离散的,则它是离散卷积。

因此,如果单个骰子表示为:

probs_1d6 = Array.new(6) { Rational(1,6) }

然后可以像这样计算2d6:

probs_2d6 = []
probs_1d6.each_with_index do |prob_a,i_a|  
  probs_1d6.each_with_index do |prob_b,i_b| 
    probs_2d6[i_a + i_b] = ( probs_2d6[i_a + i_b] || 0 ) + prob_a * prob_b
  end
end

probs_2d6
# =>  [(1/36), (1/18), (1/12), (1/9), (5/36), (1/6), 
#      (5/36), (1/9), (1/12), (1/18), (1/36)]

尽管这对于骰子的两侧是n平方的,并且完全逻辑组合可以减少这种情况,但对于更复杂的设置而言这样做通常不太灵活。这种方法的好处是你可以继续添加更多的骰子,并做其他更奇特的组合。例如,要获得4d6,您可以将2d6的两个结果卷积。使用有理数可以让您解决浮点精度方面的问题。

我跳过了一个细节,你需要存储初始偏移量(正常六面模具的+1)并将它们加在一起,以便知道概率匹配的位置。

我在gem games_dice中以浮点而不是Rational制作了一个更复杂的逻辑版本,可以应对其他一些骰子组合。

这里是一个基本的重写方法,使用上述方法以一种天真的方式(简单地一次组合一个骰子的效果):

def probability_of_sum(x, n, sides=6)
  return 0 if x < n
  single_die_probs = Array.new(sides) { Rational(1,sides) }

  combined_probs = [1] # Represents chance of getting 0 when rolling 0 dice :-)

  # This is not the most efficient way to do this, but easier to understand
  n.times do
    start_probs = combined_probs
    combined_probs = []
    start_probs.each_with_index do |prob_a,i_a|  
        single_die_probs.each_with_index do |prob_b,i_b| 
          combined_probs[i_a + i_b] = ( combined_probs[i_a + i_b] || 0 ) + prob_a * prob_b
        end
    end
  end

  combined_probs[ x - n ] || 0
end

puts probability_of_sum(400, 100).to_f
# => 0.0003172139126369326

注意,该方法实际上计算了100到600的完整概率分布,因此您只需要调用一次并存储数组(加上偏移量+100)一次,然后您可以执行其他有用的操作,例如获取概率大于一定数量。由于在Ruby中使用Rational数字,所有这些都具有完美的精度。

因为在你的情况下,你只有一种类型的骰子,我们可以避免使用Rational直到结束,仅使用整数(基本上是组合值的计数),并除以组合的总数(卷数的力量)。这速度要快得多,并在一秒钟内返回100秒骰子的值:

def probability_of_sum(x, n, sides=6)
  return 0 if x < n
  combined_probs = [1] # Represents chance of getting 0 when rolling 0 dice :-)

  n.times do
    start_probs = combined_probs
    combined_probs = []
    start_probs.each_with_index do |prob_a,i_a|  
        sides.times do |i_b| 
          combined_probs[i_a + i_b] = ( combined_probs[i_a + i_b] || 0 ) + prob_a
        end
    end
  end

  Rational( combined_probs[ x - n ] || 0, sides ** n )
end

答案 1 :(得分:6)

存在涉及二项式系数的交替和的精确解。我已经在一些地方(QuoraMSE)写出来了,你可以在其他地方找到它,尽管有一些有缺陷的版本。请注意,如果实现了这一点,则可能需要取消比最终结果大得多的二项式系数,如果使用浮点运算,则可能会失去太多精度。

Neil Slater建议使用动态编程来计算卷积是一个很好的建议。它比二项式系数的总和慢,但相当稳健。您可以通过几种方式加快速度,例如通过平方使用取幂,以及使用快速傅立叶变换,但很多人会发现这些过度。

要修正你的方法,你应该对正常近似使用(简单)连续性校正,并限制你有足够骰子的上下文,并且你正在评估你预期正常近似的最大值和最小值。无论是绝对还是相对意义上都是好的。连续性校正是将n的计数替换为从n-1/2到n + 1/2的间隔。

总共200个滚动方式的确切数量是7745954278770349845682110174816333221135826585518841002760,所以概率是除以6 ^ 100,即约1.18563 x 10 ^ -20。

简单连续性校正的正态近似值是Phi((200.5-350)/ sqrt(3500/12)) - Phi((199.5-350)/ sqrt(3500/12))= 4.2 x 10 ^ -19 。这在绝对意义上是准确的,因为它非常接近0,但它相差35倍,因此相对而言并不是很好。正态近似给出了更接近中心的更好的相对近似。

答案 2 :(得分:1)

这是我的最终版本。

  1. 分别将get_z_score中的总和偏移更改为x-0.5x+0.5以获得更精确的结果。
  2. 添加return 0 if x < n || x > n * sides以涵盖案件,其中总和小于骰子数量,然后高于骰子数量乘以边数。
  3. 使用结果
  4. 添加了基准测试

    主要功能

    # sides - number of sides on one die
    def get_mean(sides)
      (1..sides).inject(:+) / sides.to_f
    end
    
    def get_variance(sides)
      mean_of_squares = ((1..sides).inject {|sum, side| sum + side ** 2}) / sides.to_f
      square_mean = get_mean(sides) ** 2
    
      mean_of_squares - square_mean
    end
    
    def get_sigma(variance)
      variance ** 0.5
    end
    
    # x - the number of points in question
    def get_z_score(x, mean, sigma)
      (x - mean) / sigma.to_f
    end
    
    # Converts z_score to probability
    def z_to_probability(z)
      return 0 if z < -6.5
      return 1 if z > 6.5
    
      fact_k = 1
      sum = 0
      term = 1
      k = 0
    
      loop_stop = Math.exp(-23)
      while term.abs > loop_stop do
        term = 0.3989422804 * ((-1)**k) * (z**k) / (2*k+1) / (2**k) * (z**(k+1)) / fact_k
        sum += term
        k += 1
        fact_k *= k
      end
    
      sum += 0.5
      1 - sum
    end
    
    # Calculate probability of getting 'х' total points by rolling 'n' dice with 'sides' number of sides.
    def probability_of_sum(x, n, sides=6)
      return 0 if x < n || x > n * sides
    
      mean = n * get_mean(sides)
      variance = get_variance(sides)
      sigma = get_sigma(n * variance)
    
      # Rolling below the sum
      z1 = get_z_score(x-0.5, mean, sigma)
      prob_1 = z_to_probability(z1)
    
      # Rolling above the sum
      z2 = get_z_score(x+0.5, mean, sigma)
      prob_2 = z_to_probability(z2)
    
      prob_1 - prob_2
    end
    

    <强>基准

    require 'benchmark'
    
    Benchmark.bm do |x|
      x.report { @prob = probability_of_sum(350, 100).to_f }
      puts "\tWith x = 350 and n = 100:"
      puts "\tProbability: #{@prob}"
    end
    puts
    
    Benchmark.bm do |x|
      x.report { @prob = probability_of_sum(400, 100).to_f }
      puts "\tWith x = 400 and n = 100:"
      puts "\tProbability: #{@prob}"
    end
    puts
    
    Benchmark.bm do |x|
      x.report { @prob = probability_of_sum(1000, 300).to_f }
      puts "\tWith x = 1000 and n = 300:"
      puts "\tProbability: #{@prob}"
    end
    

    <强>结果

           user     system      total        real
       0.000000   0.000000   0.000000 (  0.000049)
        With x = 350 and n = 100:
        Probability: 0.023356331366255034
    
           user     system      total        real
       0.000000   0.000000   0.000000 (  0.000049)
        With x = 400 and n = 100:
        Probability: 0.00032186531055478085
    
           user     system      total        real
       0.000000   0.000000   0.000000 (  0.000032)
        With x = 1000 and n = 300:
        Probability: 0.003232390001131513
    

答案 3 :(得分:0)

我也使用Monte Carlo方法解决了这个问题,结果相对接近。

# x - sum of points to find probability for
# n - number of dice
# trials - number of trials
def monte_carlo(x, n, trials=10000)
  pos = 0

  trials.times do
    sum = n.times.inject(0) { |sum| sum + rand(1..6) }
    pos += 1 if sum == x
  end

  pos / trials.to_f
end

puts monte_carlo(300, 100, 30000)