【问题标题】:Project Euler 240: number of ways to roll diceProject Euler 240:掷骰子的方法数
【发布时间】:2012-11-29 23:46:01
【问题描述】:

我正在尝试解决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

【问题讨论】:

  • 你应该重新思考这个问题。你有 20 个插槽,你需要用 1-12 的数字填充它们,这样 10 个数字最大的插槽加起来就是 70。我们已经可以减少这个了,你有 10 个插槽,1-12 = 70。然后对于每个解决方案,您还有 10 个插槽,可以安排它们的值都不大于解决方案插槽中的任何值。请记住,问题是有多少种方法可以做到这一点 - 所以每个解决方案 10 都有许多其他可以以多种不同方式排列的插槽。
  • Inbar,.. 最初我为类似谜题测试了这段代码,其中要求 5 个骰子,前三名的总和应该是 15,这样的组合有多少?这段代码立即给出了正确的 1111 解决方案。现在复杂性已增加到 20 个骰子和 12 个面.. 这需要很多时间.. 我需要一种替代方法或方法,它可以在没有蛮力的情况下解决。

标签: python puzzle itertools dice


【解决方案1】:

这个解决方案应该可以工作 - 不确定在您的系统上需要多长时间。

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)]

它所做的是首先创建“前十名”。然后为每个“前十”添加一个可能的“下十”项目列表,其中最大值被限制为“前十”中的最小项目

results 是一个字典,其中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) 问题需要很长时间。

无论如何,数学似乎是解决这个问题的方法,而不是我的愚蠢想法。

【讨论】:

  • 感谢 Inbar 的快速响应,但我尝试了这种分而治之的方法.. 仍然需要很长时间...
  • 你在哪里可以得到你系统的计数?
  • 我试过这个,它给出了错误的答案 (82) 用于problem description 中的较小示例。
  • Gareth... 你能试试我放在主要描述中的代码吗?它应该给你 1111. import itertools def check(a,b): chk=1 for x in a: if x>b: chk=0 break return chk lst=[] count=0 for x in itertools.product(range (1,6),repeat=5): a=sorted([x[y] for y in range(5)]) if sum(a[-3:])==70 and check(a[:2] ,min(a[-10:])): count+=1
  • @Inbar:您修改后的代码仍然给出了错误的答案 (82),即掷出 5 个 6 面骰子以使前 3 个总和为 15 的方式数。
【解决方案2】:

迭代所有可能性并不好,因为有 1220 = 3833759992447475122176 种方法来掷 20 个十二面骰子,例如,每秒掷一百万次,这将花费数百万年完成。

解决此类问题的方法是使用dynamic programming。想办法把你的问题分解成几个小问题的总和,并为这些子问题建立一个答案表,直到你可以计算出你需要的结果。

例如,设T(n, d, k, t) 为路数掷 n d 面的骰子,使它们的顶部 k 总和为 t。我们如何将其分解为子问题?好吧,我们可以考虑准确掷出 d 的骰子数 i。有 nCi 种方式来选择这些 i 骰子,而 T( n - i, d - 1, ...) 选择 n - i 的方式 剩余的骰子最多只能掷出 d - 1 个。(对于 kt 的一些合适的参数选择,我'已经省略了。)

取这些的乘积,并将其总结为所有合适的 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)

【讨论】:

  • @Gareth:我需要一点帮助才能通过递归解决它......我努力想出一种递归方式来找到数字组合,以解决上述问题,总和为 70 .我没有成功 enuf...
  • 你如何想出组合集,如果没有暴力方法,总和为 70...
  • 我刚刚写了一个完整的答案来解释这一点!
猜你喜欢
  • 1970-01-01
  • 2019-03-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-09-08
  • 2015-04-30
  • 2023-03-28
相关资源
最近更新 更多