Python 2 e PuLP - 2.644.688 quadrati (ottimizzati in modo ottimale); 10.753.553 quadrati (ottimizzati in modo ottimale)
Giocato a mini golf a 1152 byte
from pulp import*
x=0
f=open("c","r")
g=open("s","w")
for k,m in enumerate(f):
if k%2:
b=map(int,m.split())
p=LpProblem("Nn",LpMinimize)
q=map(str,range(18))
ir=q[1:18]
e=LpVariable.dicts("c",(q,q),0,1,LpInteger)
rs=LpVariable.dicts("rs",(ir,ir),0,1,LpInteger)
cs=LpVariable.dicts("cs",(ir,ir),0,1,LpInteger)
p+=sum(e[r][c] for r in q for c in q),""
for i in q:p+=e["0"][i]==0,"";p+=e[i]["0"]==0,"";p+=e["17"][i]==0,"";p+=e[i]["17"]==0,""
for o in range(289):i=o/17+1;j=o%17+1;si=str(i);sj=str(j);l=e[si][str(j-1)];ls=rs[si][sj];p+=e[si][sj]<=l+ls,"";p+=e[si][sj]>=l-ls,"";p+=e[si][sj]>=ls-l,"";p+=e[si][sj]<=2-ls-l,"";l=e[str(i-1)][sj];ls=cs[si][sj];p+=e[si][sj]<=l+ls,"";p+=e[si][sj]>=l-ls,"";p+=e[si][sj]>=ls-l,"";p+=e[si][sj]<=2-ls-l,""
for r,z in enumerate(a):p+=lpSum([rs[str(r+1)][c] for c in ir])==2*z,""
for c,z in enumerate(b):p+=lpSum([cs[r][str(c+1)] for r in ir])==2*z,""
p.solve()
for r in ir:
for c in ir:g.write(str(int(e[r][c].value()))+" ")
g.write('\n')
g.write('%d:%d\n\n'%(-~k/2,value(p.objective)))
x+=value(p.objective)
else:a=map(int,m.split())
print x
(NB: le linee fortemente rientrate iniziano con tabulazioni, non spazi.)
Esempio di output: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Si scopre che problemi come quelli sono prontamente convertibili in Integer Linear Program, e avevo bisogno di un problema di base per imparare a usare PuLP - un'interfaccia Python per una varietà di solutori di LP - per un mio progetto. Si scopre anche che PuLP è estremamente facile da usare e il costruttore di LP non golfato ha funzionato perfettamente la prima volta che l'ho provato.
Le due cose belle sull'impiego di un risolutore di indirizzi IP branch-and-bound per fare il duro lavoro di risolverlo per me (oltre a non dover implementare un risolutore di branch e bound) sono che
- I solutori appositamente progettati sono molto veloci. Questo programma risolve tutti i 50000 problemi in circa 17 ore sul mio PC di casa relativamente di fascia bassa. Per risolvere ogni istanza sono occorsi da 1-1,5 secondi.
- Producono soluzioni ottimali garantite (o ti dicono che non sono riuscite a farlo). Quindi, posso essere fiducioso che nessuno batterà il mio punteggio in quadrati (anche se qualcuno potrebbe legarlo e battermi sulla parte del golf).
Come usare questo programma
Innanzitutto, dovrai installare PuLP. pip install pulp
dovrebbe fare il trucco se hai installato pip.
Quindi, dovrai inserire quanto segue in un file chiamato "c": https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Quindi, esegui questo programma in qualsiasi build Python 2 in ritardo dalla stessa directory. In meno di un giorno, avrai un file chiamato "s" che contiene 50.000 griglie di nonogrammi risolte (in formato leggibile), ognuna con il numero totale di quadrati riempiti elencati sotto di essa.
Se invece desideri massimizzare il numero di quadrati riempiti, modifica invece la LpMinimize
riga 8 LpMaximize
. Otterrai un output molto simile a questo: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Formato di input
Questo programma utilizza un formato di input modificato, poiché Joe Z. ha affermato che ci sarebbe permesso di ricodificare il formato di input se ci piace in un commento sull'OP. Fai clic sul link sopra per vedere come appare. Si compone di 10000 righe, ciascuna contenente 16 numeri. Le linee numerate pari sono le magnitudini per le righe di una determinata istanza, mentre le linee dispari sono le magnitudini per le colonne della stessa istanza della linea sopra di esse. Questo file è stato generato dal seguente programma:
from bitqueue import *
with open("nonograms_b64.txt","r") as f:
with open("nonogram_clues.txt","w") as g:
for line in f:
q = BitQueue(line.decode('base64'))
nonogram = []
for i in range(256):
if not i%16: row = []
row.append(q.nextBit())
if not -~i%16: nonogram.append(row)
s=""
for row in nonogram:
blocks=0 #magnitude counter
for i in range(16):
if row[i]==1 and (i==0 or row[i-1]==0): blocks+=1
s+=str(blocks)+" "
print >>g, s
nonogram = map(list, zip(*nonogram)) #transpose the array to make columns rows
s=""
for row in nonogram:
blocks=0
for i in range(16):
if row[i]==1 and (i==0 or row[i-1]==0): blocks+=1
s+=str(blocks)+" "
print >>g, s
(Questo programma di ricodifica mi ha anche dato un'ulteriore opportunità per testare la mia classe BitQueue personalizzata che ho creato per lo stesso progetto sopra menzionato. È semplicemente una coda in cui i dati possono essere trasferiti come sequenze di bit O byte e da cui i dati possono essere spuntato un po 'o un byte alla volta. In questo caso, ha funzionato perfettamente.)
Ho ricodificato l'input per il motivo specifico che per costruire un ILP, le informazioni extra sulle griglie utilizzate per generare le magnitudini sono perfettamente inutili. Le magnitudini sono gli unici vincoli, e quindi le magnitudini sono tutto ciò di cui avevo bisogno di accedere.
Costruttore ILP non golfato
from pulp import *
total = 0
with open("nonogram_clues.txt","r") as f:
with open("solutions.txt","w") as g:
for k,line in enumerate(f):
if k%2:
colclues=map(int,line.split())
prob = LpProblem("Nonogram",LpMinimize)
seq = map(str,range(18))
rows = seq
cols = seq
irows = seq[1:18]
icols = seq[1:18]
cells = LpVariable.dicts("cell",(rows,cols),0,1,LpInteger)
rowseps = LpVariable.dicts("rowsep",(irows,icols),0,1,LpInteger)
colseps = LpVariable.dicts("colsep",(irows,icols),0,1,LpInteger)
prob += sum(cells[r][c] for r in rows for c in cols),""
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
for i in range(1,18):
for j in range(1,18):
si = str(i); sj = str(j)
l = cells[si][str(j-1)]; ls = rowseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
l = cells[str(i-1)][sj]; ls = colseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
for r,clue in enumerate(rowclues):
prob += lpSum([rowseps[str(r+1)][c] for c in icols]) == 2 * clue,""
for c,clue in enumerate(colclues):
prob += lpSum([colseps[r][str(c+1)] for r in irows]) == 2 * clue,""
prob.solve()
print "Status for problem %d: "%(-~k/2),LpStatus[prob.status]
for r in rows[1:18]:
for c in cols[1:18]:
g.write(str(int(cells[r][c].value()))+" ")
g.write('\n')
g.write('Filled squares for %d: %d\n\n'%(-~k/2,value(prob.objective)))
total += value(prob.objective)
else:
rowclues=map(int,line.split())
print "Total number of filled squares: %d"%total
Questo è il programma che ha effettivamente prodotto l '"output di esempio" collegato sopra. Da qui le stringhe extra lunghe alla fine di ogni griglia, che ho troncato durante il golf. (La versione golfata dovrebbe produrre un output identico, meno le parole "Filled squares for "
)
Come funziona
cells = LpVariable.dicts("cell",(rows,cols),0,1,LpInteger)
rowseps = LpVariable.dicts("rowsep",(irows,icols),0,1,LpInteger)
colseps = LpVariable.dicts("colsep",(irows,icols),0,1,LpInteger)
Uso una griglia 18x18, con la parte centrale 16x16 che rappresenta la vera soluzione del puzzle. cells
è questa griglia. La prima riga crea 324 variabili binarie: "cell_0_0", "cell_0_1" e così via. Creo anche griglie degli "spazi" tra e intorno alle celle nella parte della soluzione della griglia. rowseps
punta alle 289 variabili che simboleggiano gli spazi che separano le celle in orizzontale, mentre colseps
allo stesso modo punta alle variabili che segnano gli spazi che separano le celle in verticale. Ecco un diagramma unicode:
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|□|0
- - - - - - - - - - - - - - - -
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
I 0
s e □
s sono i valori binari rilevati dalle cell
variabili, i |
s sono i valori binari tracciati dalle rowsep
variabili, e le -
s sono i valori binari tracciati dalle colsep
variabili.
prob += sum(cells[r][c] for r in rows for c in cols),""
Questa è la funzione oggettiva. Solo la somma di tutte le cell
variabili. Poiché si tratta di variabili binarie, questo è esattamente il numero di quadrati riempiti nella soluzione.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Questo porta a zero le celle attorno al bordo esterno della griglia (motivo per cui le ho rappresentate come zero sopra). Questo è il modo più conveniente per tenere traccia di quanti "blocchi" di celle sono riempiti, poiché assicura che ogni cambiamento da non riempito a riempito (spostandosi attraverso una colonna o riga) sia accompagnato da un cambiamento corrispondente da riempito a non riempito (e viceversa ), anche se la prima o l'ultima cella della riga è piena. Questo è l'unico motivo per utilizzare una griglia 18x18 in primo luogo. Non è l'unico modo per contare i blocchi, ma penso che sia il più semplice.
for i in range(1,18):
for j in range(1,18):
si = str(i); sj = str(j)
l = cells[si][str(j-1)]; ls = rowseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
l = cells[str(i-1)][sj]; ls = colseps[si][sj]
prob += cells[si][sj] <= l + ls,""
prob += cells[si][sj] >= l - ls,""
prob += cells[si][sj] >= ls - l,""
prob += cells[si][sj] <= 2 - ls - l,""
Questa è la vera carne della logica dell'ILP. Fondamentalmente richiede che ogni cella (diversa da quelle nella prima riga e colonna) sia lo xor logico della cella e del separatore direttamente alla sua sinistra nella sua riga e direttamente sopra di essa nella sua colonna. Ho ottenuto i vincoli che simulano un xor all'interno di un programma intero {0,1} da questa meravigliosa risposta: /cs//a/12118/44289
Per spiegare un po 'di più: questo vincolo xor fa sì che i separatori possano essere 1 se e solo se si trovano tra celle che sono 0 e 1 (segnando un passaggio da non riempito a riempito o viceversa). Pertanto, in una riga o colonna ci saranno esattamente il doppio di separatori a 1 valore rispetto al numero di blocchi in quella riga o colonna. In altre parole, la somma dei separatori su una determinata riga o colonna è esattamente il doppio della grandezza di quella riga / colonna. Da qui i seguenti vincoli:
for r,clue in enumerate(rowclues):
prob += lpSum([rowseps[str(r+1)][c] for c in icols]) == 2 * clue,""
for c,clue in enumerate(colclues):
prob += lpSum([colseps[r][str(c+1)] for r in irows]) == 2 * clue,""
E questo è praticamente tutto. Il resto chiede semplicemente al risolutore predefinito di risolvere l'ILP, quindi formatta la soluzione risultante mentre la scrive nel file.