数独求解器3.0 增加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或空单元格代表空格),用逗号分隔。

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

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()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值