算法知识图谱总览 / 离线与杂项技巧族 / 15谜题可解性判定

15谜题可解性判定

P2advancedstandardG-E-7

What · 是什么

N-puzzle可解性判定:逆序对数奇偶性+空格行距,不可解则无解

15-puzzle(数字滑块谜题, N-puzzle) 是 4x4 方盘放 15 个编号方块加一个空格的滑块游戏,每步将空格与相邻方块交换,目标按序排列。求解可判定性问题:给定一个盘面能否还原?判定法则:奇数阶(m 为奇数)盘面可解 ⇔ 逆序对数为偶数;偶数阶(m 为偶数)盘面可解 ⇔ 逆序对奇偶性与(空格所在行距底行的距离)奇偶性相同。最优解为 NP 困难,可解性判定与搜索(如 A*)是两个不同层面的问题。

Why · 为什么

直觉 · Intuition

每交换一次空格与相邻方块,逆序对奇偶性必翻转,而空格移动改变了其在网格中的行列。把盘面状态看成排列,还原目标排列的逆序对数为 0(偶)。对所有可还原状态,'逆序对奇偶性'与'空格位置奇偶性'存在不变量,判据正是检查两个不变量是否匹配。

原理 · Principle

Johnson & Story(1879)证明 m,n≥2 的 m×n 滑块盘可解性:构造可还原盘面的生成集合,论证所有偶数排列可解。Archer(1999)用哈密顿路径给出另一证明。判据实现:把方块按行主序读成序列(忽略空格),数逆序对数;对偶数阶盘,再加空格所在行距底行的距离(从 1 数起)贡献的奇偶。奇偶匹配则可解。

历史 · History

15-puzzle 由 Noyes Chapman 发明,1880s 风靡一时,是最古老的滑块类游戏。可解性由 Johnson & Story 于 1879 年证明,群理论角度可证其状态组为交错群 A_15。OI Wiki 与 CP-Algorithms 均收录其可解性判定与 A* 搜索解法,是最优解(至多 80 步)与 NP 困难性研究的经典对象。

How · 实现

伪代码

# 判定 m 行 n 列 N-puzzle 是否可解
board = 二维数组(空格=0)
seq = 行主序读入所有非零数字
inv = 数 seq 逆序对数
if n 为奇数:          # 列数(空格所在行横向移动不改变奇偶)
  return inv % 2 == 0
else:                 # 偶数阶
  blank_row = 空格所在行(0-index, 距底= m-1-blank_row)
  return (inv + (m - 1 - blank_row)) % 2 == 0

Python 实现

def inversions(seq):
    """归并排序求逆序对数。"""
    seq = list(seq)
    n = len(seq)
    tmp = [0] * n

    def merge_sort(l, r):
        if r - l <= 1:
            return 0
        mid = (l + r) // 2
        cnt = merge_sort(l, mid) + merge_sort(mid, r)
        i, j, k = l, mid, l
        while i < mid and j < r:
            if seq[i] <= seq[j]:
                tmp[k] = seq[i]
                i += 1
            else:
                tmp[k] = seq[j]
                cnt += mid - i
                j += 1
            k += 1
        while i < mid:
            tmp[k] = seq[i]
            i += 1
            k += 1
        while j < r:
            tmp[k] = seq[j]
            j += 1
            k += 1
        seq[l:r] = tmp[l:r]
        return cnt

    return merge_sort(0, n)


def is_solvable(board):
    """判断 m 行 n 列 N-puzzle 是否可解。
    board: list[list[int]], 0 表示空格。
    """
    m = len(board)
    n = len(board[0])
    seq = []
    blank_row = -1
    for i in range(m):
        for j in range(n):
            if board[i][j] == 0:
                blank_row = i
            else:
                seq.append(board[i][j])
    inv = inversions(seq)
    if n % 2 == 1:
        return inv % 2 == 0
    # 偶数阶: 逆序对奇偶性 与 空格距底行距离奇偶性 相同
    dist_from_bottom = m - 1 - blank_row
    return (inv + dist_from_bottom) % 2 == 0


def solved_board(m, n):
    """生成目标盘面(空格在右下)。"""
    b = [[(i * n + j + 1) % (m * n) for j in range(n)] for i in range(m)]
    return b


if __name__ == '__main__':
    # 目标盘面必可解
    assert is_solvable(solved_board(4, 4)) is True
    assert is_solvable(solved_board(3, 3)) is True

    # 3x3 已知可解盘
    ok33 = [[1, 2, 3], [4, 5, 6], [7, 8, 0]]
    assert is_solvable(ok33) is True
    # 3x3 已知不可解盘(交换1和2)
    bad33 = [[2, 1, 3], [4, 5, 6], [7, 8, 0]]
    assert is_solvable(bad33) is False

    # 4x4 目标盘
    ok44 = solved_board(4, 4)
    assert is_solvable(ok44) is True
    # 4x4 交换两个非空格数字应不可解(逆序奇偶变化)
    bad44 = [row[:] for row in ok44]
    bad44[0][0], bad44[0][1] = bad44[0][1], bad44[0][0]
    assert is_solvable(bad44) is False

    # 空格位置对偶数阶有影响: 目标盘移动空格到第0行
    moved = [row[:] for row in ok44]
    # 把空格从(3,3)移到(0,0), 其余数字循环移位保持合法
    vals = [v for row in ok44 for v in row if v != 0]
    it = iter(vals)
    newb = [[0] * 4 for _ in range(4)]
    for i in range(4):
        for j in range(4):
            if (i, j) == (0, 0):
                continue
            newb[i][j] = next(it)
    # 这种整盘平移不改变逆序数, 但空格行距变了 -> 奇偶可能改变
    print('3x3 ok=', is_solvable(ok33), ' bad=', is_solvable(bad33))
    print('4x4 ok=', is_solvable(ok44), ' bad=', is_solvable(bad44))
    print('4x4 moved blank to top-left:', is_solvable(newb))
    print('All 15-puzzle solvability tests passed!')
PyPy 提示逆序对用归并排序 O(n log n) 最稳; 若方盘数字序列很短(如 8-puzzle)直接用冒泡计数也可; 判定本身只关心奇偶, 可仅累计奇偶省时间。
时间复杂度O(M*N) 读取 + O(M*N log(M*N)) 逆序对(归并)
空间复杂度O(M*N)
注意 · 局限仅判定可解性, 不给出还原方案; 求最少步数是 NP 困难(15-puzzle 最优解至多 80 步); 判定公式依赖盘面尺寸奇偶, 混用会判错; 部分题目(如 POJ 1077 Eight)要求输出方案, 需 A*/IDA* 搜索。

易错点

例题

OJ题号难度考点链接
HackerRankn-puzzle4N Puzzle: A* 搜索求解 15-puzzle, 需要可解性判定+启发式https://www.hackerrank.com/challenges/n-puzzle
POJ10774Eight: 8-puzzle 搜索, 可解性判定(3x3 逆序对奇偶)必备http://poj.org/problem?id=1077
Codeforces645A3Amity Assessment: 3x3 滑块可解性判定(小规模奇偶性)https://codeforces.com/problemset/problem/645/A
步骤迷你图(节点 = 算法步骤 · 边 = 流程 · 绿色 = 起始步骤)

信源

✓ = 已核验 · △ = 未核验