16、构建一个Python数独求解器

构建一个Python数独求解器

1. 数独简介

数独是一种逻辑推理游戏,目标是在一个9×9的网格中填入数字,使得每一行、每一列以及每一个3×3的小方格内的数字都不重复。数独游戏通常会预先填入一些数字作为提示,玩家需要根据这些提示推断出剩余的数字。

2. 数独谜题的表示

在Python中,我们可以通过一个二维列表来表示数独谜题。每一行是一个列表,整个谜题是一个包含这些行列表的列表。例如,一个简单的数独谜题可以表示为:

puzzle = [
    [5, 3, 0, 0, 7, 0, 0, 0, 0],
    [6, 0, 0, 1, 9, 5, 0, 0, 0],
    [0, 9, 8, 0, 0, 0, 0, 6, 0],
    [8, 0, 0, 0, 6, 0, 0, 0, 3],
    [4, 0, 0, 8, 0, 3, 0, 0, 1],
    [7, 0, 0, 0, 2, 0, 0, 0, 6],
    [0, 6, 0, 0, 0, 0, 2, 8, 0],
    [0, 0, 0, 4, 1, 9, 0, 0, 5],
    [0, 0, 0, 0, 8, 0, 0, 7, 9]
]

3. 数据读取与表示

为了使程序更加通用,可以从文件中读取数独谜题。文件中的每一行包含9个字符,其中 0 表示空格。读取文件并创建初始列表的任务可以通过以下函数实现:

import sys

def read_puzzle(f):
    return [[int(a) for a in list(line.rstrip())] for line in f.readlines()]

这段代码的逻辑是:
1. 创建一个空列表作为最终结果。
2. 逐行读取输入文件。
3. 为数独谜题的每一行创建一个空行。
4. 在去除行尾的换行符后,逐字符读取该行。
5. 将每个字符转换为整数,并将其添加到行列表中。
6. 在读取完整行后,将行列表添加到数独谜题列表中。
7. 在读取输入文件的每一行后,返回结果数独谜题列表。

4. 访问数独谜题数据

定义几个函数来访问数独谜题的行、列和块数据。这些函数将返回一个包含所指示数据的列表。以下是这些函数的定义:

def row(i, grid):
    return grid[i]

def column(j, grid):
    return [grid[i][j] for i in range(9)]

