数独求解器3.0 增加latex格式读取

首先说明两种读入格式

latex输入格式说明

LaTeX 复制代码
\documentclass{article}
\begin{document}

This is some text before
```oku.

\begin{array}{|l|l|l|l|l|l|l|l|l|}
\hline
```&   &   &   & 5 &   & 2 & 9 \\
\hline
  &   & 5 &
```1 &   & 7 &   \\ % A comment here
\hline
  &   & 3 &
```& 8 &   &   \\
\hline
  & 5 & 2 &   &   &   &
```\\
\hline
  &   &   & 5 & 7 & 3 &   &   & 8 \\
```ine
3 &   &   &   &   &   &   & 1 & 5 \\
\hline
2 &
```& 5 & 7 &   &   &   \\
\hline
  &   &   & 6 & 9
```& 3 & 7 \\
\hline
  & 3 &   & 8 &   &   &
```\\
\hline
\end{array}

Or using tabular:

\begin{tabular}{|c|c|c
```|c|c|}
\hline
5&3& & &7& & & & \\
\hline
```& &1&9&5& & & \\
\hline
&9&8& & & & &6& \\
```ine
8& & & &6& & & &3\\
\hline
4& & &8& &3& & &
```\hline
7& & & &2& & & &6\\
\hline
&6& & & &
```\
\hline
& & &4&1&9& & &5\\
\hline
&
```& &7&9\\
\hline
\end{tabular}

Some text after.

\end
```t}

然后是csv读入格式

csv:

文件内容应该是9行,每行包含9个数字(1-9代表预填数字,0或空单元格代表空格),用逗号分隔。

