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
self.size = 0
class DLX:
"""Dancing Links 算法实现"""
def __init__(self, num_columns):
self.num_columns = num_columns
self.header = DLXNode(col_idx=-1)
self.columns = []
self.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:
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:
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
c = None
min_size = float('inf')
current_col = self.header.R
while current_col != self.header:
if current_col.size < min_size:
min_size = current_col.size
c = current_col
current_col = current_col.R
if c is None or c.size == 0:
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
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:
def __init__(self, board_input):
self.initial_board = [row[:] for row in board_input]
self.size = 9
self.box_size = 3
self.dlx = DLX(self.size * self.size * 4)
self.row_candidates_map = {}
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):
if self.initial_board[r][c] == 0 or self.initial_board[r][c] == val_candidate:
col_idx_cell = r * self.size + c
col_idx_row = (self.size * self.size) + (r * self.size) + (val_candidate - 1)
col_idx_col = (self.size * self.size * 2) + (c * self.size) + (val_candidate - 1)
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()
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
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]:
return None
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)]
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
validate_cmd = (frame.register(self.validate_input), '%P')
entry.config(validate="key", validatecommand=validate_cmd)
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()
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")
elif initial_board[r][c] != 0:
self.entries[r][c].config(fg="black")
else:
self.entries[r][c].config(fg="black")
def solve_sudoku(self):
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:
return
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()
solver = SudokuDLXSolver(board)
solution = solver.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.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")
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()

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