El rompecabezas KenKen agrega:REDUX A (corregido) algoritmo no recursivo
-
21-08-2019 - |
Pregunta
Esta pregunta se relaciona con aquellas partes de los rompecabezas KenKen Latin Square que le piden que encuentre todas las combinaciones posibles de números ncells con valores x tales que 1 <= x <= maxval y x(1) +...+ x(ncells) = suma objetivo.Habiendo probado varias de las respuestas más prometedoras, le otorgaré el premio a Lennart Regebro porque:
su rutina es tan rápida como la mía (+-5%), y
Señaló que mi rutina original tenía un error en alguna parte, lo que me llevó a ver qué intentaba hacer realmente.Gracias, Lennart.
Chrispy contribuyó con un algoritmo que parece equivalente al de Lennart, pero 5 horas después, el primero en llegar al cable lo recibe.
Un comentario:El algoritmo recursivo básico de Alex Martelli es un ejemplo de cómo hacer todas las combinaciones posibles y tirarlas todas a un colador y ver cuáles pasan por los agujeros.Este enfoque lleva más de 20 veces más que el de Lennart o el mío.(Suba la entrada a max_val = 100, n_cells = 5, target_sum = 250 y en mi caja son 18 segundos vs.8+ minutos.) Moraleja:No generar todas las combinaciones posibles es bueno.
Otro comentario:Las rutinas de Lennart y las mías generan las mismas respuestas en el mismo orden.¿Son realmente el mismo algoritmo visto desde diferentes ángulos?No sé.
Se me ocurre algo.Si ordenas las respuestas, comenzando, digamos, con (8,8,2,1,1) y terminando con (4,4,4,4,4) (lo que obtienes con max_val=8, n_cells=5, target_sum =20), la serie forma una especie de "descenso más lento", siendo las primeras "calientes" y las últimas "frías" y el mayor número posible de etapas intermedias.¿Está esto relacionado con la "entropía informativa"?¿Cuál es la métrica adecuada para verlo?¿Existe un algoritmo que produzca combinaciones en orden descendente (o ascendente) de calor?(Este no lo hace, por lo que puedo ver, aunque está cerca en tramos cortos, mirando la norma normalizada.desarrollador)
Aquí está la rutina de 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)
Solución
En primer lugar, me gustaría utilizar nombres de variables que significan algo, por lo que el código obtiene comprensible. Entonces, después de que yo entendía el problema, está claro que es un problema recurrente, ya que una vez que haya elegido un número, la cuestión de la búsqueda de los posibles valores para el resto de las plazas son exactamente el mismo problema, pero con diferentes valores de.
Así que lo haría así:
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))
También noto que su solución aún parece buggy. Para los valores max_val=8, target_sum=20 and n_cells=5
su código no encuentra la solución (8,6,4,1,1,)
, como un ejemplo. No estoy seguro de si eso significa que he perdido una regla en esto o no, pero como entiendo las reglas que debería ser una opción válida.
Aquí hay una versión utilizando generadores, se ahorra un par de líneas, y la memoria si los valores son muy grandes, pero a medida que la recursividad, generadores puede ser difícil de "obtener".
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)))
Otros consejos
Su algoritmo parece bastante bueno a primera vista, y no creo que OO u otro lenguaje mejoren el código.No puedo decir si la recursividad hubiera ayudado, pero admiro el enfoque no recursivo.Apuesto a que fue más difícil ponerse a trabajar y es más difícil de leer, pero probablemente sea más eficiente y definitivamente bastante inteligente.Para ser honesto, no analicé el algoritmo en detalle, pero ciertamente parece algo que tardó mucho en funcionar correctamente.Apuesto a que hubo muchos errores de uno por uno y casos extremos extraños en los que tuviste que pensar, ¿eh?
Teniendo en cuenta todo eso, básicamente todo lo que intenté hacer fue embellecer su código lo mejor que pude reemplazando los numerosos C-ismos con Python-ismos más idiomáticos.Muchas veces lo que requiere un bucle en C se puede hacer en una línea en Python.También intenté cambiar el nombre de las cosas para seguir mejor las convenciones de nomenclatura de Python y limpié un poco los comentarios.Espero no ofenderte con ninguno de mis cambios.Puedes coger lo que quieras y dejar el resto.:-)
Aquí están las notas que tomé mientras trabajaba:
- Cambió el código que inicializa.
tmp
a un montón de 1 a los más idiomáticostmp = [1] * n_cells
. - Cambió
for
bucle que resumetmp_sum
a idiomáticosum(tmp)
. - Luego reemplazó todos los bucles con un
tmp = <list> + <list>
un trazador de líneas. - Movido
raise doneException
ainit_tmp_new_ceiling
y se deshizo delsucceeded
bandera. - el check-in
init_tmp_new_ceiling
En realidad parece innecesario.Quitándolo, lo únicoraise
Los que quedaban estaban enmake_combos_n_cells
, así que simplemente los cambié a devoluciones regulares y los eliminé.doneException
enteramente. - Mezcla normalizada de 4 espacios y 8 espacios para sangría.
- Se eliminaron paréntesis innecesarios alrededor de su
if
condiciones. tmp[p2] - tmp[p1] == 0
es lo mismo quetmp[p2] == tmp[p1]
.- Cambió
while True: if new_ceiling_flag: break
awhile not new_ceiling_flag
. - No necesita inicializar las variables a 0 en la parte superior de sus funciones.
- Remoto
combos
lista y función cambiada ayield
sus tuplas a medida que se generan. - Renombrado
tmp
acombo
. - Renombrado
new_ceiling_flag
aceiling_changed
.
Y aquí está el código para su lectura:
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))
Aquí está la solución recursiva más simple que puedo pensar para "encontrar todas las combinaciones posibles de n números con valores de x tal que 1 <= x <= MAX_VAL y X (1) + ... + x (n) = objetivo ". Estoy desarrollando desde cero. Aquí hay una versión sin ninguna optimización en absoluto, sólo por simplicidad:
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
El caso base n==0
(donde no podemos dió ningún más números) o bien dar la tupla hasta ahora si satisface la condición, o nada, entonces termina (devuelve).
Si se supone que debemos producir más que las tuplas que hemos construido hasta ahora, el if/else
se asegura de que sólo rendimos tuplas no decreciente, para evitar la repetición (que hizo decir "combinación" en lugar de "permutación") .
El for
intenta todas las posibilidades de "este" elemento y bucles sobre cualquiera que sea el siguiente nivel inferior hacia abajo de la recursividad es todavía capaz de producir.
La salida que veo es:
(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)
que parece correcta.
Hay bazillion posibles optimizaciones, pero, recuerde:
En primer lugar hacer que funcione, a continuación, que sea rápido
Me correspondió con Kent Beck atribuir correctamente esta cita en "Python en una cáscara de nuez", y él me dice que lo recibió de su padre, cuyo trabajo era en realidad no relacionado con la programación; -).
En este caso, me parece que la cuestión clave es comprensión lo que está pasando, y cualquier optimización podría interferir, así que estoy saliendo todos por "simple y comprensible"; podemos, si es necesario !, optimizar los calcetines de que una vez que el OP confirma que puede entender lo que está pasando en este pura, la versión sin optimizar!
Lo sentimos decir, su código es una especie de larga y no muy legible. Si se puede tratar de resumir de alguna manera, tal vez alguien puede ayudar a escribir con más claridad.
En cuanto al propio problema, lo primero que pensé sería el uso de la recursividad. (Por lo que sé, que ya está haciendo eso. Lo siento de nuevo por mi incapacidad para leer el código.) Piense en una forma que puede reducir el problema a una versión más pequeña más fácil del mismo problema, en varias ocasiones, hasta que haya una caso trivial con una respuesta muy simple.
Para ser un poco más concreto, tiene estos tres parámetros, MAX_VAL, target_sum y n_cells. Se puede configurar uno de esos números a algún valor en particular, con el fin de darle un problema extremadamente simple que requiere ningún pensamiento en absoluto? Una vez conseguido eso, se puede reducir la versión ligeramente más duro del problema a la ya resuelto uno?
EDIT: Aquí está mi código. No me gusta la forma en que lo hace de de-duplicación. Estoy seguro de que hay una manera más Pythonic. Además, no permite utilizando el mismo número dos veces en una combinación. Para deshacer este comportamiento, puedes sacar la línea if n not in numlist:
. No estoy seguro si esto es completamente correcto, pero parece que funciona y es (en mi humilde opinión) más legible. Desde aquí se puede añadir memoization y que probablemente acelerarlo un poco.
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))
En primer lugar, estoy aprendiendo Python a mí mismo lo que esta solución no va a ser grande, pero esto es sólo un intento de solucionar esto. He tratado de resolver de forma recursiva y creo que una solución recursiva sería ideal para este tipo de problema a pesar de que la solución recursiva podría no ser ésta:
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
Aquí una solución simple en C / 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);
Un poco offtopic, pero aún podría ayudar a kenken programación.
Me dieron buenos resultados usando DLX algorhitm para resolver Killer Sudoku (muy de parecida como KenKen tiene jaulas, pero sólo sumas). En menos de segundos para la mayoría de los problemas y se implementa en lenguaje MATLAB.
Referencia de este foro http://www.setbb.com/phpbb/viewtopic. php? t = 1274 y destacar = & mforum = sudoku
sudoku killer "Mirar en Wikipedia, no puede enlace posterior hiper" spammers DAMT
Este es un ingenuo, pero sucinta, solución utilizando generadores:
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
Podría haber optimizado el código directamente enumerando la lista de redes descendente, pero me parece mucho más claro itertools.product de una solución de primer paso. Por último, llamar a la función:
for m in latinSquares(6, 12, 4):
print m
Y aquí es otra solución recursiva, basado en el generador, pero esta vez usando un poco de matemática simple calcular rangos en cada paso, evitar la repetición innecesaria:
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))
Este código fallará con un AssertionError si proporciona parámetros que son imposibles de satisfacer; este es un efecto secundario de mi "criterio de corrección" que no hacemos una recursividad innecesaria. Si no desea que los efectos secundarios, eliminar las afirmaciones.
Observe el uso de - (- x / y) para redondear después de la división. Puede haber una manera más Pythonic escribir eso. Tenga en cuenta también estoy usando expresiones generadoras vez de rendimiento.
for m in latinSquares(6,12,4):
print m