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(适用于各种LP解算器的python接口)。事实证明,PuLP极其易于使用,并且没有高尔夫球的LP构建器在我第一次尝试时就可以完美运行。
使用分支定界IP求解器为我完成艰苦的工作(而不是不必实现分支定界求解器)的两个好处是:
- 专门构建的求解器非常快。该程序可以在我相对低端的家用PC上在大约17个小时内解决所有50000个问题。每个实例需要1-1.5秒才能解决。
- 他们提供有保证的最佳解决方案(或告诉您他们没有这样做)。因此,我可以确信没有人会在方格中击败我的得分(尽管有人可能会打平它并在高尔夫球部分击败我)。
如何使用这个程序
首先,您需要安装PuLP。pip install pulp
如果您已经安装了pip,应该可以解决问题。
然后,您需要将以下内容放入名为“ 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
输入格式
该程序使用修改后的输入格式,因为Joe Z.说如果我们希望在OP中发表评论,我们将被允许对输入格式进行重新编码。单击上面的链接以查看外观。它由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的特定原因,我对输入进行了重新编码,有关用于生成幅度的网格的额外信息完全没有用。幅度是唯一的约束,因此幅度是我需要访问的全部。
空洞的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
S和□
S被跟踪的二进制值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逻辑的实质。基本上,它要求每个单元格(第一行和第一列中的单元格除外)是该单元格和分隔符的逻辑异或,直接在其行的左侧并在其列的正上方。我从这个奇妙的答案中得到了在{0,1}整数程序中模拟异或的约束:https : //cs.stackexchange.com/a/12118/44289
进一步说明:该xor约束使得分隔符在且仅当分隔符位于0和1的单元格之间(表示从未填充到填充或从填充到填充的变化)时才可以为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,然后在将生成的解决方案写入文件时对其进行格式化。