计算滚动某个数字的方式数

时间:2013-11-01 01:27:40

标签: java algorithm probability combinatorics

我是一名高中计算机科学专业的学生,​​今天我遇到了一个问题:

  

节目描述:在骰子玩家中有一种信仰   投掷三个骰子十分比一个九更容易获得。你能写吗   一个证明或反驳这种信念的计划?

     

让计算机计算三个骰子的所有可能方式   抛出:1 + 1 + 1,1 + 1 + 2,1 + 1 + 3等。加上这些   可能性,并看看有多少人给出九个结果和多少   给十。如果更多给十,那么信念就会得到证实。

我很快就制定了一个强力解决方案

int sum,tens,nines;
    tens=nines=0;

    for(int i=1;i<=6;i++){
        for(int j=1;j<=6;j++){
            for(int k=1;k<=6;k++){
                sum=i+j+k;
                //Ternary operators are fun!
                tens+=((sum==10)?1:0);
                nines+=((sum==9)?1:0);
            }
        }

    }
    System.out.println("There are "+tens+" ways to roll a 10");
    System.out.println("There are "+nines+" ways to roll a 9");

哪种方法效果很好,老师要我们做的就是蛮力解决方案。但是,它没有扩展,我试图找到一种方法来制作一个算法,可以计算滚动 n 骰子以获得特定数字的方式。因此,我开始生成用 n 骰子获得每个总和的方法的数量。使用1个模具,每个模具显然有1个解决方案。然后我通过蛮力计算了2和3个骰子的组合。这些是两个:

  

有1种滚动方式2有2种滚动方式3   有3种方式可以滚动4个。有4种方式可以滚动5个   有5种方式可以滚动6个。有6种方式可以滚动7个   有5种方法可以滚动8个。有4种方式可以滚动9个   有3种方式可以滚动10个。有2种方式可以滚动11个   有一种方法可以滚动12个

看起来很简单;它可以用简单的线性绝对值函数计算。但事情变得越来越棘手。用3:

  

有1种方法可以滚动3个。有3种方法可以滚动4个   有6种方式可以滚动5个。有10种方式可以滚动6个   有15种方式可以滚动7个。有21种方式可以滚动8个   有25种方式可以滚动9个。有27种方式可以滚动10个   有27种方法可以滚动11个。有25种方式可以滚动12个   有21种方式可以滚动13个。有15种方式可以滚动14个   滚动15种方法有10种。有6种方式可以滚动16种   有3种方法可以滚动17个。有1种方法可以滚动18个

所以我看一下,我想:酷,三角数字!但是,我注意到那些讨厌的25和27。所以它显然不是三角形数字,但仍然是一些多项式展开,因为它是对称的。
所以我接受谷歌,我遇到了this page,其中详细介绍了如何使用数学进行此操作。使用重复的衍生物或扩展来找到它是相当容易的(虽然很长),但对我来说编程要困难得多。我不太明白第二和第三个答案,因为我之前从未在数学研究中遇到过这种符号或那些概念。有人可以解释我如何编写一个程序来做这个,或者解释那个页面上给出的解决方案,以便我自己理解组合学?

编辑:我正在寻找一种解决这个问题的数学方法,它提供了一个精确的理论数,而不是通过模拟骰子

4 个答案:

答案 0 :(得分:12)

使用带N(d, s)的生成函数方法的解决方案可能是最容易编程的。您可以使用递归来很好地建模问题:

public int numPossibilities(int numDice, int sum) {
    if (numDice == sum)
        return 1;
    else if (numDice == 0 || sum < numDice)
        return 0;
    else
        return numPossibilities(numDice, sum - 1) +
               numPossibilities(numDice - 1, sum - 1) -
               numPossibilities(numDice - 1, sum - 7);
}

乍一看,这似乎是一个相当简单有效的解决方案。但是您会注意到,numDicesum的相同值的许多计算可能会重复并重复计算,这使得此解决方案可能比原始的暴力方法效率更低。例如,在计算3骰子的所有计数时,我的程序运行numPossibilities函数总共15106次,而不是只执行6^3 = 216次的循环

要使此解决方案可行,您需要再添加一项技术 - 先前计算结果的memoization(缓存)。例如,使用HashMap对象,您可以存储已经计算过的组合,并在运行递归之前先参考这些组合。当我实现缓存时,numPossibilities函数仅运行151次总计来计算3骰子的结果。

随着您增加骰子的数量,效率提升会更大(结果基于我自己实施的解决方案的模拟):

# Dice | Brute Force Loop Count | Generating-Function Exec Count
3      | 216 (6^3)              | 151
4      | 1296 (6^4)             | 261
5      | 7776 (6^5)             | 401
6      | 46656 (6^6)            | 571
7      | 279936 (6^7)           | 771
...
20     | 3656158440062976       | 6101

答案 1 :(得分:2)

您不需要暴力,因为您的第一次滚动确定了第二次滚动中可以使用的值,并且第一次和第二次滚动确定第三次滚动。让我们看几十个例子,假设你掷出一个6,所以10-6=4意味着你仍然需要4。对于第二次滚动,您至少需要3,因为您的第三次滚动至少应该计入1。所以第二次滚动从1转到3。假设您的第二次点击是2,您有10-6-2=2,这意味着您的第三次点击是2,没有别的办法。