python 复制代码
5,3,0,0
```6,0,0,1,9,5,0,0,0
0
```,6,0
8,0,0,0,6,0,0,0,3
```,0,8,0,3,0,0,1
7,0,0,0,2,0,0
```0,6,0,0,0,0,2,8,0
0,0,0,4,1,
```0,0,0,0,8,0,0,7,9
python 复制代码
import tkinter as tk
from tkinter import messagebox, filedialog
import csv
import time
import re  # 导入re模块,用于正则表达式解析LaTeX


# DLXNode, DLX, SudokuDLXSolver 类的代码保持不变,此处省略以保持简洁
# ... (粘贴之前的 DLXNode, DLX, SudokuDLXSolver 代码)
class DLXNode:
    """Dancing Links 节点类"""

    def __init__(self, row_idx=-1, col_idx=-1):
        self.L = self
        self.R = self
        self.U = self
        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 = []
        self.search_steps = 0
        self.gui_update_callback = None
        self.row_candidates_map = None

        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):
        self.search_steps += 1

        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)
            if self.gui_update_callback and self.row_candidates_map:
                self.gui_update_callback(r_node.row_idx, 'add', self.row_candidates_map)

            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

            popped_row_idx = self.solution.pop()
            if self.gui_update_callback and self.row_candidates_map:
                self.gui_update_callback(popped_row_idx, 'remove', self.row_candidates_map)

            j_node = r_node.L
            while j_node != r_node:
                self._uncover(j_node.col_header)
                j_node = j_node.L
            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, gui_update_callback=None):
        self._build_exact_cover_matrix()

        if gui_update_callback:
            self.dlx.gui_update_callback = gui_update_callback
            self.dlx.row_candidates_map = self.row_candidates_map

        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


class SudokuGUI:
    def __init__(self, root):
        self.root = root
        self.root.title("数独求解器 (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.initial_fill = [[False 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="求解", command=self.solve_sudoku, font=('Arial', 12))
        solve_button.pack(side=tk.LEFT, padx=5)

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

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

        csv_button = tk.Button(button_frame, text="从CSV加载", command=self.load_from_csv, font=('Arial', 12))
        csv_button.pack(side=tk.LEFT, padx=5)

        # --- 新增: 从LaTeX加载按钮 ---
        latex_button = tk.Button(button_frame, text="从LaTeX加载", command=self.load_from_latex, font=('Arial', 12))
        latex_button.pack(side=tk.LEFT, padx=5)  # 将新按钮添加到界面

        info_frame = tk.Frame(self.root)
        info_frame.grid(row=4, column=0, columnspan=3, pady=5)
        self.steps_label_var = tk.StringVar()
        self.steps_label_var.set("探索步数: 0")
        steps_display_label = tk.Label(info_frame, textvariable=self.steps_label_var, font=('Arial', 10))
        steps_display_label.pack()

        self.visualization_delay = 0.005

    def validate_input(self, P):
        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)]
        self.initial_fill = [[False 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("输入错误",
                                                 f"无效数字 {val_int} 在行 {r + 1}, 列 {c + 1}。只能是1-9。")
                            return None
                        board[r][c] = val_int
                        self.initial_fill[r][c] = True
                    else:
                        board[r][c] = 0
        except ValueError:
            messagebox.showerror("输入错误", "请输入数字 (1-9) 或留空。")
            return None
        return board

    def display_board(self, board_data, solved_color="blue", initial_color="black"):
        if board_data is None:
            return

        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 self.initial_fill[r][c]:
                    self.entries[r][c].config(fg=initial_color)
                elif board_data[r][c] != 0:
                    self.entries[r][c].config(fg=solved_color)
                else:
                    self.entries[r][c].config(fg=initial_color)

    def _gui_step_update(self, dlx_row_idx, action, row_candidates_map_ref):
        if not row_candidates_map_ref or dlx_row_idx not in row_candidates_map_ref:
            return

        r, c, val = row_candidates_map_ref[dlx_row_idx]

        if self.initial_fill[r][c]:
            return

        if action == 'add':
            self.cells[r][c].set(str(val))
            self.entries[r][c].config(fg="orange")
        elif action == 'remove':
            self.cells[r][c].set("")
            self.entries[r][c].config(fg="black")

        self.root.update_idletasks()
        if self.visualization_delay > 0:
            time.sleep(self.visualization_delay)

    def solve_sudoku(self):
        self.steps_label_var.set("探索步数: 0")
        # 在获取棋盘前,先记录一次初始填充状态,确保solve内部的display_board能正确区分
        # current_ui_board_for_initial_fill = self.get_board_from_ui() # 这会重置initial_fill,不好
        # 所以 get_board_from_ui 内部必须正确设置 initial_fill

        board = self.get_board_from_ui()  # 获取棋盘,此方法内部会更新 self.initial_fill
        if board is None:
            return

        # 清理之前解出的(非初始)数字的颜色和内容,为可视化做准备
        for r in range(9):
            for c in range(9):
                if not self.initial_fill[r][c]:  # 只处理非初始数字
                    self.cells[r][c].set("")  # 清空内容,以便可视化"填入"的过程
                    self.entries[r][c].config(fg="black")  # 恢复默认颜色

        all_buttons = []
        button_container = None
        for child in self.root.winfo_children():
            if isinstance(child, tk.Frame):
                try:  # 使用try-except避免grid_info()对pack布局的Frame报错
                    if child.grid_info()['row'] == '3':
                        button_container = child
                        break
                except tk.TclError:  # 如果frame是pack布局的,grid_info()会失败
                    # 可以通过其他方式识别,例如检查其子控件是否都是按钮
                    is_button_bar = True
                    if not child.winfo_children(): is_button_bar = False  # 空Frame不是
                    for sub_child in child.winfo_children():
                        if not isinstance(sub_child, tk.Button):
                            is_button_bar = False
                            break
                    if is_button_bar and child.winfo_children():  # 确保有按钮
                        # 这里的假设是按钮栏是第一个被pack的Frame (除了格子Frame)
                        # 这依赖于pack的顺序,更稳妥的方式是给button_frame一个name属性
                        if child.winfo_children()[0].winfo_class() == 'Button':  # 粗略判断
                            button_container = child
                            break

        if button_container:
            for btn_widget in button_container.winfo_children():  # 改变量名避免与外层btn冲突
                if isinstance(btn_widget, tk.Button):
                    btn_widget.config(state=tk.DISABLED)
                    all_buttons.append(btn_widget)  # all_buttons现在是控件列表
        self.root.update_idletasks()

        solver = SudokuDLXSolver(board)  # 使用已经通过get_board_from_ui得到的board
        solution = solver.solve(gui_update_callback=self._gui_step_update)

        if button_container:  # 恢复按钮
            for btn_widget in button_container.winfo_children():
                if isinstance(btn_widget, tk.Button):
                    btn_widget.config(state=tk.NORMAL)

        self.steps_label_var.set(f"探索步数: {solver.dlx.search_steps}")

        if solution:
            # self.initial_fill 需要在display_board时是正确的,它由get_board_from_ui()设置
            self.display_board(solution)
            messagebox.showinfo("成功", "数独已解决!")
        else:
            messagebox.showinfo("无解", "未能找到此数独的解。")
            # 清理盘面,只留下初始数字
            current_initial_board = [[val if self.initial_fill[r][c] else 0 for c, val in enumerate(row)] for r, row in
                                     enumerate(self.get_board_from_ui())]  # 重新获取,以防万一
            # 上面这行逻辑复杂了,直接用 self.initial_board (SudokuSolver内部存的) 或者重新构造
            # self.display_board(solver.initial_board) # 显示最初的盘面
            for r in range(9):
                for c in range(9):
                    if not self.initial_fill[r][c]:  # 只处理非初始数字
                        self.cells[r][c].set("")
                        self.entries[r][c].config(fg="black")
                    else:  # 确保初始数字颜色正确,以防万一在可视化过程中被更改
                        self.entries[r][c].config(fg="black")

    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")
                self.initial_fill[r][c] = False
        self.steps_label_var.set("探索步数: 0")

    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]))
                    self.initial_fill[r][c] = True
                    self.entries[r][c].config(fg="black")

    def load_from_csv(self):
        self.clear_board()
        file_path = filedialog.askopenfilename(
            title="选择CSV数独文件",
            filetypes=(("CSV 文件", "*.csv"), ("所有文件", "*.*"))
        )
        if not file_path:
            return

        new_board = []
        try:
            with open(file_path, 'r', newline='') as csvfile:
                reader = csv.reader(csvfile)
                for row_idx, row in enumerate(reader):
                    if row_idx >= 9:  # 最多读9行
                        messagebox.showwarning("CSV警告", f"文件 '{file_path}' 行数超过9行,只处理前9行。")
                        break
                    if len(row) != 9:
                        messagebox.showerror("CSV错误", f"文件 '{file_path}' 中的行 {row_idx + 1} 数据不符合9列标准。")
                        self.clear_board()
                        return
                    current_row = []
                    for val_str in row:
                        val_str_cleaned = val_str.strip()
                        if not val_str_cleaned or val_str_cleaned == '0':
                            current_row.append(0)
                        elif val_str_cleaned.isdigit() and 1 <= int(val_str_cleaned) <= 9:
                            current_row.append(int(val_str_cleaned))
                        else:
                            messagebox.showerror("CSV错误",
                                                 f"文件 '{file_path}' 包含无效字符 '{val_str}'。请使用0-9或空格/空。")
                            self.clear_board()
                            return
                    new_board.append(current_row)

            if len(new_board) != 9:
                messagebox.showerror("CSV错误", f"文件 '{file_path}' 未能构成完整的9行数据。实际行数: {len(new_board)}。")
                self.clear_board()
                return

            for r in range(9):
                for c in range(9):
                    if new_board[r][c] != 0:
                        self.cells[r][c].set(str(new_board[r][c]))
                        self.initial_fill[r][c] = True
                        self.entries[r][c].config(fg="black")

        except FileNotFoundError:
            messagebox.showerror("错误", f"文件 '{file_path}' 未找到。")
            self.clear_board()
        except Exception as e:
            messagebox.showerror("读取错误", f"读取CSV文件时发生错误: {e}")
            self.clear_board()

    # --- 新增: 从LaTeX array加载数独的方法 ---
    def load_from_latex(self):
        """从包含LaTeX array环境的文本文件加载数独棋盘"""
        self.clear_board()  # 清空当前棋盘
        file_path = filedialog.askopenfilename(
            title="选择LaTeX数独文件",
            # 允许.tex和纯文本文件
            filetypes=(("LaTeX 文件", "*.tex"), ("文本文件", "*.txt"), ("所有文件", "*.*"))
        )
        if not file_path:  # 如果用户取消选择
            return

        new_board = []  # 用于存储从LaTeX解析出的棋盘数据
        try:
            with open(file_path, 'r', encoding='utf-8') as f:  # 使用utf-8编码打开文件
                content = f.read()

            # 1. 使用正则表达式查找 array 环境内容
            #    这个正则表达式会匹配 \begin{array}{...} ... \end{array}
            #    re.DOTALL 使得 . 可以匹配换行符
            match = re.search(r"\\begin\{array\}.*?\n(.*?)%?\s*\\end\{array\}", content, re.DOTALL | re.IGNORECASE)
            if not match:
                match = re.search(r"\\begin\{tabular\}.*?\n(.*?)%?\s*\\end\{tabular\}", content,
                                  re.DOTALL | re.IGNORECASE)  # 也尝试tabular

            if not match:
                messagebox.showerror("LaTeX错误", f"在文件 '{file_path}' 中未找到 'array' 或 'tabular' 环境。")
                return

            array_content = match.group(1).strip()  # 获取括号内的匹配内容,并去除首尾空格

            # 2. 逐行解析array内容
            lines = array_content.splitlines()  # 按行分割
            board_rows = 0
            for line_idx, line_str in enumerate(lines):
                line_str = line_str.strip()
                if not line_str or line_str.lower().startswith(r"\hline"):  # 忽略空行和 \hline
                    continue

                if board_rows >= 9:  # 最多处理9行数据
                    messagebox.showwarning("LaTeX警告",
                                           f"文件 '{file_path}' array/tabular环境内数据行超过9行,只处理前9行。")
                    break

                # 移除行尾的 \\ 和可能存在的注释 %...
                line_str = re.sub(r"%.*$", "", line_str)  # 移除注释
                line_str = line_str.replace(r"\\", "").strip()  # 移除 \\ 并再次strip

                cells_str = line_str.split('&')  # 按 & 分割单元格
                if len(cells_str) != 9:
                    messagebox.showerror("LaTeX错误",
                                         f"文件 '{file_path}' 中array/tabular的第 {line_idx + 1} 数据行 (内容: '{line_str[:30]}...') 不包含9个单元格 (实际: {len(cells_str)})。")
                    self.clear_board()
                    return

                current_row = []
                for cell_content in cells_str:
                    cell_content_cleaned = cell_content.strip()
                    # 尝试移除常见的LaTeX大括号如 {1} -> 1
                    braced_match = re.fullmatch(r"\{(.)\}", cell_content_cleaned)
                    if braced_match:
                        cell_content_cleaned = braced_match.group(1)

                    if not cell_content_cleaned:  # 空单元格
                        current_row.append(0)
                    elif cell_content_cleaned.isdigit() and 1 <= int(cell_content_cleaned) <= 9:
                        current_row.append(int(cell_content_cleaned))
                    else:  # 非数字或无效数字,视为0或错误
                        # 如果希望更严格,可以报错:
                        # messagebox.showerror("LaTeX错误", f"单元格内容 '{cell_content}' 无效。")
                        # self.clear_board()
                        # return
                        current_row.append(0)  # 这里选择将其视为0

                new_board.append(current_row)
                board_rows += 1

            if board_rows != 9:
                messagebox.showerror("LaTeX错误",
                                     f"文件 '{file_path}' 未能从array/tabular环境解析出完整的9行数据。实际解析行数: {board_rows}。")
                self.clear_board()
                return

            # 3. 将解析到的棋盘数据加载到GUI
            for r in range(9):
                for c in range(9):
                    if new_board[r][c] != 0:
                        self.cells[r][c].set(str(new_board[r][c]))
                        self.initial_fill[r][c] = True
                        self.entries[r][c].config(fg="black")

        except FileNotFoundError:
            messagebox.showerror("错误", f"文件 '{file_path}' 未找到。")
            self.clear_board()
        except Exception as e:
            messagebox.showerror("读取错误", f"读取或解析LaTeX文件时发生错误: {e}")
            self.clear_board()


if __name__ == "__main__":
    main_root = tk.Tk()
    app = SudokuGUI(main_root)
    main_root.mainloop()
相关推荐
lly20240626 分钟前
HTML 表单
开发语言
Blossom.1181 小时前
基于深度学习的图像分割:使用DeepLabv3实现高效分割
人工智能·python·深度学习·机器学习·分类·机器人·transformer
爱代码的小黄人1 小时前
利用劳斯判据分析右半平面极点数量的方法研究
算法·机器学习·平面
深海潜水员3 小时前
【Python】 切割图集的小脚本
开发语言·python
27669582923 小时前
东方航空 m端 wasm req res分析
java·python·node·wasm·东方航空·东航·东方航空m端
Yolo566Q4 小时前
R语言与作物模型(以DSSAT模型为例)融合应用高级实战技术
开发语言·经验分享·r语言
Felven4 小时前
C. Challenging Cliffs
c语言·开发语言
星月昭铭4 小时前
Spring AI调用Embedding模型返回HTTP 400:Invalid HTTP request received分析处理
人工智能·spring boot·python·spring·ai·embedding
Dreamsi_zh4 小时前
Python爬虫02_Requests实战网页采集器
开发语言·爬虫·python
今天也好累5 小时前
C 语言基础第16天:指针补充
java·c语言·数据结构·笔记·学习·算法