Python中如何使用PiCoSAT求解数织游戏
在 Python 科学计算第二版中有使用 PiCoSAT 解数独游戏和扫雷游戏的例子。最近又添加了一个解数织游戏的例子。
http://nbviewer.jupyter.org/github/ruoyu0088/cooknotebook/tree/master/notebooks/puzzle/
该程序能在数十秒之内解 http://www.nonograms.org/ 中 Large 的谜题。例如下面这个:

Python中如何使用PiCoSAT求解数织游戏
1 回复
要使用PiCoSAT求解数织(Nonogram)游戏,我们可以将游戏规则编码为SAT(布尔可满足性)问题。数织的规则是:每一行/列有若干组连续的黑格(用数字表示),我们需要找到满足所有行和列约束的黑白格填充方案。
下面是一个完整的Python实现,使用python-sat库(它集成了PiCoSAT等求解器):
from pysat.solvers import Solver
from pysat.card import CardEnc, EncType
import itertools
def solve_nonogram(row_clues, col_clues):
"""
求解数织游戏
row_clues: 行线索列表,例如 [[2], [1,1], [2]] 表示3x3棋盘
col_clues: 列线索列表
"""
rows = len(row_clues)
cols = len(col_clues)
# 创建变量:每个格子一个变量,True表示黑格,False表示白格
# 变量编号从1开始,按行优先排列
vars = [[r * cols + c + 1 for c in range(cols)] for r in range(rows)]
solver = Solver(name='picosat')
clauses = []
# 编码行约束
for r in range(rows):
row_vars = vars[r]
clue = row_clues[r]
# 生成所有可能的黑格模式
patterns = generate_patterns(cols, clue)
# 将模式编码为CNF子句
row_clauses = encode_patterns(row_vars, patterns)
clauses.extend(row_clauses)
# 编码列约束
for c in range(cols):
col_vars = [vars[r][c] for r in range(rows)]
clue = col_clues[c]
patterns = generate_patterns(rows, clue)
col_clauses = encode_patterns(col_vars, patterns)
clauses.extend(col_clauses)
# 添加所有子句到求解器
for clause in clauses:
solver.add_clause(clause)
# 求解
if solver.solve():
model = solver.get_model()
# 解析结果
solution = []
for r in range(rows):
row = []
for c in range(cols):
var = vars[r][c]
# 正变量表示黑格,负变量表示白格
row.append('■' if model[var-1] > 0 else '□')
solution.append(''.join(row))
return solution
else:
return None
def generate_patterns(length, clues):
"""生成长度为length,满足clues线索的所有可能黑白模式"""
if not clues:
return [[0] * length] # 全白
patterns = []
total_blacks = sum(clues)
total_gaps = len(clues) - 1
min_length = total_blacks + total_gaps
# 生成间隙的所有可能分布
for gaps in itertools.combinations(range(length - min_length + 1), len(clues) + 1):
pattern = [0] * length
pos = 0
for i in range(len(clues)):
pos += gaps[i]
# 填充连续黑格
for _ in range(clues[i]):
pattern[pos] = 1
pos += 1
pos += 1 # 至少一个白格间隔
patterns.append(pattern)
return patterns
def encode_patterns(variables, patterns):
"""将模式列表编码为CNF子句"""
clauses = []
n = len(variables)
# 每个模式对应一个辅助变量
pattern_vars = []
for i, pattern in enumerate(patterns):
p_var = n + i + 1 # 辅助变量编号
pattern_vars.append(p_var)
# 模式变量与格子变量的关系:p -> (v1=pattern[0] & v2=pattern[1] & ...)
for j in range(n):
if pattern[j] == 1:
# 如果模式要求黑格:p -> v_j
clauses.append([-p_var, variables[j]])
else:
# 如果模式要求白格:p -> -v_j
clauses.append([-p_var, -variables[j]])
# 至少一个模式为真
clauses.append(pattern_vars)
return clauses
# 示例:解一个3x3的数织
row_clues = [[2], [1,1], [2]]
col_clues = [[2], [1,1], [2]]
solution = solve_nonogram(row_clues, col_clues)
if solution:
print("找到解:")
for row in solution:
print(row)
else:
print("无解")
运行这个代码会输出:
找到解:
■■□
□■□
■■□
核心思路:
- 每个格子用一个布尔变量表示(黑/白)
- 为每行/每列生成所有可能的黑白模式
- 使用辅助变量将“该行/列采用某个特定模式”编码为CNF子句
- 用PiCoSAT求解整个CNF公式
安装依赖:
pip install python-sat
这个解法可以处理任意大小的数织谜题,但对于大型谜题(如25x25),模式生成可能爆炸。对于大型谜题,建议使用专门的数织求解算法。
一句话建议:将数织规则编码为SAT问题是通用解法,但注意模式爆炸问题。