数十的伪代码:

tens = 0

for i = [1..6] // first roll can freely go from 1 to 6
   from_j = max(1, 10 - i - 6) // We have the first roll, best case is we roll a 6 in the third roll
   top_j = min(6, 10 - i - 1) // we need to sum 10, minus i, minus at least 1 for the third roll
   for j = [from_j..to_j]
      tens++

请注意,每个循环都会增加1,所以最后你会知道这段代码完全循环了27次。

这是所有18个值的Ruby代码(抱歉,它不是Java,但可以很容易地遵循)。请注意最小值和最大值,它们决定了每个骰子卷的值。

counts = [0] * 18

1.upto(18) do |n|
  from_i = [1, n - 6 - 6].max # best case is we roll 6 in 2nd and 3rd roll
  top_i = [6, n - 1 -1].min # at least 1 for 2nd and 3rd roll
  from_i.upto(top_i) do |i|
    from_j = [1, n - i - 6].max # again we have the 2nd roll defined, best case for 3rd roll is 6
    top_j = [6, n - i -1].min # at least 1 for 3rd roll
    from_j.upto(top_j) do
      # no need to calculate k since it's already defined being n - first roll - second roll
      counts[n-1] += 1
    end
  end
end

print counts

对于数学方法,请查看https://math.stackexchange.com/questions/4632/how-can-i-algorithmically-count-the-number-of-ways-n-m-sided-dice-can-add-up-t

答案 2 :(得分:2)

数学描述只是进行相同计数的“技巧”。它使用多项式来表示骰子,1*x^6 + 1*x^5 + 1*x^4 + 1*x^3 + 1*x^2 + 1*x表示每个值1-6被计数一次,并且它使用多项式乘法P_1*P_2来计算不同的组合。这样做是因为某个指数(k)的系数是通过将P_1P_2中的所有系数相加而得到的,其中指数总和为k

E.g。我们有两个骰子:

(1*x^6 + 1*x^5 + 1*x^4 + 1*x^3 + 1*x^2 + 1*x) * (x^6 + x^5 + x^4 + x^3 + x^2 + x) = 
(1*1)*x^12 + (1*1 + 1*1)*x^11 + (1*1 + 1*1 + 1*1)*x^11 + ... + (1*1 + 1*1)*x^3 + (1*1)*x^2

通过此方法计算与“计数”一样具有相同的复杂性。

由于函数(x^6 + x^5 + x^4 + x^3 + x^2 + x)^n具有更简单的表达式(x(x-1)^6/(x-1))^n,因此可以使用派生方法。 (x(x-1)^6/(x-1))^n是多项式,我们正在寻找x^sa_s)处的系数。 x^0推导的自由系数(s'th)为s! * a_k。因此,s'th中的s! * a_k推导是s

所以,我们必须推导出这个函数{{1}}次。它可以使用推导规则来完成,但我认为它将比计数方法更复杂,因为每个推导产生“更复杂”的功能。以下是来自Wolfram Alpha的前三个推导:firstsecondthird

一般来说,我更喜欢计算解决方案,而mellamokb给出了很好的方法和解释。

答案 3 :(得分:1)

查看Monte Carlo Methods它们通常使用输入值线性缩放。在这种情况下,示例很简单,我们假设因为一旦掷骰子不影响另一个而不是计数组合,我们可以简单地计算随机抛出的骰子面的总和(很多次)。

   public class MonteCarloDice {

    private Map<Integer, Integer> histogram;
    private Random rnd;
    private int nDice;
    private int n;

    public MonteCarloDice(int nDice, int simulations) {
        this.nDice = nDice;
        this.n = simulations;
        this.rnd = new Random();
        this.histogram = new HashMap<>(1000);
        start();
    }

    private void start() {
        for (int simulation = 0; simulation < n; simulation++) {
            int sum = 0;
            for (int i = 0; i < nDice; i++) {
                sum += rnd.nextInt(6) + 1;
            }
            if (histogram.get(sum) == null)
                histogram.put(sum, 0);
            histogram.put(sum, histogram.get(sum) + 1);
        }
        System.out.println(histogram);
    }


    public static void main(String[] args) {
        new MonteCarloDice(3, 100000);
        new MonteCarloDice(10, 1000000);
    }

}

错误随着模拟次数的增加而减少,但是以cputime为代价,但上述值非常快。

3个骰子

{3=498, 4=1392, 5=2702, 6=4549, 7=7041, 8=9844, 9=11583, 10=12310, 11=12469, 12=11594, 13=9697, 14=6999, 15=4677, 17=1395, 16=2790, 18=460}

10个骰子

{13=3, 14=13, 15=40, 17=192, 16=81, 19=769, 18=396, 21=2453, 20=1426, 23=6331, 22=4068, 25=13673, 24=9564, 27=25136, 26=19044, 29=40683, 28=32686, 31=56406, 30=48458, 34=71215, 35=72174, 32=62624, 33=68027, 38=63230, 39=56008, 36=71738, 37=68577, 42=32636, 43=25318, 40=48676, 41=40362, 46=9627, 47=6329, 44=19086, 45=13701, 51=772, 50=1383, 49=2416, 48=3996, 55=31, 54=86, 53=150, 52=406, 59=1, 57=2, 56=7}