数独

问题

数独是在一个9*9宫格中,保证每行每列以及九个九宫格中填入的1-9的数字不出现重复。通过穷举法实现求解。

程序

import tkinter as tk
from tkinter import ttk, messagebox
import time
import threading

class SudokuSolver:
    def __init__(self, root):
        self.root = root
        self.root.title("数独求解器 - 穷举法")
        self.root.geometry("600x700")
        
        # 初始化数独网格
        self.board = [[0 for _ in range(9)] for _ in range(9)]
        self.initial_board = [[0 for _ in range(9)] for _ in range(9)]  # 存储初始题目
        self.cells = [[None for _ in range(9)] for _ in range(9)]
        self.selected_cell = None
        self.solving = False
        self.speed = 100  # 默认速度(毫秒)
        self.algorithm_time = 0  # 纯算法时间
        self.step_count = 0  # 步骤计数器
        
        # 初始化组件变量
        self.speed_label = None
        self.speed_scale = None
        self.status_label = None
        self.stats_label = None
        
        self.create_widgets()
        
    def create_widgets(self):
        # 主框架
        main_frame = ttk.Frame(self.root, padding="10")
        main_frame.pack(fill=tk.BOTH, expand=True)
        
        # 标题
        title_label = ttk.Label(main_frame, text="数独求解器 - 穷举回溯法", 
                               font=("Arial", 16, "bold"))
        title_label.pack(pady=10)
        
        # 数独网格框架
        grid_frame = ttk.Frame(main_frame)
        grid_frame.pack(pady=20)
        
        # 创建9x9网格
        self.create_grid(grid_frame)
        
        # 控制面板
        control_frame = ttk.Frame(main_frame)
        control_frame.pack(pady=20)
        
        # 速度控制
        speed_frame = ttk.Frame(control_frame)
        speed_frame.pack(pady=10)
        
        ttk.Label(speed_frame, text="演示速度:").pack(side=tk.LEFT)
        self.speed_scale = ttk.Scale(speed_frame, from_=10, to=1000, 
                                   orient=tk.HORIZONTAL, length=200,
                                   command=self.update_speed)
        self.speed_scale.set(100)
        self.speed_scale.pack(side=tk.LEFT, padx=10)
        
        self.speed_label = ttk.Label(speed_frame, text="100 ms")
        self.speed_label.pack(side=tk.LEFT)
        
        # 按钮
        button_frame = ttk.Frame(control_frame)
        button_frame.pack(pady=10)
        
        ttk.Button(button_frame, text="开始求解", 
                  command=self.start_solving).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="直接作答", 
                  command=self.direct_solve).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="重置", 
                  command=self.reset_board).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="清空", 
                  command=self.clear_board).pack(side=tk.LEFT, padx=5)
        ttk.Button(button_frame, text="示例", 
                  command=self.load_example).pack(side=tk.LEFT, padx=5)
        
        # 状态显示
        self.status_label = ttk.Label(main_frame, text="请点击格子并输入数字1-9", 
                                     font=("Arial", 10))
        self.status_label.pack(pady=10)
        
        # 统计信息
        self.stats_label = ttk.Label(main_frame, text="步骤: 0 | 算法时间: 0.00s", 
                                    font=("Arial", 10))
        self.stats_label.pack(pady=5)
        
        # 操作说明
        instruction_text = """操作说明:
        1. 点击格子,然后按数字键1-9填入数字
        2. 按0或Delete键清空格子
        3. 调整速度滑块控制演示速度
        4. 点击"开始求解"观看穷举求解过程
        5. 点击"直接作答"直接获得答案
        6. 紫色格子表示初始题目,绿色表示尝试的数字,红色表示回溯"""
        
        instruction_label = ttk.Label(main_frame, text=instruction_text, 
                                     justify=tk.LEFT, font=("Arial", 9))
        instruction_label.pack(pady=10)
    
    def create_grid(self, parent):
        # 创建9x9网格,每个3x3区域有更粗的边框
        for i in range(9):
            for j in range(9):
                # 确定边框宽度 - 为3x3区域边界设置更粗的边框
                border_width = 1
                if i % 3 == 0 and i != 0:
                    border_width = 3
                if j % 3 == 0 and j != 0:
                    border_width = 3
                
                # 为外边框设置更粗的边框
                if i == 0 or i == 8:
                    border_width = 3
                if j == 0 or j == 8:
                    border_width = 3
                
                cell = tk.Entry(parent, width=3, font=("Arial", 16), 
                               justify=tk.CENTER, relief="solid",
                               borderwidth=border_width)
                cell.grid(row=i, column=j, padx=(1, 1), pady=(1, 1), ipady=5)
                
                # 绑定事件
                cell.bind("<Button-1>", lambda e, row=i, col=j: self.select_cell(row, col))
                cell.bind("<Key>", lambda e, row=i, col=j: self.on_key_press(e, row, col))
                cell.bind("<FocusIn>", lambda e, row=i, col=j: self.select_cell(row, col))
                
                self.cells[i][j] = cell
    
    def select_cell(self, row, col):
        # 取消之前选中格子的高亮
        if self.selected_cell:
            old_row, old_col = self.selected_cell
            self.cells[old_row][old_col].config(bg=self.get_cell_color(old_row, old_col))
        
        # 高亮当前选中的格子
        self.selected_cell = (row, col)
        self.cells[row][col].config(bg="#e0e0ff")
        self.cells[row][col].focus_set()
        
        self.status_label.config(text=f"选中格子: ({row+1}, {col+1})")
    
    def get_cell_color(self, row, col):
        """根据格子内容返回对应的背景颜色"""
        if self.initial_board[row][col] != 0:
            return "#e0c0ff"  # 初始题目 - 紫色
        else:
            return "white"  # 空白格子 - 白色
    
    def on_key_press(self, event, row, col):
        if event.char in '123456789':
            self.set_cell_value(row, col, int(event.char))
        elif event.keysym in ['Delete', 'BackSpace'] or event.char == '0':
            self.set_cell_value(row, col, 0)
        
        # 阻止默认行为
        return "break"
    
    def set_cell_value(self, row, col, value):
        self.board[row][col] = value
        if value == 0:
            self.cells[row][col].delete(0, tk.END)
        else:
            self.cells[row][col].delete(0, tk.END)
            self.cells[row][col].insert(0, str(value))
        
        # 更新格子颜色
        self.cells[row][col].config(bg=self.get_cell_color(row, col))
    
    def update_speed(self, value):
        self.speed = int(float(value))
        # 确保 speed_label 已创建
        if hasattr(self, 'speed_label') and self.speed_label is not None:
            self.speed_label.config(text=f"{self.speed} ms")
    
    def reset_board(self):
        for i in range(9):
            for j in range(9):
                self.set_cell_value(i, j, self.board[i][j])
        self.status_label.config(text="棋盘已重置")
    
    def clear_board(self):
        for i in range(9):
            for j in range(9):
                self.set_cell_value(i, j, 0)
                self.initial_board[i][j] = 0  # 同时清空初始题目
        self.status_label.config(text="棋盘已清空")
        self.step_count = 0
        self.algorithm_time = 0
        self.update_stats()
    
    def load_example(self):
        # 加载一个示例数独
        example = [
            [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]
        ]
        
        for i in range(9):
            for j in range(9):
                self.set_cell_value(i, j, example[i][j])
                self.initial_board[i][j] = example[i][j]  # 保存初始题目
        
        self.status_label.config(text="已加载示例数独")
    
    def start_solving(self):
        if self.solving:
            return
        
        # 保存当前棋盘状态作为初始题目
        for i in range(9):
            for j in range(9):
                if self.board[i][j] != 0:
                    self.initial_board[i][j] = self.board[i][j]
                # 更新格子颜色为初始题目颜色
                self.cells[i][j].config(bg=self.get_cell_color(i, j))
            
        # 在新线程中运行求解过程,避免阻塞UI
        thread = threading.Thread(target=self.solve_thread)
        thread.daemon = True
        thread.start()
    
    def direct_solve(self):
        """直接求解,不演示中间过程"""
        if self.solving:
            return
        
        # 保存当前棋盘状态作为初始题目
        for i in range(9):
            for j in range(9):
                if self.board[i][j] != 0:
                    self.initial_board[i][j] = self.board[i][j]
            
        # 在新线程中运行直接求解过程
        thread = threading.Thread(target=self.direct_solve_thread)
        thread.daemon = True
        thread.start()
    
    def solve_thread(self):
        """演示求解线程"""
        self.solving = True
        self.status_label.config(text="开始求解...")
        self.step_count = 0
        self.algorithm_time = 0
        
        # 禁用控制按钮
        self.toggle_buttons(False)
        
        # 开始求解
        start_time = time.time()
        solved = self.solve_sudoku()
        end_time = time.time()
        
        self.solving = False
        
        # 启用控制按钮
        self.toggle_buttons(True)
        
        total_time = end_time - start_time
        if solved:
            self.status_label.config(text=f"求解完成! 总用时: {total_time:.2f}秒")
        else:
            self.status_label.config(text="无解!")
    
    def direct_solve_thread(self):
        """直接求解线程"""
        self.solving = True
        self.status_label.config(text="直接求解中...")
        
        # 禁用控制按钮
        self.toggle_buttons(False)
        
        # 开始直接求解
        start_time = time.time()
        solved = self.solve_sudoku_direct()
        end_time = time.time()
        
        self.solving = False
        
        # 启用控制按钮
        self.toggle_buttons(True)
        
        total_time = end_time - start_time
        if solved:
            # 更新UI显示最终结果
            self.update_final_board()
            self.status_label.config(text=f"直接求解完成! 用时: {total_time:.2f}秒")
        else:
            self.status_label.config(text="无解!")
    
    def solve_sudoku_direct(self):
        """直接求解数独,不更新UI,不延迟"""
        # 创建棋盘副本
        board_copy = [row[:] for row in self.board]
        
        # 直接求解
        solved = self.solve_direct(board_copy)
        
        # 如果求解成功,更新棋盘
        if solved:
            self.board = board_copy
        return solved
    
    def solve_direct(self, board):
        """直接求解数独的递归函数"""
        empty_cell = self.find_empty_cell_direct(board)
        if not empty_cell:
            return True  # 没有空格子,求解完成
        
        row, col = empty_cell
        
        for num in range(1, 10):
            if self.is_valid_direct(board, row, col, num):
                # 尝试填入数字
                board[row][col] = num
                
                # 递归求解
                if self.solve_direct(board):
                    return True
                
                # 回溯
                board[row][col] = 0
        
        return False
    
    def find_empty_cell_direct(self, board):
        """在直接求解模式下查找空格子"""
        for i in range(9):
            for j in range(9):
                if board[i][j] == 0:
                    return (i, j)
        return None
    
    def is_valid_direct(self, board, row, col, num):
        """在直接求解模式下检查数字是否有效"""
        # 检查行
        for i in range(9):
            if board[row][i] == num:
                return False
        
        # 检查列
        for i in range(9):
            if board[i][col] == num:
                return False
        
        # 检查3x3宫格
        start_row, start_col = 3 * (row // 3), 3 * (col // 3)
        for i in range(3):
            for j in range(3):
                if board[start_row + i][start_col + j] == num:
                    return False
        
        return True
    
    def update_final_board(self):
        """更新UI显示最终结果"""
        for i in range(9):
            for j in range(9):
                value = self.board[i][j]
                if value != 0:
                    self.cells[i][j].delete(0, tk.END)
                    self.cells[i][j].insert(0, str(value))
                    # 如果是算法填入的数字,显示为浅蓝色
                    if self.initial_board[i][j] == 0:
                        self.cells[i][j].config(bg="#c0e0ff")
    
    def toggle_buttons(self, state):
        state_str = "normal" if state else "disabled"
        for widget in self.root.winfo_children():
            for child in widget.winfo_children():
                if isinstance(child, ttk.Frame):
                    for button in child.winfo_children():
                        if isinstance(button, ttk.Button):
                            button.config(state=state_str)
    
    def is_valid(self, board, row, col, num):
        # 检查行
        for i in range(9):
            if board[row][i] == num:
                return False
        
        # 检查列
        for i in range(9):
            if board[i][col] == num:
                return False
        
        # 检查3x3宫格
        start_row, start_col = 3 * (row // 3), 3 * (col // 3)
        for i in range(3):
            for j in range(3):
                if board[start_row + i][start_col + j] == num:
                    return False
        
        return True
    
    def solve_sudoku(self):
        # 使用回溯法求解数独
        empty_cell = self.find_empty_cell()
        if not empty_cell:
            return True  # 没有空格子,求解完成
        
        row, col = empty_cell
        
        for num in range(1, 10):
            # 记录算法开始时间
            algo_start = time.time()
            
            if self.is_valid(self.board, row, col, num):
                # 尝试填入数字
                self.board[row][col] = num
                self.step_count += 1
                
                # 更新UI - 确保格子显示为绿色
                self.root.after(0, lambda r=row, c=col, v=num: self.update_cell_ui(r, c, v, "try"))
                
                # 记录算法结束时间
                algo_end = time.time()
                self.algorithm_time += (algo_end - algo_start)
                self.update_stats()
                
                # 延迟用于演示
                time.sleep(self.speed / 1000.0)
                
                # 递归求解
                if self.solve_sudoku():
                    return True
                
                # 回溯
                algo_start = time.time()
                self.board[row][col] = 0
                self.step_count += 1
                
                # 更新UI
                self.root.after(0, lambda r=row, c=col, v=0: self.update_cell_ui(r, c, v, "backtrack"))
                
                # 记录算法结束时间
                algo_end = time.time()
                self.algorithm_time += (algo_end - algo_start)
                self.update_stats()
                
                # 延迟用于演示
                time.sleep(self.speed / 1000.0)
            else:
                # 即使数字无效,也记录算法时间
                algo_end = time.time()
                self.algorithm_time += (algo_end - algo_start)
                self.update_stats()
        
        return False
    
    def find_empty_cell(self):
        for i in range(9):
            for j in range(9):
                if self.board[i][j] == 0:
                    return (i, j)
        return None
    
    def update_cell_ui(self, row, col, value, action_type):
        # 如果是初始题目格子,保持紫色,不改变
        if self.initial_board[row][col] != 0:
            return
            
        if value == 0:
            self.cells[row][col].delete(0, tk.END)
            if action_type == "backtrack":
                self.cells[row][col].config(bg="#ffcccc")  # 回溯时显示红色
                # 立即更新UI
                self.root.update_idletasks()
                self.root.update()
        else:
            self.cells[row][col].delete(0, tk.END)
            self.cells[row][col].insert(0, str(value))
            if action_type == "try":
                self.cells[row][col].config(bg="#ccffcc")  # 尝试时显示绿色
                # 立即更新UI
                self.root.update_idletasks()
                self.root.update()
    
    def update_stats(self):
        if hasattr(self, 'stats_label') and self.stats_label is not None:
            self.root.after(0, lambda: self.stats_label.config(
                text=f"步骤: {self.step_count} | 算法时间: {self.algorithm_time:.3f}s"))

def main():
    root = tk.Tk()
    app = SudokuSolver(root)
    root.mainloop()

if __name__ == "__main__":
    main()

执行结果

数独求解

image-20251102170427575