dance links算法 解决数独问题 python实现

class DLXNode:
    """Dancing Links 节点类"""

    def __init__(self, row_idx=-1, col_idx=-1):
        self.L = self.R = self.U = self.D = self
        self.col_header = self
        self.row_idx = row_idx
        self.col_idx = col_idx  # Only meaningful for column headers
        self.size = 0  # Only for column headers: number of 1s in column


class DLX:
    """Dancing Links 算法实现"""

    def __init__(self, num_columns):
        self.num_columns = num_columns
        self.header = DLXNode(col_idx=-1)  # Main header
        self.columns = []  # List of column header nodes
        self.solution = []  # Stores row indices of the solution

        for j in range(num_columns):
            col_node = DLXNode(col_idx=j)
            self.columns.append(col_node)
            col_node.L = self.header.L
            col_node.R = self.header
            self.header.L.R = col_node
            self.header.L = col_node

    def add_row(self, row_elements_indices, row_idx):
        first_node_in_row = None
        for col_idx in row_elements_indices:
            col_header_node = self.columns[col_idx]
            col_header_node.size += 1

            new_node = DLXNode(row_idx=row_idx)
            new_node.col_header = col_header_node

            new_node.U = col_header_node.U
            new_node.D = col_header_node
            col_header_node.U.D = new_node
            col_header_node.U = new_node

            if first_node_in_row is None:
                first_node_in_row = new_node
            else:
                new_node.L = first_node_in_row.L
                new_node.R = first_node_in_row
                first_node_in_row.L.R = new_node
                first_node_in_row.L = new_node
        return first_node_in_row

    def _cover(self, target_col_header):
        target_col_header.R.L = target_col_header.L
        target_col_header.L.R = target_col_header.R

        i_node = target_col_header.D
        while i_node != target_col_header:
            j_node = i_node.R
            while j_node != i_node:
                j_node.D.U = j_node.U
                j_node.U.D = j_node.D
                if j_node.col_header:  # Check to prevent errors if list is malformed
                    j_node.col_header.size -= 1
                j_node = j_node.R
            i_node = i_node.D

    def _uncover(self, target_col_header):
        i_node = target_col_header.U
        while i_node != target_col_header:
            j_node = i_node.L
            while j_node != i_node:
                if j_node.col_header:  # Check
                    j_node.col_header.size += 1
                j_node.D.U = j_node
                j_node.U.D = j_node
                j_node = j_node.L
            i_node = i_node.U

        target_col_header.R.L = target_col_header
        target_col_header.L.R = target_col_header

    def search(self):
        if self.header.R == self.header:
            return True  # Solution found

        c = None
        min_size = float('inf')
        current_col = self.header.R
        while current_col != self.header:
            if current_col.size < min_size:  # S-heuristic
                min_size = current_col.size
                c = current_col
            current_col = current_col.R

        if c is None or c.size == 0:  # Pruning if a column is empty and not covered
            return False

        self._cover(c)

        r_node = c.D
        while r_node != c:
            self.solution.append(r_node.row_idx)

            j_node = r_node.R
            while j_node != r_node:
                self._cover(j_node.col_header)
                j_node = j_node.R

            if self.search():
                return True

            # Backtrack
            j_node = r_node.L
            while j_node != r_node:
                self._uncover(j_node.col_header)
                j_node = j_node.L

            self.solution.pop()
            r_node = r_node.D

        self._uncover(c)
        return False


