Python 2 y PuLP: 2,644,688 cuadrados (óptimamente minimizados); 10,753,553 cuadrados (óptimamente maximizados)
Mínimamente golfizado a 1152 bytes
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
(Nota: las líneas muy sangradas comienzan con pestañas, no espacios).
Salida de ejemplo: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Resulta que problemas como son fácilmente convertibles en programas lineales enteros, y necesitaba un problema básico para aprender a usar PuLP, una interfaz de Python para una variedad de solucionadores de LP, para un proyecto propio. También resulta que PuLP es extremadamente fácil de usar, y el generador de LP sin golf funcionó perfectamente la primera vez que lo probé.
Las dos cosas buenas de emplear un solucionador de IP de ramificación y enlace para hacer el arduo trabajo de resolver esto por mí (aparte de no tener que implementar un solucionador de ramificación y enlace) son que
- Los solucionadores especialmente diseñados son realmente rápidos. Este programa resuelve todos los problemas de 50000 en aproximadamente 17 horas en mi PC hogareña relativamente baja. Cada instancia tomó de 1-1.5 segundos para resolver.
- Producen soluciones óptimas garantizadas (o le dicen que no lo hicieron). Por lo tanto, puedo estar seguro de que nadie superará mi puntaje en cuadros (aunque alguien podría empatarlo y vencerme en la parte de golf).
Cómo usar este programa
Primero, necesitará instalar PuLP. pip install pulp
debería hacer el truco si tienes pip instalado.
Luego, deberá colocar lo siguiente en un archivo llamado "c": https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Luego, ejecute este programa en cualquier compilación de Python 2 tardía desde el mismo directorio. En menos de un día, tendrá un archivo llamado "s" que contiene 50,000 cuadrículas de nonogramas resueltas (en formato legible), cada una con el número total de cuadrados rellenos enumerados debajo.
Si desea maximizar el número de cuadrados rellenos en su lugar, cambie la LpMinimize
línea 8 a LpMaximize
. Obtendrá una salida muy similar a esta: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Formato de entrada
Este programa utiliza un formato de entrada modificado, ya que Joe Z. dijo que se nos permitiría volver a codificar el formato de entrada si quisiéramos en un comentario sobre el OP. Haga clic en el enlace de arriba para ver cómo se ve. Se compone de 10000 líneas, cada una con 16 números. Las líneas pares son las magnitudes de las filas de una instancia determinada, mientras que las líneas impares son las magnitudes de las columnas de la misma instancia que la línea que se encuentra sobre ellas. Este archivo fue generado por el siguiente programa:
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
(Este programa de re-codificación también me dio una oportunidad adicional para probar mi clase personalizada de BitQueue que creé para el mismo proyecto mencionado anteriormente. Es simplemente una cola a la cual los datos pueden ser empujados como secuencias de bits O bytes, y desde los cuales los datos pueden aparecer un bit o un byte a la vez. En este caso, funcionó perfectamente).
Re-codifiqué la entrada por la razón específica de que para construir un ILP, la información adicional sobre las cuadrículas que se usaron para generar las magnitudes es perfectamente inútil. Las magnitudes son las únicas restricciones, por lo que las magnitudes son todo lo que necesitaba acceso.
Constructor ILP sin golf
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
Este es el programa que realmente produjo el "resultado de ejemplo" vinculado anteriormente. De ahí las cuerdas extra largas al final de cada cuadrícula, que trunqué al jugar golf. (La versión de golf debería producir un resultado idéntico, menos las palabras "Filled squares for "
)
Cómo funciona
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)
Utilizo una cuadrícula de 18x18, con la parte central de 16x16 como la solución de rompecabezas real. cells
es esta cuadrícula La primera línea crea 324 variables binarias: "cell_0_0", "cell_0_1", y así sucesivamente. También creo cuadrículas de los "espacios" entre y alrededor de las celdas en la parte de solución de la cuadrícula. rowseps
apunta a las 289 variables que simbolizan los espacios que separan las celdas horizontalmente, mientras que de colseps
manera similar apunta a las variables que marcan los espacios que separan las celdas verticalmente. Aquí hay un diagrama 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
Los 0
sys □
son los valores binarios rastreados por las cell
variables, los |
s son los valores binarios rastreados por las rowsep
variables y los -
s son los valores binarios rastreados por las colsep
variables.
prob += sum(cells[r][c] for r in rows for c in cols),""
Esta es la función objetivo. Solo la suma de todas las cell
variables. Como se trata de variables binarias, esta es exactamente la cantidad de cuadrados rellenos en la solución.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Esto solo establece las celdas alrededor del borde exterior de la cuadrícula a cero (por eso las representé como ceros arriba). Esta es la forma más conveniente de rastrear cuántos "bloques" de celdas se llenan, ya que garantiza que cada cambio de lleno a lleno (moviéndose a través de una columna o fila) coincida con un cambio correspondiente de llenado a vacío (y viceversa) ), incluso si se llena la primera o la última celda de la fila. Esta es la única razón para usar una cuadrícula de 18x18 en primer lugar. No es la única forma de contar bloques, pero creo que es la más simple.
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,""
Esta es la verdadera carne de la lógica de la ILP. Básicamente requiere que cada celda (que no sean las de la primera fila y columna) sea el xor lógico de la celda y el separador directamente a su izquierda en su fila y directamente encima de ella en su columna. Obtuve las restricciones que simulan un xor dentro de un programa entero {0,1} de esta maravillosa respuesta: /cs//a/12118/44289
Para explicar un poco más: esta restricción xor hace que los separadores puedan ser 1 si y solo si se encuentran entre celdas que son 0 y 1 (marcando un cambio de vacío a lleno o viceversa). Por lo tanto, habrá exactamente el doble de separadores de 1 valor en una fila o columna que el número de bloques en esa fila o columna. En otras palabras, la suma de los separadores en una fila o columna dada es exactamente el doble de la magnitud de esa fila / columna. De ahí las siguientes restricciones:
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,""
Y eso es todo. El resto solo le pide al solucionador predeterminado que resuelva el ILP, luego formatea la solución resultante a medida que la escribe en el archivo.