我正在尝试解决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
答案 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)
问题。
无论如何,数学似乎确实是这样做的方式,而不是我的愚蠢想法。