Python 2 e PuLP - 2.644.688 quadrados (minimizado idealmente); 10.753.553 quadrados (maximizado da melhor maneira)
Golfe mínimo para 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
(NB: as linhas fortemente recuadas começam com tabulações, não espaços.)
Exemplo de saída: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Acontece que problemas como esses são facilmente conversíveis em Programas Lineares Inteiros, e eu precisava de um problema básico para aprender a usar o PuLP - uma interface python para uma variedade de solucionadores de LP - para um projeto meu. Acontece também que o PuLP é extremamente fácil de usar, e o construtor de LP despojado funcionou perfeitamente na primeira vez que o experimentei.
As duas coisas boas sobre o emprego de um solucionador de IP ramificado e vinculado para fazer o trabalho duro de resolver isso para mim (além de não ter que implementar um solucionador de ramificação e vinculado) são:
- Os solucionadores criados de propósito são realmente rápidos. Este programa resolve todos os 50000 problemas em cerca de 17 horas no meu PC doméstico relativamente baixo. Cada instância levou de 1 a 1,5 segundos para ser resolvida.
- Eles produzem soluções ótimas garantidas (ou dizem que falharam em fazê-lo). Assim, posso ter certeza de que ninguém vai bater minha pontuação em quadrados (embora alguém possa empatar e me vencer na parte do golfe).
Como usar este programa
Primeiro, você precisará instalar o PuLP. pip install pulp
deve fazer o truque se você tiver o pip instalado.
Em seguida, será necessário colocar o seguinte em um arquivo chamado "c": https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Em seguida, execute este programa em qualquer versão posterior do Python 2 do mesmo diretório. Em menos de um dia, você terá um arquivo chamado "s" que contém 50.000 grades não programadas (em formato legível), cada uma com o número total de quadrados preenchidos listados abaixo.
Se você deseja maximizar o número de quadrados preenchidos, altere a LpMinimize
linha 8 para LpMaximize
. Você obterá resultados muito parecidos com este: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Formato de entrada
Este programa usa um formato de entrada modificado, já que Joe Z. disse que poderíamos recodificar o formato de entrada se gostarmos de um comentário sobre o OP. Clique no link acima para ver como é. Consiste em 10000 linhas, cada uma contendo 16 números. As linhas numeradas pares são as magnitudes das linhas de uma determinada instância, enquanto as linhas numeradas ímpares são as magnitudes das colunas da mesma instância que a linha acima delas. Este arquivo foi gerado pelo seguinte 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
(Esse programa de recodificação também me deu uma oportunidade extra de testar minha classe BitQueue personalizada que criei para o mesmo projeto mencionado acima. É simplesmente uma fila na qual os dados podem ser enviados como sequências de bits OU bytes e a partir dos quais os dados podem ser exibido um pouco ou um byte de cada vez. Nesse caso, funcionou perfeitamente.)
Recodifiquei a entrada pelo motivo específico de que, para construir um ILP, as informações extras sobre as grades usadas para gerar as magnitudes são perfeitamente inúteis. As magnitudes são as únicas restrições e, portanto, as magnitudes são tudo o que eu precisava acessar.
Construtor ILP Ungolfed
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 é o programa que realmente produziu a "saída de exemplo" vinculada acima. Daí as cordas extra longas no final de cada grade, que eu truncava ao jogar no golfe. (A versão golfada deve produzir saída idêntica, menos as palavras "Filled squares for "
)
Como 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)
Eu uso uma grade 18x18, com a parte central 16x16 sendo a solução real do quebra-cabeça. cells
é essa grade. A primeira linha cria 324 variáveis binárias: "cell_0_0", "cell_0_1" e assim por diante. Também crio grades dos "espaços" entre e ao redor das células na parte da solução da grade. rowseps
aponta para as 289 variáveis que simbolizam os espaços que separam as células horizontalmente, enquanto da colseps
mesma forma aponta para as variáveis que marcam os espaços que separam as células verticalmente. Aqui está um 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
Os 0
s e os □
são os valores binários rastreados pelas cell
variáveis, os |
s são os valores binários rastreados pelas rowsep
variáveis e os -
s são os valores binários rastreados pelas colsep
variáveis.
prob += sum(cells[r][c] for r in rows for c in cols),""
Esta é a função objetivo. Apenas a soma de todas as cell
variáveis. Como essas são variáveis binárias, esse é exatamente o número de quadrados preenchidos na solução.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Isso apenas define as células ao redor da borda externa da grade como zero (e é por isso que eu as representei como zeros acima). Essa é a maneira mais conveniente de rastrear quantos "blocos" de células são preenchidos, pois garante que cada alteração de não preenchida para preenchida (movendo-se através de uma coluna ou linha) seja correspondida por uma alteração correspondente de preenchida para não preenchida (e vice-versa ), mesmo que a primeira ou a última célula da linha esteja preenchida. Esse é o único motivo para usar uma grade 18x18 em primeiro lugar. Não é a única maneira de contar blocos, mas acho que é a mais simples.
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 é a verdadeira carne da lógica do ILP. Basicamente, exige que cada célula (exceto as da primeira linha e coluna) seja o xor lógico da célula e o separador diretamente à sua esquerda na linha e diretamente acima dela na coluna. Eu recebi as restrições que simulam um xor em um programa inteiro {0,1} com esta resposta maravilhosa: /cs//a/12118/44289
Para explicar um pouco mais: essa restrição xor permite que os separadores possam ser 1 se e somente se estiverem entre células que são 0 e 1 (marcando uma mudança de não preenchida para preenchida ou vice-versa). Assim, haverá exatamente o dobro de separadores com valor 1 em uma linha ou coluna do número de blocos nessa linha ou coluna. Em outras palavras, a soma dos separadores em uma determinada linha ou coluna é exatamente o dobro da magnitude dessa linha / coluna. Daí as seguintes restrições:
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 é isso mesmo. O restante apenas pede ao solucionador padrão que resolva o ILP e formata a solução resultante enquanto a grava no arquivo.