Головоломка KenKen добавляет:REDUX A (исправленный) нерекурсивный алгоритм
-
21-08-2019 - |
Вопрос
Этот вопрос относится к тем частям головоломок KenKen Latin Square, в которых вас просят найти все возможные комбинации чисел ncells со значениями x такие, что 1 <= x <= maxval и x(1) + ...+ x(cells) = targetsum.Протестировав несколько наиболее многообещающих ответов, я собираюсь вручить приз за ответ Леннарту Регебро, потому что:
его программа такая же быстрая, как и моя (+-5%), и
он указал, что где-то в моей исходной программе была ошибка, что позволило мне понять, что она на самом деле пытается сделать.Спасибо, Леннарт.
Криспи представил алгоритм, который кажется эквивалентным алгоритму Леннарта, но через 5 часов, ооочень, первым он доходит до провода.
Замечание:Простейший рекурсивный алгоритм Алекса Мартелли представляет собой пример создания всех возможных комбинаций, бросания их всех в сито и наблюдения за тем, какие из них проходят через отверстия.Этот подход занимает в 20+ раз больше времени, чем у Леннарта или у меня.(Увеличьте входные данные до max_val = 100, n_cells = 5, target_sum = 250, и на моем компьютере это 18 секунд против 18 секунд.8+ мин.) Мораль:Не генерировать все возможные комбинации — это хорошо.
Еще одно замечание:Мы с Леннартом подпрограммы генерируем те же ответы в том же порядке.Действительно ли это один и тот же алгоритм, рассматриваемый под разными углами?Я не знаю.
Мне что-то приходит в голову.Если вы отсортируете ответы, начиная, скажем, с (8,8,2,1,1) и заканчивая (4,4,4,4,4) (то, что вы получите с max_val=8, n_cells=5, target_sum =20) серия образует своего рода «медленнейший спуск», причем первые «горячие», а последние «холодные» и максимально возможное число ступеней между ними.Связано ли это с «информационной энтропией»?Какова правильная метрика для рассмотрения?Существует ли алгоритм, который создает комбинации в порядке убывания (или возрастания) тепла?(Насколько я вижу, этого нет, хотя на коротких отрезках времени он близок к нормализованному стандартному значению.разработчик)
Вот процедура Python:
#!/usr/bin/env python
#filename: makeAddCombos.07.py -- stripped for StackOverflow
def initialize_combo( max_val, n_cells, target_sum):
"""returns combo
Starting from left, fills combo to max_val or an intermediate value from 1 up.
E.g.: Given max_val = 5, n_cells=4, target_sum = 11, creates [5,4,1,1].
"""
combo = []
#Put 1 in each cell.
combo += [1] * n_cells
need = target_sum - sum(combo)
#Fill as many cells as possible to max_val.
n_full_cells = need //(max_val - 1)
top_up = max_val - 1
for i in range( n_full_cells): combo[i] += top_up
need = target_sum - sum(combo)
# Then add the rest to next item.
if need > 0:
combo[n_full_cells] += need
return combo
#def initialize_combo()
def scrunch_left( combo):
"""returns (new_combo,done)
done Boolean; if True, ignore new_combo, all done;
if Falso, new_combo is valid.
Starts a new combo list. Scanning from right to left, looks for first
element at least 2 greater than right-end element.
If one is found, decrements it, then scrunches all available counts on its
right up against its right-hand side. Returns the modified combo.
If none found, (that is, either no step or single step of 1), process
done.
"""
new_combo = []
right_end = combo[-1]
length = len(combo)
c_range = range(length-1, -1, -1)
found_step_gt_1 = False
for index in c_range:
value = combo[index]
if (value - right_end) > 1:
found_step_gt_1 = True
break
if not found_step_gt_1:
return ( new_combo,True)
if index > 0:
new_combo += combo[:index]
ceil = combo[index] - 1
new_combo += [ceil]
new_combo += [1] * ((length - 1) - index)
need = sum(combo[index:]) - sum(new_combo[index:])
fill_height = ceil - 1
ndivf = need // fill_height
nmodf = need % fill_height
if ndivf > 0:
for j in range(index + 1, index + ndivf + 1):
new_combo[j] += fill_height
if nmodf > 0:
new_combo[index + ndivf + 1] += nmodf
return (new_combo, False)
#def scrunch_left()
def make_combos_n_cells_ge_two( combos, max_val, n_cells, target_sum):
"""
Build combos, list of tuples of 2 or more addends.
"""
combo = initialize_combo( max_val, n_cells, target_sum)
combos.append( tuple( combo))
while True:
(combo, done) = scrunch_left( combo)
if done:
break
else:
combos.append( tuple( combo))
return combos
#def make_combos_n_cells_ge_two()
if __name__ == '__main__':
combos = []
max_val = 8
n_cells = 5
target_sum = 20
if n_cells == 1: combos.append( (target_sum,))
else:
combos = make_combos_n_cells_ge_two( combos, max_val, n_cells, target_sum)
import pprint
pprint.pprint( combos)
Решение
Прежде всего, я бы использовал имена переменных, которые что-то значат, чтобы код был понятен.Затем, после того, как я понял проблему, это явно рекурсивная проблема, поскольку, как только вы выбрали одно число, вопрос поиска возможных значений для остальных квадратов представляет собой точно такую же проблему, но с другими значениями.
Поэтому я бы сделал это так:
from __future__ import division
from math import ceil
def make_combos(max_val,target_sum,n_cells):
combos = []
# The highest possible value of the next cell is whatever is
# largest of the max_val, or the target_sum minus the number
# of remaining cells (as you can't enter 0).
highest = min(max_val, target_sum - n_cells + 1)
# The lowest is the lowest number you can have that will add upp to
# target_sum if you multiply it with n_cells.
lowest = int(ceil(target_sum/n_cells))
for x in range(highest, lowest-1, -1):
if n_cells == 1: # This is the last cell, no more recursion.
combos.append((x,))
break
# Recurse to get the next cell:
# Set the max to x (or we'll get duplicates like
# (6,3,2,1) and (6,2,3,1), which is pointless.
# Reduce the target_sum with x to keep the sum correct.
# Reduce the number of cells with 1.
for combo in make_combos(x, target_sum-x, n_cells-1):
combos.append((x,)+combo)
return combos
if __name__ == '__main__':
import pprint
# And by using pprint the output gets easier to read
pprint.pprint(make_combos( 6,12,4))
Я также заметил, что ваше решение все еще кажется ошибочным.Для ценностей max_val=8, target_sum=20 and n_cells=5
ваш код не находит решения (8,6,4,1,1,)
, В качестве примера.Я не уверен, означает ли это, что я пропустил какое-то правило или нет, но, насколько я понимаю, правила должны быть допустимым вариантом.
Вот версия с использованием генераторов. Она экономит пару строк и память, если значения действительно большие, но в случае рекурсии генераторы может быть сложно «получить».
from __future__ import division
from math import ceil
def make_combos(max_val,target_sum,n_cells):
highest = min(max_val, target_sum - n_cells + 1)
lowest = int(ceil(target_sum/n_cells))
for x in xrange(highest, lowest-1, -1):
if n_cells == 1:
yield (x,)
break
for combo in make_combos(x, target_sum-x, n_cells-1):
yield (x,)+combo
if __name__ == '__main__':
import pprint
pprint.pprint(list(make_combos( 6,12,4)))
Другие советы
Ваш алгоритм на первый взгляд кажется довольно хорошим, и я не думаю, что ОО или другой язык улучшит код.Не могу сказать, помогла бы рекурсия, но я восхищаюсь нерекурсивным подходом.Могу поспорить, что с ним было труднее работать и его труднее читать, но, вероятно, он более эффективен и определенно довольно умный.Честно говоря, я не анализировал алгоритм подробно, но он определенно выглядит так, что для его правильной работы потребовалось много времени.Могу поспорить, что было много ошибок с отклонением на 1 и странных крайних случаев, которые вам пришлось продумать, а?
Учитывая все это, по сути, все, что я пытался сделать, — это как можно лучше улучшить ваш код, заменив многочисленные C-измы более идиоматичными Python-измами.Часто то, что требует цикла в C, можно выполнить в одной строке в Python.Также я попытался переименовать вещи, чтобы лучше соответствовать соглашениям об именах Python, и немного подчистил комментарии.Надеюсь, я не обижу вас своими изменениями.Вы можете взять то, что хотите, а остальное оставить.:-)
Вот записи, которые я делал во время работы:
- Изменен код, который инициализирует
tmp
к куче единиц к более идиоматичномуtmp = [1] * n_cells
. - Измененный
for
цикл, который суммируетtmp_sum
идиоматическийsum(tmp)
. - Затем заменили все петли на
tmp = <list> + <list>
один лайнер. - Взолнованный
raise doneException
кinit_tmp_new_ceiling
и избавился отsucceeded
флаг. - Регистрация
init_tmp_new_ceiling
на самом деле кажется ненужным.Удалив его, единственноеraise
левые были внутриmake_combos_n_cells
, поэтому я просто изменил их на обычные и удалилdoneException
полностью. - Нормализованное сочетание 4 пробелов и 8 пробелов для отступов.
- Удалены ненужные круглые скобки вокруг вашего
if
условия. tmp[p2] - tmp[p1] == 0
это то же самое, чтоtmp[p2] == tmp[p1]
.- Измененный
while True: if new_ceiling_flag: break
кwhile not new_ceiling_flag
. - Вам не нужно инициализировать переменные значением 0 в верхней части ваших функций.
- Удаленный
combos
list и изменил функцию наyield
его кортежи по мере их создания. - Переименован
tmp
кcombo
. - Переименован
new_ceiling_flag
кceiling_changed
.
И вот код для вашего прочтения:
def initial_combo(ceiling=5, target_sum=13, num_cells=4):
"""
Returns a list of possible addends, probably to be modified further.
Starts a new combo list, then, starting from left, fills items to ceiling
or intermediate between 1 and ceiling or just 1. E.g.:
Given ceiling = 5, target_sum = 13, num_cells = 4: creates [5,5,2,1].
"""
num_full_cells = (target_sum - num_cells) // (ceiling - 1)
combo = [ceiling] * num_full_cells \
+ [1] * (num_cells - num_full_cells)
if num_cells > num_full_cells:
combo[num_full_cells] += target_sum - sum(combo)
return combo
def all_combos(ceiling, target_sum, num_cells):
# p0 points at the rightmost item and moves left under some conditions
# p1 starts out at rightmost items and steps left
# p2 starts out immediately to the left of p1 and steps left as p1 does
# So, combo[p2] and combo[p1] always point at a pair of adjacent items.
# d combo[p2] - combo[p1]; immediate difference
# cd combo[p2] - combo[p0]; cumulative difference
# The ceiling decreases by 1 each iteration.
while True:
combo = initial_combo(ceiling, target_sum, num_cells)
yield tuple(combo)
ceiling_changed = False
# Generate all of the remaining combos with this ceiling.
while not ceiling_changed:
p2, p1, p0 = -2, -1, -1
while combo[p2] == combo[p1] and abs(p2) <= num_cells:
# 3,3,3,3
if abs(p2) == num_cells:
return
p2 -= 1
p1 -= 1
p0 -= 1
cd = 0
# slide_ptrs_left loop
while abs(p2) <= num_cells:
d = combo[p2] - combo[p1]
cd += d
# 5,5,3,3 or 5,5,4,3
if cd > 1:
if abs(p2) < num_cells:
# 5,5,3,3 --> 5,4,4,3
if d > 1:
combo[p2] -= 1
combo[p1] += 1
# d == 1; 5,5,4,3 --> 5,4,4,4
else:
combo[p2] -= 1
combo[p0] += 1
yield tuple(combo)
# abs(p2) == num_cells; 5,4,4,3
else:
ceiling -= 1
ceiling_changed = True
# Resume at make_combo_same_ceiling while
# and follow branch.
break
# 4,3,3,3 or 4,4,3,3
elif cd == 1:
if abs(p2) == num_cells:
return
p1 -= 1
p2 -= 1
if __name__ == '__main__':
print list(all_combos(ceiling=6, target_sum=12, num_cells=4))
Вот самое простое рекурсивное решение, которое я могу придумать, чтобы «найти все возможные комбинации n чисел со значениями x такие, что 1 <= x <= max_val и x(1) +...+ x(n) = цель».Я разрабатываю его с нуля.Вот версия вообще без какой-либо оптимизации, просто для простоты:
def apcnx(n, max_val, target, xsofar=(), sumsofar=0):
if n==0:
if sumsofar==target:
yield xsofar
return
if xsofar:
minx = xsofar[-1] - 1
else:
minx = 0
for x in xrange(minx, max_val):
for xposs in apcnx(n-1, max_val, target, xsofar + (x+1,), sumsofar+x+1):
yield xposs
for xs in apcnx(4, 6, 12):
print xs
Базовый случай n==0
(где мы не можем получить больше чисел) либо выдать кортеж, если он удовлетворяет условию, либо ничего, а затем завершить (возвратить).
Если мы должны создавать более длинные кортежи, чем мы создали до сих пор, if/else
гарантирует, что мы получаем только неубывающие кортежи, чтобы избежать повторения (вы сказали «комбинация», а не «перестановка»).
А for
пробует все возможности для «этого» элемента и перебирает все, что еще может дать следующий нижний уровень рекурсии.
Результат, который я вижу:
(1, 1, 4, 6)
(1, 1, 5, 5)
(1, 2, 3, 6)
(1, 2, 4, 5)
(1, 3, 3, 5)
(1, 3, 4, 4)
(2, 2, 2, 6)
(2, 2, 3, 5)
(2, 2, 4, 4)
(2, 3, 3, 4)
(3, 3, 3, 3)
что кажется правильным.
Существует множество возможных оптимизаций, но помните:
Сначала заставь это работать, затем сделай это быстро
Я переписывался с Кентом Беком, чтобы правильно атрибутировать эту цитату в «В двух словах о Python», и он рассказал мне, что получил ее от своего отца, чья работа на самом деле не была связана с программированием ;-).
В данном случае, мне кажется, ключевым вопросом является понимание что происходит, и любая оптимизация может помешать, поэтому я стараюсь "просто и понятно";мы можем, если понадобится!, оптимизировать носки, как только ОП подтвердит, что они может понять, что происходит в этой чистой, неоптимизированной версии!
К сожалению, ваш код довольно длинный и не особо читабельный.Если вы попытаетесь как-то обобщить это, возможно, кто-нибудь поможет вам написать это более четко.
Что касается самой проблемы, моей первой мыслью было бы использовать рекурсию.(Насколько я знаю, ты уже это делаешь.Еще раз извините за мою неспособность прочитать ваш код.) Подумайте, как можно свести проблему к меньшей, более простой версии одной и той же проблемы, неоднократно, пока не получите тривиальный случай с очень простым ответом.
Если быть более конкретным, у вас есть три параметра: max_val, target_sum и n_cells.Можете ли вы присвоить одному из этих чисел какое-то конкретное значение, чтобы решить чрезвычайно простую задачу, не требующую вообще никаких размышлений?Получив это, сможете ли вы свести более сложную версию проблемы к уже решенной?
РЕДАКТИРОВАТЬ:Вот мой код.Мне не нравится, как он выполняет дедупликацию.Я уверен, что есть более питонический способ.Кроме того, он запрещает использовать одно и то же число дважды в одной комбинации.Чтобы отменить это поведение, просто удалите строку if n not in numlist:
.Я не уверен, что это полностью правильно, но, похоже, это работает и (ИМХО) более читабельно.Вы можете легко добавить запоминание, и это, вероятно, немного ускорит работу.
def get_combos(max_val, target, n_cells):
if target <= 0:
return []
if n_cells is 1:
if target > max_val:
return []
else:
return [[target]]
else:
combos = []
for n in range(1, max_val+1, 1):
for numlist in get_combos(max_val, target-n, n_cells-1):
if n not in numlist:
combos.append(numlist + [n])
return combos
def deduplicate(combos):
for numlist in combos:
numlist.sort()
answer = [tuple(numlist) for numlist in combos]
return set(answer)
def kenken(max_val, target, n_cells):
return deduplicate(get_combos(max_val, target, n_cells))
Прежде всего, я сам изучаю Python, поэтому это решение не будет хорошим, но это всего лишь попытка решить эту проблему.Я пытался решить ее рекурсивно и думаю, что рекурсивное решение было бы идеальным для такого рода проблем, хотя ЭТО рекурсивное решение может быть не таким:
def GetFactors(maxVal, noOfCells, targetSum):
l = []
while(maxVal != 0):
remCells = noOfCells - 1
if(remCells > 2):
retList = GetFactors(maxVal, remCells, targetSum - maxVal)
#Append the returned List to the original List
#But first, add the maxVal to the start of every elem of returned list.
for i in retList:
i.insert(0, maxVal)
l.extend(retList)
else:
remTotal = targetSum - maxVal
for i in range(1, remTotal/2 + 1):
itemToInsert = remTotal - i;
if (i > maxVal or itemToInsert > maxVal):
continue
l.append([maxVal, i, remTotal - i])
maxVal -= 1
return l
if __name__ == "__main__":
l = GetFactors(5, 5, 15)
print l
Вот простое решение на C/С++:
const int max = 6;
int sol[N_CELLS];
void enum_solutions(int target, int n, int min) {
if (target == 0 && n == 0)
report_solution(); /* sol[0]..sol[N_CELLS-1] is a solution */
if (target <= 0 || n == 0) return; /* nothing further to explore */
sol[n - 1] = min; /* remember */
for (int i = min; i <= max; i++)
enum_solutions(target - i, n - 1, i);
}
enum_solutions(12, 4, 1);
Немного оффтоп, но все равно может помочь в программировании kenken.
Я получил хорошие результаты, используя алгоритм DLX для решения Killer Sudoku (очень похоже на KenKen, там есть клетки, но только суммы).Для большинства задач это заняло меньше секунды и было реализовано на языке MATLAB.
ссылка на этот форумhttp://www.setbb.com/phpbb/viewtopic.php?t=1274&highlight=&mforum=sudoku
Убийца Судоку "Посмотрите на Википедию, не могу опубликовать гиперсвязи" DAMT Spammers
Вот наивное, но лаконичное решение с использованием генераторов:
def descending(v):
"""Decide if a square contains values in descending order"""
return list(reversed(v)) == sorted(v)
def latinSquares(max_val, target_sum, n_cells):
"""Return all descending n_cells-dimensional squares,
no cell larger than max_val, sum equal to target_sum."""
possibilities = itertools.product(range(1,max_val+1),repeat=n_cells)
for square in possibilities:
if descending(square) and sum(square) == target_sum:
yield square
Я мог бы оптимизировать этот код, напрямую перечисляя список нисходящих сеток, но я считаю, что itertools.product гораздо удобнее для решения первого прохода.Наконец, вызов функции:
for m in latinSquares(6, 12, 4):
print m
А вот еще одно рекурсивное решение на основе генератора, но на этот раз с использованием простых математических вычислений для расчета диапазонов на каждом этапе, избегая ненужной рекурсии:
def latinSquares(max_val, target_sum, n_cells):
if n_cells == 1:
assert(max_val >= target_sum >= 1)
return ((target_sum,),)
else:
lower_bound = max(-(-target_sum / n_cells), 1)
upper_bound = min(max_val, target_sum - n_cells + 1)
assert(lower_bound <= upper_bound)
return ((v,) + w for v in xrange(upper_bound, lower_bound - 1, -1)
for w in latinSquares(v, target_sum - v, n_cells - 1))
Этот код завершится ошибкой AssertionError, если вы предоставите параметры, которые невозможно удовлетворить;это побочный эффект моего «критерия правильности», заключающийся в том, что мы никогда не делаем ненужной рекурсии.Если вы не хотите этого побочного эффекта, удалите утверждения.
Обратите внимание на использование -(-x/y) для округления после деления.Возможно, есть более питонический способ написать это.Обратите внимание, что я использую выражения генератора вместо урожайности.
for m in latinSquares(6,12,4):
print m