class SudokuDLXSolver:  # Renamed to avoid conflict if user runs previous script separately
    def __init__(self, board_input):
        self.initial_board = [row[:] for row in board_input]  # Store a copy
        self.size = 9
        self.box_size = 3
        self.dlx = DLX(self.size * self.size * 4)  # 324 columns
        self.row_candidates_map = {}  # Maps DLX row index to (r, c, val)

    def _build_exact_cover_matrix(self):
        dlx_row_idx = 0
        for r in range(self.size):
            for c in range(self.size):
                for val_candidate in range(1, self.size + 1):
                    # Only consider this candidate if the cell is empty OR matches a pre-filled number
                    if self.initial_board[r][c] == 0 or self.initial_board[r][c] == val_candidate:
                        # Constraint columns
                        # 1. Cell constraint: (r, c) is filled (cols 0-80)
                        col_idx_cell = r * self.size + c
                        # 2. Row constraint: row r has digit val_candidate (cols 81-161)
                        col_idx_row = (self.size * self.size) + (r * self.size) + (val_candidate - 1)
                        # 3. Column constraint: col c has digit val_candidate (cols 162-242)
                        col_idx_col = (self.size * self.size * 2) + (c * self.size) + (val_candidate - 1)
                        # 4. Box constraint: box_idx has digit val_candidate (cols 243-323)
                        box_r, box_c = r // self.box_size, c // self.box_size
                        box_idx = box_r * self.box_size + box_c
                        col_idx_box = (self.size * self.size * 3) + (box_idx * self.size) + (val_candidate - 1)

                        current_dlx_row_elements = [col_idx_cell, col_idx_row, col_idx_col, col_idx_box]

                        self.dlx.add_row(current_dlx_row_elements, dlx_row_idx)
                        self.row_candidates_map[dlx_row_idx] = (r, c, val_candidate)
                        dlx_row_idx += 1

    def solve(self):
        self._build_exact_cover_matrix()

        # Handle pre-filled cells by "pre-covering" relevant items in DLX
        # This is implicitly handled by only adding valid rows in _build_exact_cover_matrix
        # and the DLX search mechanism. If a cell (r,c) has a value 'V',
        # then only the DLX row corresponding to (r,c,V) is added for that cell's position constraint.
        # The other constraints (row, col, box) for 'V' will then need to be satisfied.

        if self.dlx.search():
            solution_board = [[0 for _ in range(self.size)] for _ in range(self.size)]
            for row_idx in self.dlx.solution:
                r, c, val = self.row_candidates_map[row_idx]
                solution_board[r][c] = val

            # Verify if the solution is complete and consistent with initial board
            for r_init in range(self.size):
                for c_init in range(self.size):
                    if self.initial_board[r_init][c_init] != 0 and \
                            self.initial_board[r_init][c_init] != solution_board[r_init][c_init]:
                        # This should not happen if logic is correct, but as a safeguard
                        return None  # Inconsistent solution
            return solution_board
        else:
            return None


import tkinter as tk
from tkinter import messagebox


class SudokuGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("Sudoku Solver (DLX)")

        self.cells = [[tk.StringVar() for _ in range(9)] for _ in range(9)]
        self.entries = [[None for _ in range(9)] for _ in range(9)]

        # Styling for 3x3 blocks
        self.frames = [[tk.Frame(self.root, borderwidth=1, relief="solid")
                        for _ in range(3)] for _ in range(3)]

        for r_block in range(3):
            for c_block in range(3):
                frame = self.frames[r_block][c_block]
                frame.grid(row=r_block, column=c_block, padx=1, pady=1, sticky="nsew")
                for r_in_block in range(3):
                    for c_in_block in range(3):
                        r = r_block * 3 + r_in_block
                        c = c_block * 3 + c_in_block

                        entry = tk.Entry(frame, textvariable=self.cells[r][c],
                                         width=2, font=('Arial', 18, 'bold'), justify='center',
                                         borderwidth=1, relief="solid")
                        entry.grid(row=r_in_block, column=c_in_block, padx=1, pady=1, ipady=5, sticky="nsew")
                        self.entries[r][c] = entry
                        # Basic validation to allow only digits 1-9 or empty
                        validate_cmd = (frame.register(self.validate_input), '%P')
                        entry.config(validate="key", validatecommand=validate_cmd)

        # --- Control Buttons ---
        button_frame = tk.Frame(self.root)
        button_frame.grid(row=3, column=0, columnspan=3, pady=10)

        solve_button = tk.Button(button_frame, text="Solve", command=self.solve_sudoku, font=('Arial', 12))
        solve_button.pack(side=tk.LEFT, padx=10)

        clear_button = tk.Button(button_frame, text="Clear", command=self.clear_board, font=('Arial', 12))
        clear_button.pack(side=tk.LEFT, padx=10)

        example_button = tk.Button(button_frame, text="Example", command=self.load_example, font=('Arial', 12))
        example_button.pack(side=tk.LEFT, padx=10)

    def validate_input(self, P):
        """Allow only empty string or a single digit from 1-9."""
        if P == "" or (P.isdigit() and len(P) == 1 and P != '0'):
            return True
        return False

    def get_board_from_ui(self):
        board = [[0 for _ in range(9)] for _ in range(9)]
        try:
            for r in range(9):
                for c in range(9):
                    val_str = self.cells[r][c].get()
                    if val_str:
                        val_int = int(val_str)
                        if not (1 <= val_int <= 9):
                            messagebox.showerror("Input Error",
                                                 f"Invalid number {val_int} at row {r + 1}, col {c + 1}. Only 1-9.")
                            return None
                        board[r][c] = val_int
                    else:
                        board[r][c] = 0
        except ValueError:
            messagebox.showerror("Input Error", "Please enter only numbers (1-9) or leave cells empty.")
            return None
        return board

    def display_board(self, board_data, color_solved=True):
        if board_data is None:
            messagebox.showinfo("No Solution", "This Sudoku puzzle has no solution.")
            return

        initial_board = self.get_board_from_ui()  # Get what was initially there for coloring

        for r in range(9):
            for c in range(9):
                self.cells[r][c].set(str(board_data[r][c]) if board_data[r][c] != 0 else "")
                if color_solved and initial_board[r][c] == 0 and board_data[r][c] != 0:
                    self.entries[r][c].config(fg="blue")  # Color newly solved cells
                elif initial_board[r][c] != 0:
                    self.entries[r][c].config(fg="black")  # Original numbers
                else:
                    self.entries[r][c].config(fg="black")

    def solve_sudoku(self):
        # Reset foreground colors
        for r in range(9):
            for c in range(9):
                self.entries[r][c].config(fg="black")

        board = self.get_board_from_ui()
        if board is None:  # Input error occurred
            return

        # Disable buttons during solve
        for widget in self.root.winfo_children():
            if isinstance(widget, tk.Frame):
                for sub_widget in widget.winfo_children():
                    if isinstance(sub_widget, tk.Button):
                        sub_widget.config(state=tk.DISABLED)
        self.root.update_idletasks()  # Process pending events

        solver = SudokuDLXSolver(board)
        solution = solver.solve()

        # Re-enable buttons
        for widget in self.root.winfo_children():
            if isinstance(widget, tk.Frame):
                for sub_widget in widget.winfo_children():
                    if isinstance(sub_widget, tk.Button):
                        sub_widget.config(state=tk.NORMAL)

        if solution:
            self.display_board(solution)
            messagebox.showinfo("Success", "Sudoku Solved!")
        else:
            messagebox.showinfo("No Solution", "Could not find a solution for this Sudoku puzzle.")

    def clear_board(self):
        for r in range(9):
            for c in range(9):
                self.cells[r][c].set("")
                self.entries[r][c].config(fg="black")  # Reset color

    def load_example(self):
        self.clear_board()
        example_board = [
            [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 r in range(9):
            for c in range(9):
                if example_board[r][c] != 0:
                    self.cells[r][c].set(str(example_board[r][c]))


if __name__ == "__main__":
    main_root = tk.Tk()
    app = SudokuGUI(main_root)
    main_root.mainloop()


在这里插入图片描述

后续优化

  1. 增加输入识别格式,支持识别图片快速得到解
  2. 或者根据latex格式的矩阵输入
  3. 或者前面两者结合(调用mathpix api或者其他latex 图片转公式api)
  4. 增加探索路径步数与求解可视化
  5. 增加批量图片文件处理
  6. 增加其余相关算法的实现与比较
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值