Python 2 & PuLP - 2.644.688 Quadrate (optimal minimiert); 10.753.553 Quadrate (optimal maximiert)
Minimal auf 1152 Bytes golfen
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
(Hinweis: Die stark eingerückten Zeilen beginnen mit Tabulatoren und nicht mit Leerzeichen.)
Beispielausgabe: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Es stellt sich heraus, dass Probleme wie zum Beispiel leicht in ganzzahlige lineare Programme konvertierbar sind, und ich brauchte ein grundlegendes Problem, um zu lernen, wie man PuLP - eine Python-Schnittstelle für eine Vielzahl von LP-Solvern - für ein eigenes Projekt verwendet. Es stellt sich auch heraus, dass PuLP extrem einfach zu bedienen ist und der ungolfed LP-Builder beim ersten Versuch einwandfrei funktioniert hat.
Die zwei schönen Dinge beim Einsatz eines Branch-and-Bound-IP-Solvers, um die harte Arbeit zu erledigen, die ich für dieses Problem geleistet habe (abgesehen davon, dass ich keinen Branch-and-Bound-Solver implementieren muss), sind die folgenden
- Speziell entwickelte Löser sind sehr schnell. Dieses Programm löst alle 50000 Probleme in ungefähr 17 Stunden auf meinem relativ einfachen Heim-PC. Für die Lösung jeder Instanz wurden 1-1,5 Sekunden benötigt.
- Sie produzieren garantiert optimale Lösungen (oder teilen Ihnen mit, dass sie dies nicht getan haben). So kann ich sicher sein, dass niemand meine Punktzahl in Quadraten schlagen wird (obwohl es jemand binden und mich auf dem Golf-Teil schlagen könnte).
Wie man dieses Programm benutzt
Zunächst müssen Sie PuLP installieren. pip install pulp
sollte den Trick machen, wenn Sie Pip installiert haben.
Dann müssen Sie Folgendes in eine Datei mit dem Namen "c" einfügen: einfügen https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Führen Sie dieses Programm dann in einem beliebigen späten Python 2-Build aus demselben Verzeichnis aus. In weniger als einem Tag haben Sie eine Datei mit dem Namen "s", die 50.000 gelöste Nonogramm-Gitter (in lesbarem Format) mit der Gesamtzahl der darunter aufgeführten ausgefüllten Quadrate enthält.
Wenn Sie stattdessen die Anzahl der ausgefüllten Felder maximieren möchten, ändern Sie die LpMinimize
Option in Zeile 8 in LpMaximize
. Sie erhalten eine sehr ähnliche Ausgabe: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Eingabeformat
Dieses Programm verwendet ein modifiziertes Eingabeformat, da Joe Z. angab, dass wir das Eingabeformat neu codieren dürfen, wenn wir dies in einem Kommentar zum OP wünschen. Klicken Sie auf den Link oben, um zu sehen, wie es aussieht. Es besteht aus 10000 Zeilen mit jeweils 16 Ziffern. Die geradzahligen Linien sind die Beträge für die Zeilen einer bestimmten Instanz, während die ungeradzahligen Linien die Beträge für die Spalten derselben Instanz wie die darüber liegende Zeile sind. Diese Datei wurde vom folgenden Programm generiert:
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
(Dieses Programm zur Neucodierung gab mir außerdem die Möglichkeit, meine benutzerdefinierte BitQueue-Klasse zu testen, die ich für dasselbe oben erwähnte Projekt erstellt habe. Es handelt sich lediglich um eine Warteschlange, in die Daten als Folgen von Bits ODER Bytes übertragen werden können und aus der Daten stammen können entweder ein bisschen oder ein Byte auf einmal gepoppt werden. In diesem Fall hat es perfekt funktioniert.)
Ich habe die Eingabe aus dem speziellen Grund neu codiert, dass zum Erstellen eines ILP die zusätzlichen Informationen zu den Gittern, die zum Generieren der Größen verwendet wurden, vollkommen unbrauchbar sind. Die Größen sind die einzigen Einschränkungen, und deshalb brauche ich nur Zugriff auf die Größen.
Ungolfed ILP-Erbauer
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
Dies ist das Programm, das tatsächlich die oben verlinkte "Beispielausgabe" erzeugt hat. Daher die extra langen Saiten am Ende jedes Gitters, die ich beim Golfen abgeschnitten habe. (Die Golfversion sollte eine identische Ausgabe ohne die Wörter produzieren "Filled squares for "
)
Wie es funktioniert
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)
Ich verwende ein 18x18-Raster, wobei der mittlere 16x16-Teil die eigentliche Rätsellösung ist. cells
ist dieses Gitter. In der ersten Zeile werden 324 Binärvariablen erstellt: "cell_0_0", "cell_0_1" usw. Ich erstelle auch Gitter der "Räume" zwischen und um die Zellen im Lösungsteil des Gitters. rowseps
Verweist auf die 289 Variablen, die die Räume symbolisieren, die Zellen horizontal trennen, und colseps
auf Variablen, die die Räume markieren, die Zellen vertikal trennen. Hier ist ein Unicode-Diagramm:
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
Das 0
s und □
s sind die von den cell
Variablen verfolgten Binärwerte , das |
s sind die von den rowsep
Variablen verfolgten Binärwerte und das -
s sind die von den colsep
Variablen verfolgten Binärwerte .
prob += sum(cells[r][c] for r in rows for c in cols),""
Dies ist die Zielfunktion. Nur die Summe aller cell
Variablen. Da es sich um binäre Variablen handelt, entspricht dies genau der Anzahl der ausgefüllten Quadrate in der Lösung.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Dadurch werden die Zellen um den äußeren Rand des Gitters auf Null gesetzt (weshalb ich sie oben als Nullen dargestellt habe). Dies ist die zweckmäßigste Methode, um zu verfolgen, wie viele "Zellenblöcke" gefüllt sind, da so sichergestellt wird, dass jeder Wechsel von ungefüllt zu gefüllt (über eine Spalte oder Zeile) mit einem entsprechenden Wechsel von gefüllt zu ungefüllt (und umgekehrt) übereinstimmt ), auch wenn die erste oder letzte Zelle in der Zeile gefüllt ist. Dies ist der einzige Grund für die Verwendung eines 18x18-Rasters. Es ist nicht die einzige Möglichkeit, Blöcke zu zählen, aber ich denke, es ist die einfachste.
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,""
Dies ist das eigentliche Fleisch der Logik der ILP. Grundsätzlich ist es erforderlich, dass jede Zelle (mit Ausnahme der Zelle in der ersten Zeile und Spalte) das logische xor der Zelle und des Trennzeichens direkt links in der Zeile und direkt darüber in der Spalte ist. Ich habe die Einschränkungen, die ein xor innerhalb eines {0,1} Integer-Programms simulieren, aus dieser wunderbaren Antwort erhalten: /cs//a/12118/44289
Um ein bisschen mehr zu erklären: Diese xor-Einschränkung bewirkt, dass die Trennzeichen genau dann 1 sein können, wenn sie zwischen den Zellen 0 und 1 liegen (ein Wechsel von ungefüllt zu gefüllt oder umgekehrt). Somit gibt es in einer Zeile oder Spalte genau doppelt so viele einwertige Trennzeichen wie die Anzahl der Blöcke in dieser Zeile oder Spalte. Mit anderen Worten, die Summe der Trennzeichen in einer bestimmten Zeile oder Spalte ist genau doppelt so groß wie diese Zeile / Spalte. Daher die folgenden Einschränkungen:
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,""
Und das war's auch schon. Der Rest fordert den Standardlöser lediglich auf, das ILP zu lösen, und formatiert dann die resultierende Lösung, während sie in die Datei geschrieben wird.