Project Euler 240: количество способов бросить кости

Я пытаюсь решить проблему Project Euler 240:

Сколькими способами можно бросить двадцать 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

Ниже приведен код для проблемы, определенной в описании проблемы. Он отлично работает и дает точное решение....

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

person deeshank    schedule 12.12.2012    source источник
comment
вы должны переосмыслить вопрос. у вас есть 20 слотов, вам нужно заполнить их числами от 1 до 12 так, чтобы 10 слотов с наибольшей суммой чисел составляли в сумме 70. уже мы можем уменьшить это, у вас есть 10 слотов, 1-12 = 70. и тогда для каждого решения у вас есть еще 10 слотов, которые можно расположить так, чтобы ни одно из их значений не превышало любое значение в слотах решения. помните, вопрос в том, сколькими способами это можно сделать - поэтому каждое решение 10 имеет много других слотов, которые можно расположить МНОЖЕСТВОМ различных способов.   -  person Inbar Rose    schedule 12.12.2012
comment
Inbar,.. Изначально я тестировал этот код для похожей головоломки, где в нем задавали 5 кубиков, а сумма тройки должна быть 15 и сколько таких комбинаций? этот код мгновенно выдал решение как 1111, что совершенно правильно. Теперь, когда сложность увеличилась до 20 игральных костей, а также 12 граней... это занимает так много времени... мне нужен альтернативный подход или способ решить эту проблему без грубой силы.   -  person deeshank    schedule 12.12.2012


Ответы (2)


Бесполезно перебирать все варианты, потому что существует 1220 = 3833759992447475122176 способов бросить 20 двенадцатигранных игральных костей, и, скажем, при миллионе бросков в секунду на это уйдут миллионы лет. .

Чтобы решить эту проблему, используйте динамическое программирование. Найдите какой-нибудь способ разделить вашу проблему на сумму нескольких более мелких задач и составьте таблицу ответов на эти подзадачи, пока не сможете вычислить нужный вам результат.

Например, пусть T(n, d, k, t) — количество способов бросить < em>n d-гранных игральных костей, так что сумма верхних k из них равна t. Как мы можем разделить это на подзадачи? Что ж, мы могли бы посчитать количество игральных костей, i, которые точно бросают d. Существует nCi способов выбрать эти i кубики, и T( n – i, d – 1, ...) способов выбора n – i оставшиеся кости, которые должны выбросить не более d − 1. (Для некоторого подходящего выбора параметров для k и t, которые я опустился.)

Возьмите их произведение и просуммируйте для всех подходящих значений i, и все готово. (Ну, не совсем готово: вам нужно указать базовые случаи, но это должно быть легко.)

Количество подзадач, которые вам нужно вычислить, будет не более (n + 1)(d + 1)(k + 1 )(t + 1), что в случае проекта Эйлера (n = 20, d = 12, k = 10, t = 70) не превышает 213213. (На практике гораздо меньше, потому что многие ветви дерева быстро достигают базовых случаев: в моей реализации получается, что ответы всего 791 подзадачи достаточно для вычисления ответа.)

Чтобы написать динамическую программу, обычно проще всего выразить ее рекурсивно и использовать запоминание, чтобы избежать повторного использования. вычисление ответа на подзадачи. В Python вы можете использовать @functools.lru_cache декоратор.

Таким образом, скелет вашей программы может выглядеть так. Я заменил важные детали на ???, чтобы не лишать вас удовольствия разобраться самостоятельно. Поработайте с небольшими примерами (например, «два шестигранных кубика, 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))

При соответствующем выборе для всех ??? это решает задачу проекта Эйлера за несколько миллисекунд:

>>> 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)
person Gareth Rees    schedule 12.12.2012
comment
@ Гарет: мне нужна небольшая помощь в решении этой проблемы с помощью рекурсии ... Я очень старался придумать рекурсивный способ найти комбинацию чисел, которая в сумме дает 70 для указанной проблемы. Я не был успешным enuf ... - person deeshank; 13.12.2012
comment
как вы придумаете набор комбинаций, который будет составлять 70 без метода грубой силы... - person deeshank; 13.12.2012
comment
Я только что написал целый ответ, объясняющий именно это! - person Gareth Rees; 13.12.2012

это решение должно работать - не уверен, сколько времени это займет в вашей системе.

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 минуту. поэтому я уверен, что это в допустимом диапазоне.

удачи :)

обновление 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])

которое представляет собой элегантное однострочное решение и дает правильный ответ для calc_ways1(5,6,3,15), но требует вечности для проблемы calc_ways1(20,12,10,70).

в любом случае, математика, похоже, подходит для этого, а не мои глупые идеи.

person Inbar Rose    schedule 12.12.2012
comment
Спасибо, Инбар, за быстрый ответ, но я пробовал этот подход "разделяй и властвуй".. Тем не менее, это занимает много времени... - person deeshank; 12.12.2012
comment
где вы можете получить счет в вашей системе? - person deeshank; 12.12.2012
comment
Я попробовал это, и это дало неправильный ответ (82) для меньшего примера в описании проблемы. - person Gareth Rees; 12.12.2012
comment
Гарет... можешь попробовать код, который я указал в основном описании. Это должно дать u 1111. (1,6),repeat=5): a=sorted([x[y] для y в диапазоне(5)]) if sum(a[-3:])==70 и check(a[:2] ,мин(а[-10:])): количество+=1 - person deeshank; 12.12.2012
comment
@Inbar: ваш пересмотренный код по-прежнему дает неправильный ответ (82) для количества способов бросить 5 6-гранных кубиков, чтобы сумма 3 верхних составила 15. - person Gareth Rees; 12.12.2012
comment
@GarethRees я превратил это в функцию, и когда я ввожу print calc_ways(5, 6, 3, 15), я получаю 148 - person Inbar Rose; 12.12.2012
comment
Извините: да, вы правы: ваш исправленный код дает в этом случае 148. Но правильный ответ 1111. - person Gareth Rees; 12.12.2012
comment
да, 148 частично верно.. тогда, когда вы переставите все эти 148 и удалите дубликаты через final_answer=list(set(answerset)) ==> len(final_answer)=1111 - person deeshank; 12.12.2012
comment
@Dshank, я не уверен, что вполне следую логике, не могли бы вы меня просветить? мы могли бы начать чат, чтобы облегчить. - person Inbar Rose; 12.12.2012
comment
конечно Инбар... нет проблем.. я сейчас в пути.. так что я доберусь до тебя к утру наверняка.. единственные случаи, которые ты мог пропустить, это... скажем, например... одна из комбинаций будет [6, 3,6,1,2], так как u вычислил начальный расчет для вычисления максимального значения (15).. другие комбинации [1,2,3,6,6] [2,3,6,1,6 ]...и так далее.. пропустил бы это в вычислении - person deeshank; 12.12.2012
comment
@Inbar: Ты понял :) блестяще :) - person deeshank; 12.12.2012