项目Euler 240:掷骰子的方式数量

时间:2012-12-12 09:45:15

标签: python puzzle itertools dice

我正在尝试解决Project Euler problem 240

  

有多少种方法可以滚动20个12面骰子(编号为1到12的边),使前十个总和达到70?

我想出了解决这个问题的代码。但是计算真的需要很多时间。我知道这种方法非常糟糕。有人可以建议我如何修复此代码以更好地执行?

import itertools
def check(a,b):   # check all the elements in a list a, are lesser than or equal to value b
    chk=0
    for x in a:
        if x<=b:
            chk=1
    return chk

lst=[]
count=0
for x in itertools.product(range(1,13),repeat=20):
    a=sorted([x[y] for y in range(20)])
    if sum(a[-10:])==70 and check(a[:10],min(a[-10:])):
        count+=1

下面的代码是针对problem说明中定义的问题。它完美无缺,并提供了精确的解决方案......

import itertools
def check(a,b):
     chk=1
     for x in a:
         if x>b:
             chk=0
             break
     return chk


count=0
for x in itertools.product(range(1,7),repeat=5):
    a=sorted([x[y] for y in range(5)])
    if sum(a[-3:])==15 and check(a[:2],min(a[-3:])):
        count+=1

2 个答案:

答案 0 :(得分:11)

迭代所有可能性都没有好处,因为有12个 20 = 3833759992447475122176方式可以滚动20个十二面骰子,比如每秒一百万卷,这需要数百万几年完成。

解决此类问题的方法是使用dynamic programming。找到一些方法将您的问题分解为几个较小问题的总和,并建立一个表格来解决这些子问题,直到您可以计算出所需的结果。

例如,让T( n d k t )为多种方式滚动 n d 骰子,使其顶部的 k 总和为 t 。我们如何将其分解为子问题?好吧,我们可以考虑完全滚动 d 的骰子数量 i 。有 n C i 方式来选择这些 i 骰子和T( n - i d - 1,...)方法选择 n - i 剩余的骰子必须最多滚动 d - 1.(对于 k t 的一些合适的参数选择我是已经过了。)

获取这些产品,并总结一下 i 的所有合适值,然后就完成了。 (好吧,还没完成:你必须指定基本案例,但这应该很容易。)

您需要计算的子问题的数量最多( n + 1)( d + 1)( k + 1)( t + 1),在Project Euler案例中( n = 20, d = 12, k = 10, t = 70)最多为213213.(实际上,它远不如此,因为树的许多分支很快到达基本情况:在我的实现中,结果证明只有791个子问题的答案足以计算答案。)

要编写动态程序,通常最简单的方式是递归表达它并使用memoization来避免重新计算子问题的答案。在Python中,您可以使用@functools.lru_cache decorator

所以程序的骨架看起来像这样。我已经用???替换了关键细节,以免剥夺你为自己解决问题的乐趣。在尝试更大的案例之前,使用小例子(例如“两个6面骰子,其中前1个总和为6”)来检查你的逻辑是否正确。

def combinations(n, k):
    """Return C(n, k), the number of combinations of k out of n."""
    c = 1
    k = min(k, n - k)
    for i in range(1, k + 1):
        c *= (n - k + i)
        c //= i
    return c

@lru_cache(maxsize=None)
def T(n, d, k, t):
    """Return the number of ways n distinguishable d-sided dice can be
    rolled so that the top k dice sum to t.

    """
    # Base cases
    if ???: return 1
    if ???: return 0

    # Divide and conquer. Let N be the maximum number of dice that
    # can roll exactly d.
    N = ???
    return sum(combinations(n, i)
               * T(n - i, d - 1, ???)
               for i in range(N + 1))

通过适当选择所有???,这可以在几毫秒内解决Project Euler问题:

>>> from timeit import timeit
>>> timeit(lambda:T(20, 12, 10, 70), number=1)
0.008017531014047563
>>> T.cache_info()
CacheInfo(hits=1844, misses=791, maxsize=None, currsize=791)

答案 1 :(得分:0)

此解决方案应该有效 - 不确定系统需要多长时间。

from itertools import product

lg = (p for p in product(xrange(1,13,1),repeat=10) if sum(p) == 70)

results = {}
for l in lg:
    results[l] = [p for p in product(xrange(1,min(l),1),repeat=10)]

它的作用是首先创造“前十名”。然后在每个“前十名”中添加一个可能的“下十个”项目列表,其中最大值上限为“前十名”中的最小项目

结果是一个字典,其中key是“前十名”,值是可能的“下一个十”的列表

解决方案(符合要求的组合数量)将计算所有结果字典中的列表数量,如下所示:

count = 0
for k, v in results.items():    
    count += len(v)

然后count将成为结果。

<强>更新

好吧,我想到了一个更好的方法。

from itertools import product
import math

def calc_ways(dice, sides, top, total):
    top_dice = (p for p in product(xrange(1,sides+1,1),repeat=top) if sum(p) == total)
    n_count = dict((n, math.pow(n, dice-top)) for n in xrange(1,sides+1,1))

    count = 0
    for l in top_dice:
        count += n_count[min(l)]

    return count

因为我只计算“下一个十”的长度,我想我只是预先计算“前十”中每个“最低”数字的选项数量,所以我创建了一个字典来做到这一点。上面的代码运行得更顺畅,因为它只包含一个小字典,一个计数器和一个生成器。你可以想象,它可能还需要很长时间......但我在1分钟内完成了前100万个结果。所以我确定它在可行的范围内。

祝你好运:)

更新2

经过你的另一次评论,我理解我做错了什么,并试图纠正它。

from itertools import product, combinations_with_replacement, permutations
import math

def calc_ways(dice, sides, top, total):
    top_dice = (p for p in product(xrange(1,sides+1,1),repeat=top) if sum(p) == total)
    n_dice = dice-top
    n_sets = len(set([p for p in permutations(range(n_dice)+['x']*top)]))
    n_count = dict((n, n_sets*len([p for p in combinations_with_replacement(range(1,n+1,1),n_dice)])) for n in xrange(1,sides+1,1))

    count = 0
    for l in top_dice:
        count += n_count[min(l)]

    return count

你可以想象这是一场灾难,甚至没有给出正确的答案。我想我会把这个留给数学家。因为我解决这个问题的方法就是:

def calc_ways1(dice, sides, top, total):
    return len([p for p in product(xrange(1,sides+1,1),repeat=dice) if sum(sorted(p)[-top:]) == total])

这是一个优雅的1行解决方案,为calc_ways1(5,6,3,15)提供了正确的答案,但却永远解决了calc_ways1(20,12,10,70)问题。

无论如何,数学似乎确实是这样做的方式,而不是我的愚蠢想法。