def block(i, j, grid):
    return [grid[(i//3)*3+a][(j//3)*3+b] for a in range(3) for b in range(3)]

这些函数可以帮助我们方便地获取数独谜题的各个部分的数据,从而简化后续的逻辑处理。

5. 可能值矩阵的创建

解数独的第一步是计算每个方格的可能值集合。我们称这个为可能集。由于任何行、列或块中都不能重复值,所以可能的集合是通过从1到9的值开始,然后消除在行、列或块中其他地方出现的任何值来确定的。

def missing(lst):
    return [x for x in range(1, 10) if x not in lst]

def possible(i, j, puzzle):
    if puzzle[i][j]:
        return []
    row_set = missing(row(i, puzzle))
    col_set = missing(column(j, puzzle))
    block_set = missing(block(i, j, puzzle))
    return [x for x in row_set if x in col_set and x in block_set]

def create_possible_matrix(puzzle):
    return [[possible(i, j, puzzle) for j in range(9)] for i in range(9)]

missing 函数接受一个列表作为参数,并返回一个包含从1到9的值的列表,这些值不在参数列表中。 possible 函数计算特定(i, j)对的可能集。 create_possible_matrix 函数使用 possible 函数来创建所有可能集的矩阵。

6. 解决方法的启发式策略

如果一个可能的集合只包含一个元素,那么这个值必须是该单元格的正确值。然而,这种情况并不经常发生;这会使谜题变得太容易。还可以使用另一种模式来推断一个值。如果一个值出现在一个可能的集合中,并且在同一个块(或行或列)中的任何其他可能的集合中都没有出现,那么这个值必须是该单元格的正确答案。

下面是实现这些逻辑的代码片段:

def check_except(x, lst, pos):
    flattened = [item for sublist in lst for item in sublist]
    return x not in flattened[:pos] + flattened[pos+1:]

def solve(puzzle):
    changed = True
    while changed:
        changed = False
        possible_matrix = create_possible_matrix(puzzle)
        for i in range(9):
            for j in range(9):
                if len(possible_matrix[i][j]) == 1:
                    puzzle[i][j] = possible_matrix[i][j][0]
                    changed = True
                else:
                    for x in possible_matrix[i][j]:
                        if (check_except(x, [possible_matrix[i][k] for k in range(9)], j) or
                            check_except(x, [possible_matrix[k][j] for k in range(9)], i) or
                            check_except(x, [possible_matrix[(i//3)*3+a][(j//3)*3+b] for a in range(3) for b in range(3)], (i % 3) * 3 + (j % 3))):
                            puzzle[i][j] = x
                            changed = True
                            break
    return puzzle

表格:数独谜题的初始状态与解决后的状态

初始状态 解决后的状态
5 3 0 5 3 4
6 0 0 6 7 2
0 9 8 1 9 8

通过这些函数和逻辑,我们可以逐步填充数独谜题,直到找到解决方案。

7. 列表推导式的使用

Python中的列表推导式是一种简洁的表达方式,用于创建新的列表。通过将循环从表达式的外部移动到内部,列表推导式可以显著缩短代码并提高可读性。例如, missing 函数和 possible 函数都使用了列表推导式来简化代码。

def missing(lst):
    return [x for x in range(1, 10) if x not in lst]

def possible(i, j, puzzle):
    if puzzle[i][j]:
        return []
    row_set = missing(row(i, puzzle))
    col_set = missing(column(j, puzzle))
    block_set = missing(block(i, j, puzzle))
    return [x for x in row_set if x in col_set and x in block_set]

mermaid格式流程图:数独求解流程

graph TD;
    A[读取数独谜题] --> B[创建可能值矩阵];
    B --> C{是否存在唯一解};
    C -->|是| D[更新数独谜题];
    C -->|否| E{是否存在唯一值};
    E -->|是| F[更新数独谜题];
    E -->|否| G[继续检查];
    G --> H[检查是否完成];
    H -->|是| I[输出结果];
    H -->|否| B;

通过以上步骤,我们已经展示了如何使用Python编写一个简单的数独求解器。接下来,我们将讨论如何进一步优化和改进这个程序,以应对更复杂的数独谜题。

8. 改进与优化

虽然上述实现已经能够解决一些简单的数独谜题,但对于更复杂的谜题,我们需要引入更多的启发式方法来提高求解器的能力。以下是两种常用的优化方法:

8.1 配对测试

配对测试是一种启发式方法,它利用了数独谜题中两个单元格可能值相同的特性。如果两个单元格有一对相同的可能值,我们可以从同一块中的所有其他单元格中排除这些值。例如:

1 5 1 5

在这个例子中,我们知道1和5将分别填入这两个单元格中的一个,因此可以排除块中其他单元格的1和5。

8.2 框线技术

框线技术是一种“必须”的方向移动方法。假设我们通过检查其他行或列中的值知道某个数字必须出现在某一行或某一列中,那么我们可以从其他行或列中排除这个值。例如:

9

如果我们知道9必须出现在第三列中,那么我们可以从其他列中排除9,从而简化问题。

实现配对测试和框线技术

以下是实现这两种技术的具体代码:

def pair_test(puzzle, possible_matrix):
    for i in range(9):
        for j in range(9):
            if len(possible_matrix[i][j]) == 2:
                value_pair = possible_matrix[i][j]
                for a in range(3):
                    for b in range(3):
                        if (i // 3) * 3 + a != i and (j // 3) * 3 + b != j:
                            if set(value_pair).issubset(set(possible_matrix[(i // 3) * 3 + a][(j // 3) * 3 + b])):
                                for k in range(9):
                                    if k != j and possible_matrix[i][k] != value_pair:
                                        possible_matrix[i][k] = [x for x in possible_matrix[i][k] if x not in value_pair]
                                    if k != i and possible_matrix[k][j] != value_pair:
                                        possible_matrix[k][j] = [x for x in possible_matrix[k][j] if x not in value_pair]
                                return True
    return False

def box_line_technique(puzzle, possible_matrix):
    for i in range(9):
        for j in range(9):
            for value in possible_matrix[i][j]:
                if all(value not in possible_matrix[i][k] for k in range(9) if k != j) or \
                   all(value not in possible_matrix[k][j] for k in range(9) if k != i) or \
                   all(value not in possible_matrix[(i // 3) * 3 + a][(j // 3) * 3 + b] for a in range(3) for b in range(3) if (i // 3) * 3 + a != i and (j // 3) * 3 + b != j):
                    puzzle[i][j] = value
                    return True
    return False

mermaid格式流程图:改进后的数独求解流程

graph TD;
    A[读取数独谜题] --> B[创建可能值矩阵];
    B --> C{是否存在唯一解};
    C -->|是| D[更新数独谜题];
    C -->|否| E{是否存在唯一值};
    E -->|是| F[更新数独谜题];
    E -->|否| G[配对测试];
    G --> H{配对测试成功};
    H -->|是| D;
    H -->|否| I[框线技术];
    I --> J{框线技术成功};
    J -->|是| D;
    J -->|否| K[继续检查];
    K --> L[检查是否完成];
    L -->|是| M[输出结果];
    L -->|否| B;

9. 函数式编程的优势

通过列表推导式表示的函数式程序在Python中具有显著优势。函数通常较短,比它们的传统替代方法更快,且易于阅读。此外,在开发列表推导式时所犯的错误往往会产生重大影响。大错误比小错误更容易发现,因此,一个导致几乎每个输入都产生错误结果的改变很可能会被非常快速地发现。

例如, missing 函数和 possible 函数都使用了列表推导式,使得代码更加简洁和易读:

def missing(lst):
    return [x for x in range(1, 10) if x not in lst]

def possible(i, j, puzzle):
    if puzzle[i][j]:
        return []
    row_set = missing(row(i, puzzle))
    col_set = missing(column(j, puzzle))
    block_set = missing(block(i, j, puzzle))
    return [x for x in row_set if x in col_set and x in block_set]

10. 自底向上的开发方法

本章描述的解决方案方法采用了自底向上的开发方法。我们从数据的表示开始,逐步构建解决问题的逻辑。这种方法使得程序结构更加清晰,易于理解和维护。与自顶向下的方法相比,自底向上的方法更注重底层细节,逐步构建高层逻辑。

列表:自底向上开发方法的优点

  • 清晰性 :从基础功能开始,逐步构建复杂逻辑,使得代码更加清晰。
  • 可维护性 :每个功能模块相对独立,便于后续维护和扩展。
  • 可靠性 :逐步验证每个模块的正确性,减少整体程序的错误率。

11. 总结与展望

通过以上的讨论,我们已经展示了如何使用Python编写一个数独求解器。从数独谜题的表示,到可能值矩阵的创建,再到解决方法的启发式策略,每个步骤都经过了详细的解释和代码实现。此外,我们还介绍了如何通过配对测试和框线技术来优化程序,以应对更复杂的数独谜题。

表格:数独求解器的性能对比

方法 简单谜题 复杂谜题
基础方法 ✔️
加入配对测试 ✔️ ✔️
加入框线技术 ✔️ ✔️

通过这些改进,数独求解器能够处理更广泛的谜题类型,提高了程序的实用性和鲁棒性。希望这篇文章能够帮助你更好地理解数独求解器的工作原理,并激发你对Python编程的兴趣。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值