Python 2 et PuLP - 2 644 688 carrés (minimisés de manière optimale); 10 753 553 carrés (optimisé au maximum)
Golf minimalement à 1152 octets
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: les lignes fortement en retrait commencent par des tabulations, pas des espaces.)
Exemple de sortie: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Il s'avère que des problèmes comme ceux-ci sont facilement convertibles en programmes linéaires entiers, et j'avais besoin d'un problème de base pour apprendre à utiliser PuLP - une interface python pour une variété de solveurs LP - pour mon propre projet. Il s'avère également que PuLP est extrêmement facile à utiliser, et le générateur de LP non golfé a parfaitement fonctionné la première fois que je l'ai essayé.
Les deux bonnes choses à propos de l'utilisation d'un solveur IP de branche et de liaison pour faire le travail difficile de résoudre ce problème pour moi (à part le fait de ne pas avoir à implémenter un solveur de branche et lié) sont que
- Les solveurs spécialement conçus sont vraiment rapides. Ce programme résout tous les 50000 problèmes en environ 17 heures sur mon PC domestique relativement bas de gamme. Chaque instance a pris de 1 à 1,5 seconde pour être résolue.
- Ils produisent des solutions optimales garanties (ou vous disent qu'ils ne l'ont pas fait). Ainsi, je peux être sûr que personne ne battra mon score dans les carrés (bien que quelqu'un puisse le lier et me battre sur la partie golf).
Comment utiliser ce programme
Tout d'abord, vous devrez installer PuLP. pip install pulp
devrait faire l'affaire si vous avez installé pip.
Ensuite, vous devrez mettre les éléments suivants dans un fichier appelé "c": https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Ensuite, exécutez ce programme dans n'importe quelle version Python 2 tardive du même répertoire. En moins d'une journée, vous aurez un fichier appelé "s" qui contient 50 000 grilles de nonogrammes résolues (dans un format lisible), chacune avec le nombre total de carrés remplis répertoriés en dessous.
Si vous souhaitez plutôt maximiser le nombre de carrés remplis, remplacez la LpMinimize
ligne 8 par à la LpMaximize
place. Vous obtiendrez une sortie très semblable à celle-ci: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Format d'entrée
Ce programme utilise un format d'entrée modifié, car Joe Z. a déclaré que nous serions autorisés à ré-encoder le format d'entrée si nous le souhaitons dans un commentaire sur l'OP. Cliquez sur le lien ci-dessus pour voir à quoi il ressemble. Il se compose de 10 000 lignes, chacune contenant 16 chiffres. Les lignes numérotées paires sont les magnitudes des lignes d'une instance donnée, tandis que les lignes numérotées impaires sont les magnitudes des colonnes de la même instance que la ligne au-dessus d'elles. Ce fichier a été généré par le programme suivant:
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
(Ce programme de ré-encodage m'a également donné une opportunité supplémentaire de tester ma classe BitQueue personnalisée que j'ai créée pour le même projet mentionné ci-dessus. être sauté soit un bit soit un octet à la fois. Dans ce cas, cela a parfaitement fonctionné.)
J'ai ré-encodé l'entrée pour la raison spécifique que pour construire un ILP, les informations supplémentaires sur les grilles qui ont été utilisées pour générer les amplitudes sont parfaitement inutiles. Les grandeurs sont les seules contraintes, et donc les grandeurs sont tout ce à quoi j'avais besoin d'accéder.
Générateur ILP non 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
Il s'agit du programme qui a réellement produit "l'exemple de sortie" lié ci-dessus. D'où les cordes extra longues à la fin de chaque grille, que j'ai tronquées lors du golf. (La version golfée devrait produire une sortie identique, moins les mots "Filled squares for "
)
Comment ça fonctionne
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)
J'utilise une grille 18x18, la partie centrale 16x16 étant la véritable solution du puzzle. cells
est cette grille. La première ligne crée 324 variables binaires: "cell_0_0", "cell_0_1", etc. Je crée également des grilles des "espaces" entre et autour des cellules dans la partie solution de la grille. rowseps
pointe sur les 289 variables qui symbolisent les espaces qui séparent les cellules horizontalement, tandis que colseps
pointe également sur les variables qui marquent les espaces qui séparent les cellules verticalement. Voici un diagramme 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
Les 0
s et □
s sont les valeurs binaires suivies par les cell
variables, les |
s sont les valeurs binaires suivies par les rowsep
variables et les -
s sont les valeurs binaires suivies par les colsep
variables.
prob += sum(cells[r][c] for r in rows for c in cols),""
Telle est la fonction objective. Juste la somme de toutes les cell
variables. Puisque ce sont des variables binaires, c'est exactement le nombre de carrés remplis dans la solution.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Cela met simplement les cellules autour du bord extérieur de la grille à zéro (c'est pourquoi je les ai représentées comme des zéros ci-dessus). Il s'agit du moyen le plus rapide de suivre le nombre de "blocs" de cellules remplis, car il garantit que chaque changement de non rempli à rempli (se déplaçant sur une colonne ou une ligne) correspond à un changement correspondant de rempli à non rempli (et vice versa). ), même si la première ou la dernière cellule de la ligne est remplie. C'est la seule raison d'utiliser une grille 18x18 en premier lieu. Ce n'est pas la seule façon de compter les blocs, mais je pense que c'est la plus 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,""
C'est la vraie chair de la logique de l'ILP. Fondamentalement, cela nécessite que chaque cellule (autre que celles de la première ligne et colonne) soit le xor logique de la cellule et le séparateur directement à sa gauche dans sa ligne et directement au-dessus dans sa colonne. J'ai obtenu les contraintes qui simulent un xor dans un programme entier {0,1} à partir de cette merveilleuse réponse: /cs//a/12118/44289
Pour expliquer un peu plus: cette contrainte xor fait que les séparateurs peuvent être 1 si et seulement s'ils se trouvent entre des cellules qui sont 0 et 1 (marquant un changement de non rempli à rempli ou vice versa). Ainsi, il y aura exactement deux fois plus de séparateurs à 1 valeur dans une ligne ou une colonne que le nombre de blocs dans cette ligne ou cette colonne. En d'autres termes, la somme des séparateurs sur une ligne ou une colonne donnée est exactement le double de la magnitude de cette ligne / colonne. D'où les contraintes suivantes:
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,""
Et c'est à peu près tout. Le reste demande simplement au solveur par défaut de résoudre l'ILP, puis formate la solution résultante pendant qu'elle l'écrit dans le fichier.