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("无解")

运行这个代码会输出:

找到解:
■■□
□■□
■■□

核心思路

  1. 每个格子用一个布尔变量表示(黑/白)
  2. 为每行/每列生成所有可能的黑白模式
  3. 使用辅助变量将“该行/列采用某个特定模式”编码为CNF子句
  4. 用PiCoSAT求解整个CNF公式

安装依赖

pip install python-sat

这个解法可以处理任意大小的数织谜题,但对于大型谜题(如25x25),模式生成可能爆炸。对于大型谜题,建议使用专门的数织求解算法。

一句话建议:将数织规则编码为SAT问题是通用解法,但注意模式爆炸问题。

回到顶部