KenKen addendi di puzzle: Redux A (corretto) algoritmo non ricorsivo
-
21-08-2019 - |
Domanda
Questa domanda si riferisce a quelle parti del puzzle KenKen quadrato latino, che chiederà di trovare tutte le possibili combinazioni di numeri ncells con valori x tale che 1 <= x <= MAXVAL e x (1) + ... + x ( ncells) = targetsum. Dopo aver testato molte delle risposte più promettenti, ho intenzione di assegnare la risposta-premio a Lennart Regebro, perché:
-
la sua routine è veloce come la mia (+ -5%), e
-
Egli ha sottolineato che la mia routine originale aveva un bug da qualche parte, che mi ha portato a vedere che cosa stava realmente cercando di fare. Grazie, Lennart.
chrispy contribuito un algoritmo che sembra equivalente a Lennart di, ma 5 ore più tardi, sooo, in primo luogo al filo ottiene.
Una nota: bare-bones algoritmo ricorsivo di Alex Martelli è un esempio di fare ogni possibile combinazione e li getta in un setaccio e vedere che passare attraverso i fori. Questo approccio prende 20+ volte più lungo di Lennart del o la mia. (Jack fino all'ingresso a max_val = 100, n_cells = 5, target_sum = 250 e sulla mia scatola è di 18 secondi contro 8+ min.) Morale:. Non genera ogni possibile combinazione è buona
Un'altra osservazione: Lennart e di mia routine generano le stesse risposte nello stesso ordine . Sono in realtà lo stesso algoritmo visto da diverse angolazioni? Non lo so.
Qualcosa mi viene in mente. Se si ordinano le risposte, a partire, per esempio, con (8,8,2,1,1) e termina con (4,4,4,4,4) (quello che si ottiene con max_val = 8, n_cells = 5, target_sum = 20), la serie costituisce una sorta di "lento discesa", con i primi essere "caldo" e l'ultimo è "freddo" e il maggior numero possibile di fasi intermedie. È questo correlate a "entropia informazionale"? Qual è la metrica appropriata per guardarlo? Esiste un algoritmo che producs le combinazioni in ordine decrescente (o ascendente) ordine di calore? (Questo non lo fa, per quanto posso vedere, anche se è vicino su brevi tratti, guardando std normalizzata. Dev.)
Ecco la routine 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)
Soluzione
Prima di tutto, mi piacerebbe utilizzare i nomi delle variabili che significano qualcosa, in modo che il codice diventa comprensibile. Poi, dopo ho capito il problema, è chiaramente un problema ricorsivo, come una volta si è scelto un numero, il problema di trovare i valori possibili per il resto delle piazze sono esattamente lo stesso problema, ma con valori diversi in.
Quindi mi sento di fare in questo modo:
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))
Ho anche notato che la soluzione sembra ancora buggy. Per i valori max_val=8, target_sum=20 and n_cells=5
il codice non trovare la soluzione (8,6,4,1,1,)
, come esempio. Non sono sicuro se questo significa che ho perso una regola in questo o no, ma mi pare di capire le regole che dovrebbero essere una valida opzione.
Ecco una versione con generatori, consente di risparmiare un paio di righe, e la memoria se i valori sono molto grandi, ma come la ricorsione, generatori può essere difficile da "ottenere".
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)))
Altri suggerimenti
Il vostro algoritmo sembra abbastanza buona a prima vista, e non credo OO o un'altra lingua sarebbe migliorare il codice. Non posso dire se la ricorsione avrebbe aiutato, ma ammiro l'approccio non ricorsivo. Scommetto che era più difficile da far funzionare ed è più difficile da leggere, ma probabilmente è più efficiente ed è sicuramente abbastanza intelligente. Per essere onesti non ho analizzare l'algoritmo in dettaglio ma sembra certamente come qualcosa che ha avuto molto tempo per ottenere il corretto funzionamento. Scommetto che ci sono stati un sacco di off-by-1 errori e casi limite strani si doveva pensare attraverso, eh?
Dato tutto questo, in fondo tutto quello che ho cercato di fare è stato piuttosto il vostro codice come meglio potevo, sostituendo i numerosi C-ismi con più idiomatiche Python-ismi. Spesso ciò che richiede un ciclo in C può essere fatto in una sola riga in Python. Inoltre ho provato a cambiare titolo le cose di seguire le convenzioni di denominazione Python meglio e ripulito i commenti un po '. Spero non ti offendere con nessuno dei miei cambiamenti. Si può prendere quello che vuoi e lasciare il resto. : -)
Qui ci sono le note che ho preso come ho lavorato:
- Cambiato il codice che inizializza
tmp
a un gruppo di 1 di al più idiomaticatmp = [1] * n_cells
. - Modificato
for
ciclo che riassumetmp_sum
a idiomaticasum(tmp)
. - Quindi sostituito tutte le spire con
tmp = <list> + <list>
sola linea. - Spostato
raise doneException
ainit_tmp_new_ceiling
e si liberò delsucceeded
bandiera. - Il check-in
raise
in realtà sembra inutile. Rimuoverlo, l'unicomake_combos_n_cells
s sinistra erano indoneException
, così ho cambiato coloro ai rendimenti regolari e cadutoif
del tutto. - normalizzato mix di 4 spazi e 8 spazi per l'indentazione.
- Rimosso parentesi inutili intorno ai vostri
tmp[p2] - tmp[p1] == 0
condizioni. -
tmp[p2] == tmp[p1]
è la stessa cosa diwhile True: if new_ceiling_flag: break
. - Cambiato
while not new_ceiling_flag
acombos
. - Non è necessario per inizializzare le variabili a 0 nella parte superiore delle vostre funzioni.
- Rimosso
yield
elenco e la funzione modificata percombo
sue tuple appena vengono generati. - Rinominato
new_ceiling_flag
aceiling_changed
. - Rinominato <=> a <=>.
Ed ecco il codice per il vostro esame:
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))
Ecco la soluzione ricorsiva semplice che mi viene in mente a "trovare tutte le possibili combinazioni di n numeri con valori x tale che 1 <= x <= max_val e x (1) + ... + x (n) = bersaglio ". Sto sviluppando da zero. Ecco una versione senza nessuna ottimizzazione a tutti, solo per semplicità:
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
Il caso base n==0
(dove non si può ottenere una qualsiasi più numeri) determinare o tupla finora se soddisfa la condizione, o nulla, allora si conclude (resi).
Se noi dovremmo produrre tuple di lunghezza superiore che abbiamo costruito finora, il if/else
si assicura cediamo solo tuple non decrescente, al fine di evitare la ripetizione (che hai fatto dire "combinazione" piuttosto che "permutazione") .
Il for
cerca tutte le possibilità per "questo" voce e loop sopra qualunque sia il livello immediatamente inferiore verso il basso della ricorsione è ancora in grado di produrre.
L'output che vedo è:
(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)
che sembra corretto.
Ci sono bazillion possibili ottimizzazioni, ma, ricorda:
Per prima farlo funzionare, allora lo rendono veloce
I corrispondenza con Kent Beck attribuire correttamente questa citazione in "Python in a Nutshell", e lui mi dice ha ottenuto da suo padre, il cui compito era in realtà estranei alla programmazione; -).
In questo caso, mi sembra che la questione chiave è comprensione che cosa sta succedendo, e qualsiasi ottimizzazione potrebbe interferire, quindi sto andando tutti fuori per "semplice e comprensibile"; siamo in grado, se necessario !, ottimizzare i calzini fuori una volta il PO conferma che possono a capire quello che sta succedendo in questa pura, la versione non ottimizzata!
Mi spiace dirlo, il codice è una specie di lunga e non particolarmente leggibile. Se si può cercare di riassumere in qualche modo, forse qualcuno può aiutare a scrivere in modo più chiaro.
Per quanto riguarda il problema stesso, il mio primo pensiero sarebbe quello di utilizzare la ricorsione. (Per quanto ne so, si sta già facendo. Ancora una volta Mi dispiace per la mia incapacità di leggere il codice.) Pensare ad un modo che è possibile ridurre il problema ad una versione più piccola più facile dello stesso problema, più volte, fino ad ottenere una caso banale con una risposta molto semplice.
Per essere un po 'più concreto, si dispone di questi tre parametri, max_val, target_sum e n_cells. Si può impostare uno di quei numeri a un valore particolare, al fine di darvi un problema estremamente semplice che non richiede pensiero a tutti? Una volta che avete, si può ridurre la versione leggermente più difficile del problema al già risolto uno?
EDIT: Ecco il mio codice. Non mi piace il modo in cui lo fa la de-duplicazione. Sono sicuro che c'è un modo più Pythonic. Inoltre, esso non consente utilizzando lo stesso numero due volte in una combinazione. Per annullare questo comportamento, basta prendere la linea if n not in numlist:
. Non sono sicuro se questo è del tutto corretto, ma sembra funzionare ed è (secondo me) più leggibile. Si potrebbe facilmente aggiungere Memoizzazione e che sarebbe probabilmente accelerarlo un bel po '.
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))
Prima di tutto, sto imparando Python me quindi questa soluzione non sarà grande, ma questo è solo un tentativo di risolvere questo. Ho cercato di risolverlo in modo ricorsivo e penso che una soluzione ricorsiva sarebbe l'ideale per questo tipo di problema, anche se questa soluzione ricorsiva potrebbe non essere questa:
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
Ecco una soluzione semplice in 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 po 'offtopic, ma ancora potrebbe contribuire a KenKen programmazione.
ho ottenuto buoni risultati utilizzando DLX algorhitm per risolvere Sudoku Killer (molto simmilar come KenKen ha gabbie, ma solo le somme). Ci sono voluti meno di secondo per la maggior parte dei problemi ed è stato implementato in linguaggio MATLAB.
di riferimento presenti in questo forum http://www.setbb.com/phpbb/viewtopic. php? t = 1274 & highlight = & mforum = sudoku
sudoku dell'assassino "Guarda wikipedia, cant post link iper" spammer damt
Ecco un ingenuo, ma succinta, soluzione con generatori:
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
avrei ottimizzato tale codice enumerando direttamente l'elenco di reti discendente, ma trovare itertools.product molto più chiaro per una soluzione di primo passaggio. Infine, chiamando la funzione:
for m in latinSquares(6, 12, 4):
print m
Ed ecco un'altra soluzione ricorsiva generatore basata, ma questa volta utilizzando qualche semplice matematica per calcolare intervalli ad ogni passo, evitando inutili ricorsione:
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))
Questo codice non verrà effettuata con un AssertionError se si fornisce i parametri che sono impossibili da soddisfare; questo è un effetto collaterale del mio "criterio di correttezza" che non facciamo mai una ricorsione non necessaria. Se non si desidera che effetto collaterale, rimuovere le affermazioni.
Si noti l'uso - (- x / y) per arrotondare dopo la divisione. Ci può essere un modo più divinatorio di scrivere che. Si noti anche Sto utilizzando espressioni generatore invece di resa.
for m in latinSquares(6,12,4):
print m