Python 2 & PuLP - 2 644 688 квадратов (оптимально минимизированы); 10 753 553 квадрата (оптимально максимизированы)
Минимально гольф до 1152 байтов
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
(Примечание: строки с сильными отступами начинаются с табуляции, а не пробелов.)
Пример вывода: https://drive.google.com/file/d/0B-0NVE9E8UJiX3IyQkJZVk82Vkk/view?usp=sharing
Оказывается, что подобные проблемы легко конвертируются в целочисленные линейные программы, и мне нужна была основная проблема, чтобы научиться использовать PuLP - интерфейс Python для различных программ для решения LP - для моего собственного проекта. Также оказалось, что PuLP чрезвычайно прост в использовании, и неумелый сборщик LP сработал отлично, когда я впервые попробовал его.
Две полезные вещи, связанные с использованием IP-решателя с ветвями и связями для выполнения тяжелой работы по решению этой проблемы для меня (за исключением того, что нет необходимости реализовывать решатель ветвей и границ):
- Специализированные решатели действительно быстрые. Эта программа решает все 50000 проблем за 17 часов на моем относительно недорогом домашнем ПК. Каждый экземпляр занимал от 1 до 1,5 секунд, чтобы решить.
- Они дают гарантированные оптимальные решения (или сообщают вам, что им это не удалось). Таким образом, я могу быть уверен, что никто не побьет мой результат в квадратах (хотя кто-то может связать его и победить меня в игре в гольф).
Как использовать эту программу
Сначала вам нужно установить PuLP. pip install pulp
должен сделать трюк, если у вас установлен пипс.
Затем вам нужно поместить в файл с именем «c» следующее: https://drive.google.com/file/d/0B-0NVE9E8UJiNFdmYlk1aV9aYzQ/view?usp=sharing
Затем запустите эту программу в любой поздней сборке Python 2 из того же каталога. Менее чем через день у вас будет файл с именем «s», содержащий 50 000 решенных неограммных сеток (в читаемом формате), в каждой из которых будет указано общее количество заполненных квадратов.
Если вы хотите максимизировать количество заполненных квадратов вместо этого, измените LpMinimize
на строке 8 LpMaximize
вместо этого. Вы получите очень похожий результат: https://drive.google.com/file/d/0B-0NVE9E8UJiYjJ2bzlvZ0RXcUU/view?usp=sharing
Формат ввода
Эта программа использует измененный формат ввода, поскольку Джо З. сказал, что нам будет разрешено перекодировать формат ввода, если мы захотим в комментарии к ОП. Нажмите на ссылку выше, чтобы увидеть, как это выглядит. Он состоит из 10000 строк, каждая из которых содержит 16 цифр. Строки с четными номерами - это величины для строк данного экземпляра, а строки с нечетными номерами - это величины для столбцов того же экземпляра, что и линия над ними. Этот файл был сгенерирован следующей программой:
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
(Эта программа перекодирования также дала мне дополнительную возможность протестировать мой собственный класс BitQueue, который я создал для того же проекта, упомянутого выше. Это просто очередь, в которую данные могут передаваться как последовательности битов ИЛИ байтов, и из которых данные могут быть вытолкнутым либо битом, либо байтом за раз. В этом случае это работало отлично.)
Я перекодировал входные данные по той конкретной причине, что для построения ILP дополнительная информация о сетках, которые использовались для генерации величин, совершенно бесполезна. Величины - единственные ограничения, и поэтому величины - это все, к чему мне нужен был доступ.
Ungolfed ILP строитель
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
Это программа, которая фактически создала «пример вывода», связанный выше. Отсюда и очень длинные струны в конце каждой сетки, которые я обрезал при игре в гольф. (Версия для гольфа должна давать идентичный результат, без слов "Filled squares for "
)
Как это устроено
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)
Я использую сетку 18x18, с центральной частью 16x16, которая является настоящим решением головоломки. cells
это сетка. Первая строка создает 324 двоичных переменных: «cell_0_0», «cell_0_1» и т. Д. Я также создаю сетки "пробелов" между и вокруг ячеек в части решения сетки. rowseps
указывает на 289 переменных, которые символизируют пространства, которые разделяют ячейки по горизонтали, а colseps
также указывает на переменные, которые отмечают пространства, которые разделяют ячейки по вертикали. Вот диаграмма 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
В 0
ы и □
с являются двоичные значения отслеживаемых cell
переменных, то |
s являются двоичные значения отслеживали с помощью rowsep
переменных, и -
s являются двоичные значения отслеживали с помощью colsep
переменных.
prob += sum(cells[r][c] for r in rows for c in cols),""
Это целевая функция. Просто сумма всех cell
переменных. Поскольку это бинарные переменные, это как раз количество заполненных квадратов в решении.
for i in rows:
prob += cells["0"][i] == 0,""
prob += cells[i]["0"] == 0,""
prob += cells["17"][i] == 0,""
prob += cells[i]["17"] == 0,""
Это просто устанавливает ячейки вокруг внешнего края сетки в ноль (вот почему я представлял их как нули выше). Это наиболее целесообразный способ отследить, сколько «блоков» ячеек заполнено, поскольку он гарантирует, что каждому изменению с незаполненного на заполненный (перемещение по столбцу или строке) соответствует соответствующее изменение с заполненного на незаполненный (и наоборот). ), даже если первая или последняя ячейка в строке заполнена. Это единственная причина использования сетки 18x18. Это не единственный способ подсчета блоков, но я думаю, что это самый простой.
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,""
Это настоящее мясо логики ILP. В основном это требует, чтобы каждая ячейка (кроме тех, которые находятся в первой строке и столбце) была логическим xor ячейки и разделителя непосредственно слева от ее строки и непосредственно над ней в ее столбце. Я получил ограничения, которые имитируют xor в целочисленной программе {0,1} из этого замечательного ответа: /cs//a/12118/44289
Чтобы объяснить немного больше: это ограничение xor делает так, чтобы разделители могли быть 1, если и только если они лежат между ячейками, которые являются 0 и 1 (отмечая изменение от незаполненного к заполненному или наоборот). Таким образом, в строке или столбце будет ровно вдвое больше однозначных разделителей, чем количество блоков в этой строке или столбце. Другими словами, сумма разделителей в данной строке или столбце ровно в два раза больше этой строки / столбца. Отсюда следующие ограничения:
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,""
И это в значительной степени так. Остальные просто запрашивают решение по умолчанию для решения ILP, а затем форматируют полученное решение, когда оно записывает его в файл.