我是一名高中计算机科学专业的学生,今天我遇到了一个问题:
节目描述:在骰子玩家中有一种信仰 投掷三个骰子十分比一个九更容易获得。你能写吗 一个证明或反驳这种信念的计划?
让计算机计算三个骰子的所有可能方式 抛出: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,其中详细介绍了如何使用数学进行此操作。使用重复的衍生物或扩展来找到它是相当容易的(虽然很长),但对我来说编程要困难得多。我不太明白第二和第三个答案,因为我之前从未在数学研究中遇到过这种符号或那些概念。有人可以解释我如何编写一个程序来做这个,或者解释那个页面上给出的解决方案,以便我自己理解组合学?
编辑:我正在寻找一种解决这个问题的数学方法,它提供了一个精确的理论数,而不是通过模拟骰子
答案 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);
}
乍一看,这似乎是一个相当简单有效的解决方案。但是您会注意到,numDice
和sum
的相同值的许多计算可能会重复并重复计算,这使得此解决方案可能比原始的暴力方法效率更低。例如,在计算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
答案 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_1
和P_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^s
(a_s
)处的系数。 x^0
推导的自由系数(s'th
)为s! * a_k
。因此,s'th
中的s! * a_k
推导是s
。
所以,我们必须推导出这个函数{{1}}次。它可以使用推导规则来完成,但我认为它将比计数方法更复杂,因为每个推导产生“更复杂”的功能。以下是来自Wolfram Alpha的前三个推导:first,second和third。
一般来说,我更喜欢计算解决方案,而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}