【Python高级编程】图着色动态可视化 APP

目录

一、引言

[1. 问题核心](#1. 问题核心)

[2. 算法整体逻辑](#2. 算法整体逻辑)

[步骤 1:构建图的邻接矩阵](#步骤 1:构建图的邻接矩阵)

[步骤 2:枚举颜色数k](#步骤 2:枚举颜色数k)

[步骤 3:回溯法验证k种颜色是否可行](#步骤 3:回溯法验证k种颜色是否可行)

[3. 最终结果](#3. 最终结果)

[二、图着色动态可视化 APP的开发过程](#二、图着色动态可视化 APP的开发过程)

[1. 需求分析:明确核心需求](#1. 需求分析:明确核心需求)

[2. 技术选型:适配需求的轻量栈](#2. 技术选型:适配需求的轻量栈)

[3. 整体架构设计:模块化封装](#3. 整体架构设计:模块化封装)

[4. 分阶段开发过程:从骨架到完整功能](#4. 分阶段开发过程:从骨架到完整功能)

[阶段 1:基础框架搭建(UI 骨架 + 核心数据初始化)](#阶段 1:基础框架搭建(UI 骨架 + 核心数据初始化))

[阶段 2:核心算法实现(回溯法图着色)](#阶段 2:核心算法实现(回溯法图着色))

[阶段 3:可视化功能开发(绘图 + 布局 + 颜色自定义)](#阶段 3:可视化功能开发(绘图 + 布局 + 颜色自定义))

[阶段 4:动画与交互控制(核心体验优化)](#阶段 4:动画与交互控制(核心体验优化))

[阶段 5:数据管理与结果导出(完善功能闭环)](#阶段 5:数据管理与结果导出(完善功能闭环))

[阶段 6:异常处理与边界条件优化](#阶段 6:异常处理与边界条件优化)

[5. 关键技术难点与解决方案](#5. 关键技术难点与解决方案)

[难点 1:TTK 按钮无法直接设置背景色](#难点 1:TTK 按钮无法直接设置背景色)

[难点 2:递归算法改为分步动画](#难点 2:递归算法改为分步动画)

[难点 3:Matplotlib 嵌入 Tkinter 后的视图控制](#难点 3:Matplotlib 嵌入 Tkinter 后的视图控制)

[难点 4:颜色更新的无效渲染](#难点 4:颜色更新的无效渲染)

[6. 迭代优化:从功能可用到体验优秀](#6. 迭代优化:从功能可用到体验优秀)

[7. 总结](#7. 总结)

[三、图着色动态可视化 APP的Python代码完整实现](#三、图着色动态可视化 APP的Python代码完整实现)

四、程序运行部分截图展示

五、总结


一、引言

在介绍本文的项目之前,我们先做一下这道题:

图着色问题

给定无向连通图 G(V,E) ,求最小的整数 m,用 m 种颜色对 G 种的顶点着色,使得任意两个相邻定点的着色不同

输入格式:

第一行输入两个整数 n(1≤n≤7) 和 m(n−1≤m≤n∗(n−1)/2)

接下来 m 行,每行两个整数 u,v(1≤u,v≤n)

输出格式:

输出满足条件最小的 m

输入样例:

复制代码
5 8
1 2
1 3
1 4
2 3
2 4
2 5
3 4
4 5

输出样例:

在这里给出相应的输出。例如:

复制代码
4

这个算法是用来求解无向连通图最小顶点着色数 的,核心思路是枚举颜色数 + 回溯法验证,步骤如下:

1. 问题核心

要找到最小的整数k,使得用k种颜色给图的顶点着色后,任意相邻顶点颜色不同(即 "合法着色")。

2. 算法整体逻辑

分两步:枚举可能的颜色数用回溯法验证该颜色数是否能实现合法着色

步骤 1:构建图的邻接矩阵

先把输入的图转化为邻接矩阵 (二维数组adj):

  • adj[u][v] = True 表示顶点uv是相邻的;
  • adj[u][v] = False 表示顶点uv不相邻。
步骤 2:枚举颜色数k

从最小的可能值(k=1)开始尝试,直到k=nn是顶点数,因为n个顶点用n种颜色一定能合法着色 ------ 每个顶点用不同颜色)。

步骤 3:回溯法验证k种颜色是否可行

is_valid函数实现 "尝试用k种颜色给所有顶点合法着色":

  • 从第 1 个顶点开始,依次给每个顶点(记为v)分配颜色;
  • 对当前顶点v,尝试 1~k的所有颜色:
    • 检查已着色的相邻顶点 (前v-1个顶点,因为后序顶点还没着色)是否和当前颜色冲突;
    • 若不冲突,就给顶点v分配该颜色,递归处理下一个顶点v+1
    • 若递归后能让所有顶点(直到v=n+1)都合法着色,说明k种颜色可行;
    • 若不行,就 "回溯"(把当前顶点的颜色清空),尝试下一个颜色。

3. 最终结果

第一个能通过回溯验证的k,就是图的最小着色数

总结来说:通过 "从小到大试颜色数 + 回溯法逐个顶点试颜色",找到能合法着色的最小k

Python代码:

python 复制代码
def is_valid(v, color, adj, k, n):
    """
    回溯验证是否能用k种颜色完成着色
    v: 当前处理的顶点编号
    color: 颜色数组,color[i]表示顶点i的颜色(0为未着色)
    adj: 邻接矩阵
    k: 尝试的颜色数
    n: 总顶点数
    """
    if v == n + 1:  # 所有顶点着色完成
        return True
    # 尝试为当前顶点分配1~k的颜色
    for c in range(1, k + 1):
        # 检查已着色的相邻顶点是否冲突(仅需检查前v-1个顶点,后序未着色)
        conflict = False
        for i in range(1, v):
            if adj[v][i] and color[i] == c:
                conflict = True
                break
        if not conflict:
            color[v] = c  # 分配颜色
            if is_valid(v + 1, color, adj, k, n):
                return True
            color[v] = 0  # 回溯,撤销颜色
    return False

# 输入处理
n, m = map(int, input().split())
# 构建邻接矩阵(顶点编号1~n)
adj = [[False] * (n + 1) for _ in range(n + 1)]
for _ in range(m):
    u, v = map(int, input().split())
    adj[u][v] = True
    adj[v][u] = True

# 枚举颜色数k(从1到n,n种颜色必可行)
for k in range(1, n + 1):
    color = [0] * (n + 1)  # 初始化颜色数组
    if is_valid(1, color, adj, k, n):
        print(k)
        exit()

# 理论上不会执行到此处(n种颜色可给每个顶点分配不同颜色)
print(n)

为了方便理解这道题,我们开发了一个基于 Python + Tkinter + Matplotlib 的图着色算法动态可视化工具,核心目标是将经典的回溯法图着色算法以交互式、可视化的方式呈现,同时提供灵活的 UI 操作和数据管理能力。本文将详细介绍图着色动态可视化 APP的开发过程以及Python代码完整实现。

二、图着色动态可视化 APP的开发过程

1. 需求分析:明确核心需求

开发前首先梳理清晰需求,分为核心需求,确保功能聚焦且实用:

  • 支持输入图的顶点数、边数和边信息,构建无向图;
  • 实现回溯法图着色算法,动态可视化每一步的着色 / 回溯过程;
  • 展示算法执行进度,输出详细日志;
  • 支持自定义着色颜色(8 种可配置),区分不同顶点的颜色分配。
  • 多布局支持(圆形 / 螺旋 / 网格),适配不同顶点数的可视化效果;
  • 视图控制(缩放、平移、字体大小调整),提升交互体验;
  • 配置管理(保存 / 加载图信息、颜色、布局),方便复用;
  • 结果导出(图片 / 文本),满足数据留存需求;
  • 动画控制(暂停 / 继续 / 速度调节),适配不同演示节奏;
  • 完善的异常处理和用户引导(使用帮助)。

2. 技术选型:适配需求的轻量栈

针对 "桌面端轻量可视化工具" 的定位,选择以下技术栈,兼顾开发效率和运行性能:

技术 / 库 选型原因
Python 3.x 语法简洁,生态丰富,适合快速开发桌面工具
Tkinter (ttk) Python 内置 GUI 库,无需额外安装,轻量稳定;ttk 提供更美观的原生控件
Matplotlib 专业绘图库,支持嵌入 Tkinter 窗口,可精细化控制图形元素(顶点、边、颜色)
JSON/os/filedialog 处理配置保存 / 加载、文件导出,Python 内置库,无需依赖
numpy/matplotlib.colors 解决颜色精度比较、RGBA 格式转换等可视化细节问题
time/math 处理时间戳(日志)、坐标计算(布局生成)

3. 整体架构设计:模块化封装

采用面向对象 的设计思路,将所有功能封装在GraphColoringApp类中,保证代码的内聚性和可维护性。该类的核心变量和方法分工:

  • 核心数据变量:存储顶点数 / 边数、邻接矩阵、颜色数组、布局坐标、视图参数等;
  • UI 初始化方法:构建左右分栏布局,初始化所有控件并绑定事件;
  • 算法方法check_conflict(冲突检查)、color_step(分步着色);
  • 可视化方法draw_graph(绘图)、generate_*_positions(布局生成);
  • 交互方法start/pause/resume_coloring(动画控制)、adjust_scale(缩放);
  • 数据管理方法save/load_config(配置管理)、export_*(结果导出)。

4. 分阶段开发过程:从骨架到完整功能

阶段 1:基础框架搭建(UI 骨架 + 核心数据初始化)

目标:完成窗口布局和核心数据定义,为后续功能打基础。

  1. 窗口初始化:设置窗口标题、尺寸,启用高 DPI 适配(解决模糊问题);
  2. 布局拆分:将界面分为 "左侧功能区" 和 "右侧可视化区",左侧进一步拆分为输入、布局、颜色、控制、进度、日志、视图控制 7 个子区域,保证 UI 层次清晰;
  3. 核心数据初始化 :定义顶点数n、边数m、邻接矩阵adj、颜色数组color、视图参数(缩放因子scale_factor、平移偏移x/y_offset)等变量,设置默认值;
  4. Matplotlib 嵌入 :创建FigureCanvas,将绘图区域嵌入 Tkinter 右侧窗口,初始化空画布。

阶段 2:核心算法实现(回溯法图着色)

目标:将递归的图着色算法改为 "分步执行",适配可视化需求。

  1. 算法核心逻辑(回溯法)
    • 核心思路:从 1 种颜色开始尝试,若无法完成所有顶点着色则递增颜色数,直到找到最小可行颜色数;
    • 关键方法:check_conflict(v, c)检查顶点v分配颜色c是否与相邻顶点冲突;
    • 适配可视化:放弃递归直接执行,改为color_step分步执行(每一步仅处理一个顶点 / 颜色),通过root.after()调度下一步,实现 "动画效果"。
  2. 输入解析parse_input方法校验顶点数 / 边数合法性,解析边信息构建邻接矩阵,抛出友好的错误提示;
  3. 进度与日志
    • update_progress实时更新进度条(按当前处理顶点数 / 总顶点数计算);
    • log方法输出带时间戳的日志,区分error/info/success标签,提升可读性。

阶段 3:可视化功能开发(绘图 + 布局 + 颜色自定义)

目标:将算法执行过程 "可视化",让每一步操作可见。

  1. 基础绘图
    • draw_graph方法分为 "初始化绘图"(绘制边、顶点)和 "增量更新"(仅更新顶点颜色),避免重复绘制提升性能;
    • 顶点用plt.Circle绘制,边用plt.plot绘制,通过zorder控制层级(边 < 顶点 < 文字);
  2. 多布局支持
    • 实现generate_circle/spiral/grid_positions三个布局函数,根据顶点数动态计算坐标(如圆形布局按角度均分,网格布局接近正方形);
    • change_layout方法绑定下拉框事件,切换布局时重新计算坐标并重绘;
  3. 颜色自定义
    • 初始化DEFAULT_COLOR_MAP作为默认颜色配置,支持用户通过colorchooser修改;
    • 解决 TTK 按钮背景色自定义问题:通过ttk.Style配置按钮样式(Color{c_id}.TButton),绑定颜色选择事件。

阶段 4:动画与交互控制(核心体验优化)

目标:让可视化过程可控制、易操作。

  1. 动画控制
    • start_coloring:解析输入→初始化状态→启动分步着色(after调度color_step);
    • pause/resume_coloring:通过is_paused状态变量控制动画暂停 / 继续;
    • 速度控制:用Scale组件绑定speed_var,调整after的延迟时间;
  2. 视图控制
    • 缩放:通过scale_factor调整顶点大小和坐标轴范围,限制缩放范围(0.2~3.0)避免极端效果;
    • 平移:通过x/y_offset调整坐标轴偏移,支持滑动条和快捷按钮(上下左右)两种操作方式;
    • 字体大小:新增font_size_var,绑定滑块事件,适配缩放因子保证字体同步调整;
  3. 重置功能reset方法清空所有输入、状态、视图参数,恢复初始状态,避免重启程序。

阶段 5:数据管理与结果导出(完善功能闭环)

目标:支持配置复用和结果留存。

  1. 配置保存 / 加载
    • save_config:将顶点数、边数、边信息、颜色映射、布局、字体大小序列化为 JSON,通过filedialog保存到本地;
    • load_config:读取 JSON 文件,恢复 UI 控件值和核心数据,重新绘制图形;
  2. 结果导出
    • export_image:调用 Matplotlib 的savefig保存当前可视化图形为 PNG/JPG;
    • export_text:将图信息、着色结果、字体大小等写入 TXT 文件;
  3. 使用帮助show_help方法创建置顶弹窗,提供详细的操作指南和算法参考代码(C++/Python/Java),降低使用门槛。

阶段 6:异常处理与边界条件优化

目标:提升程序稳定性和用户体验。

  1. 输入校验:检查顶点数(≥1)、边数(≥0)、边格式(u v)、顶点范围(1~n),抛出明确的错误提示;
  2. 防递归调用 :添加is_drawing锁,避免视图操作时重复调用绘图函数导致卡顿;
  3. 颜色精度处理 :用np.allclose比较 RGBA 颜色数组(容错 1e-6),避免浮点数精度问题导致的无效更新;
  4. 边界限制:缩放 / 平移 / 字体大小设置上下限,防止参数越界;
  5. 资源清理on_closing方法取消after调度、关闭 Matplotlib 画布,避免内存泄漏;
  6. 日志容错:日志输出添加 try-except,防止日志报错导致主程序崩溃。

5. 关键技术难点与解决方案

开发过程中遇到的核心问题及解决思路:

难点 1:TTK 按钮无法直接设置背景色

  • 问题:TTK 控件为了适配系统主题,默认屏蔽了background参数;
  • 解决方案:创建自定义Style(如Color{c_id}.TButton),通过style.configure配置按钮背景色,绑定到对应按钮。

难点 2:递归算法改为分步动画

  • 问题:原生回溯法是递归执行,无法分步可视化;
  • 解决方案:
    1. 将递归逻辑拆解为color_step分步函数,用状态变量(current_v当前顶点、current_c当前颜色、current_k当前尝试颜色数)记录执行位置;
    2. root.after()异步调度下一步执行,实现 "一步一绘" 的动画效果;
    3. is_running/is_paused控制执行状态,支持暂停 / 继续。

难点 3:Matplotlib 嵌入 Tkinter 后的视图控制

  • 问题:缩放 / 平移需要同步更新图形元素(顶点大小、字体、坐标轴);
  • 解决方案:
    1. 维护scale_factor(缩放因子)和x/y_offset(平移偏移);
    2. 每次视图变化时调用update_axes_limits更新坐标轴范围;
    3. 绘图时根据缩放因子调整顶点半径、字体大小,保证视觉一致性。

难点 4:颜色更新的无效渲染

  • 问题:每次着色步骤都重绘整个图形,导致卡顿;
  • 解决方案:
    1. 绘图函数draw_graph区分init=True(全量绘制)和init=False(仅更新顶点颜色);
    2. 仅当顶点颜色实际变化时,才更新Circlefacecolor,减少无效绘制。

6. 迭代优化:从功能可用到体验优秀

最终版本在基础功能上的优化点:

  1. 中文适配 :设置 Matplotlib 字体为SimHei,解决中文日志 / 标签显示乱码;
  2. 动态顶点大小:根据顶点数调整基础半径(顶点数越多,半径越小);
  3. 进度条精准度:按当前处理顶点数计算进度,而非颜色尝试次数;
  4. 操作反馈:所有交互(如布局切换、缩放)都输出日志,告知用户操作结果;
  5. 高 DPI 适配 :调用root.tk.call('tk', 'scaling', 1.2)提升高分辨率屏幕下的 UI 清晰度。

7. 总结

这个图着色可视化工具的开发过程遵循 "需求驱动→模块化设计→分阶段实现→难点攻坚→体验优化" 的思路,核心是:

  1. 将复杂算法拆解为可可视化的分步逻辑,平衡算法完整性和交互体验;
  2. 围绕 "用户操作" 设计 UI 和交互,兼顾新手友好性和功能灵活性;
  3. 注重细节优化(如颜色精度、资源清理、边界限制),提升程序稳定性。

最终实现的工具不仅能直观展示图着色算法的核心逻辑(回溯、冲突检查、最小颜色数求解),还通过丰富的交互功能降低了算法学习和演示的门槛,符合 "可视化教学工具" 的核心定位。

三、图着色动态可视化 APP的Python代码完整实现

python 复制代码
import tkinter as tk
from tkinter import ttk, scrolledtext, colorchooser, filedialog, messagebox
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.colors as mcolors
import matplotlib

matplotlib.interactive(False)
import math
import time
import json
import os
import numpy as np
import sys

sys.setrecursionlimit(10000)

# 初始颜色映射(支持自定义修改)
DEFAULT_COLOR_MAP = {
    0: "white",  # 未着色(固定不可改)
    1: "red",  # 颜色1
    2: "blue",  # 颜色2
    3: "green",  # 颜色3
    4: "yellow",  # 颜色4
    5: "purple",  # 颜色5
    6: "orange",  # 颜色6
    7: "cyan",  # 颜色7
    8: "magenta"  # 颜色8
}


class GraphColoringApp:
    def __init__(self, root):
        self.root = root
        self.root.title("图着色动态可视化APP")
        self.root.geometry("1400x900")
        self.root.resizable(True, True)

        # 设置ttk主题为clam(支持Button背景色)
        style = ttk.Style(root)
        style.theme_use("clam")

        # 核心数据初始化
        self.n = 0  # 顶点数
        self.m = 0  # 边数
        self.adj = []  # 邻接矩阵
        self.color = []  # 颜色数组(0=未着色)
        self.vertex_pos = {}  # 顶点坐标(用于绘图)
        self.current_k = 1  # 当前尝试的颜色数
        self.current_v = 1  # 当前处理的顶点
        self.current_c = 1  # 当前尝试的颜色
        self.is_running = False  # 是否正在执行着色
        self.is_paused = False  # 是否暂停
        self.vertex_patches = {}  # 缓存顶点绘图元素
        self.after_id = None  # 保存after调度ID,用于取消
        self.color_map = DEFAULT_COLOR_MAP.copy()  # 可自定义的颜色映射
        self.layout_type = tk.StringVar(value="圆形")  # 当前布局类型

        # 缩放和平移相关变量
        self.scale_factor = tk.DoubleVar(value=1.0)  # 初始缩放因子
        self.x_offset = tk.DoubleVar(value=0.0)  # x轴平移偏移
        self.y_offset = tk.DoubleVar(value=0.0)  # y轴平移偏移
        self.base_x_range = (-6, 6)  # 基础x范围
        self.base_y_range = (-6, 6)  # 基础y范围

        # ========== 字体大小控制变量 ==========
        self.font_size_var = tk.IntVar(value=10)  # 初始字体大小(默认10号)
        self.min_font_size = 4  # 最小字体大小限制
        self.max_font_size = 24  # 最大字体大小限制

        # 添加重绘锁,防止递归调用
        self.is_drawing = False

        # 1. 左侧功能区域(进一步拆分:输入区+布局区+颜色区+控制区+进度区+日志区+缩放平移区)
        self.left_frame = ttk.Frame(root, width=400)
        self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10)
        self.left_frame.pack_propagate(False)

        # 1.1 输入子区域(新增保存/加载按钮)
        self.input_subframe = ttk.LabelFrame(self.left_frame, text="图信息输入", padding=5)
        self.input_subframe.pack(fill=tk.X, padx=5, pady=5)

        ttk.Label(self.input_subframe, text="顶点数(n):").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.n_entry = ttk.Entry(self.input_subframe, width=10)
        self.n_entry.grid(row=0, column=1, padx=5, pady=5)

        ttk.Label(self.input_subframe, text="边数(m):").grid(row=1, column=0, padx=5, pady=5, sticky=tk.W)
        self.m_entry = ttk.Entry(self.input_subframe, width=10)
        self.m_entry.grid(row=1, column=1, padx=5, pady=5)

        ttk.Label(self.input_subframe, text="输入边(格式:u v,每行一条):").grid(row=2, column=0, columnspan=3, padx=5,
                                                                               pady=5, sticky=tk.W)
        self.edge_text = scrolledtext.ScrolledText(self.input_subframe, width=32, height=6)
        self.edge_text.grid(row=3, column=0, columnspan=3, padx=5, pady=5)

        # 保存/加载配置按钮 + 使用帮助按钮
        self.save_config_btn = ttk.Button(self.input_subframe, text="保存配置", command=self.save_config)
        self.save_config_btn.grid(row=4, column=0, padx=5, pady=5)
        self.load_config_btn = ttk.Button(self.input_subframe, text="加载配置", command=self.load_config)
        self.load_config_btn.grid(row=4, column=1, padx=5, pady=5)
        self.help_btn = ttk.Button(self.input_subframe, text="使用帮助", command=self.show_help)
        self.help_btn.grid(row=4, column=2, padx=5, pady=5)

        # 1.2 布局选择子区域
        self.layout_subframe = ttk.LabelFrame(self.left_frame, text="布局选择", padding=5)
        self.layout_subframe.pack(fill=tk.X, padx=5, pady=5)

        ttk.Label(self.layout_subframe, text="布局类型:").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.layout_combobox = ttk.Combobox(
            self.layout_subframe,
            textvariable=self.layout_type,
            values=["圆形", "螺旋", "网格"],
            state="readonly",
            width=15
        )
        self.layout_combobox.grid(row=0, column=1, padx=5, pady=5)
        self.layout_combobox.bind("<<ComboboxSelected>>", self.change_layout)

        # 1.3 颜色自定义子区域
        self.color_subframe = ttk.LabelFrame(self.left_frame, text="颜色自定义(编号1-8)", padding=5)
        self.color_subframe.pack(fill=tk.X, padx=5, pady=5)

        # 生成8个颜色选择项
        self.color_btns = {}
        self.style = ttk.Style()  # 初始化style对象
        for c_id in range(1, 9):
            row = (c_id - 1) // 2
            col = (c_id - 1) % 2
            ttk.Label(self.color_subframe, text=f"颜色{c_id}:").grid(row=row, column=col * 2, padx=5, pady=3,
                                                                     sticky=tk.W)

            style_name = f"Color{c_id}.TButton"
            # 初始化颜色按钮的style
            self.style.configure(style_name, background=self.color_map[c_id])

            # 颜色显示按钮
            btn = ttk.Button(
                self.color_subframe,
                text="",
                width=5,
                command=lambda c=c_id: self.choose_color(c),
                style=style_name  # 直接指定style
            )
            btn.grid(row=row, column=col * 2 + 1, padx=5, pady=3)
            self.color_btns[c_id] = btn

        # 1.4 控制子区域
        self.ctrl_subframe = ttk.LabelFrame(self.left_frame, text="动画控制", padding=5)
        self.ctrl_subframe.pack(fill=tk.X, padx=5, pady=5)

        # 速度控制(毫秒/步)
        ttk.Label(self.ctrl_subframe, text="演示速度(ms/步):").grid(row=0, column=0, padx=5, pady=5, sticky=tk.W)
        self.speed_var = tk.IntVar(value=800)
        self.speed_scale = ttk.Scale(self.ctrl_subframe, from_=100, to=2000, variable=self.speed_var,
                                     orient=tk.HORIZONTAL)
        self.speed_scale.grid(row=0, column=1, columnspan=3, padx=5, pady=5)

        # 功能按钮
        self.reset_btn = ttk.Button(self.ctrl_subframe, text="重置", command=self.reset)
        self.reset_btn.grid(row=1, column=0, padx=5, pady=8)
        self.start_btn = ttk.Button(self.ctrl_subframe, text="开始着色", command=self.start_coloring)
        self.start_btn.grid(row=1, column=1, padx=5, pady=8)
        self.pause_btn = ttk.Button(self.ctrl_subframe, text="暂停", command=self.pause_coloring, state=tk.DISABLED)
        self.pause_btn.grid(row=1, column=2, padx=5, pady=8)
        self.resume_btn = ttk.Button(self.ctrl_subframe, text="继续", command=self.resume_coloring, state=tk.DISABLED)
        self.resume_btn.grid(row=1, column=3, padx=5, pady=8)

        # 导出结果按钮
        self.export_img_btn = ttk.Button(self.ctrl_subframe, text="导出图片", command=self.export_image)
        self.export_img_btn.grid(row=2, column=0, padx=5, pady=8)
        self.export_txt_btn = ttk.Button(self.ctrl_subframe, text="导出文本", command=self.export_text)
        self.export_txt_btn.grid(row=2, column=1, padx=5, pady=8)

        # 1.5 进度条子区域
        self.progress_subframe = ttk.LabelFrame(self.left_frame, text="执行进度", padding=5)
        self.progress_subframe.pack(fill=tk.X, padx=5, pady=5)

        self.progress_var = tk.DoubleVar(value=0.0)
        self.progress_bar = ttk.Progressbar(
            self.progress_subframe,
            variable=self.progress_var,
            maximum=100.0,
            mode="determinate"
        )
        self.progress_bar.pack(fill=tk.X, padx=5, pady=5)
        self.progress_label = ttk.Label(self.progress_subframe, text="0% (0/0)")
        self.progress_label.pack(padx=5, pady=2)

        # 1.6 日志子区域
        self.log_subframe = ttk.LabelFrame(self.left_frame, text="执行日志", padding=5)
        self.log_subframe.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)

        self.log_text = scrolledtext.ScrolledText(self.log_subframe, width=37, height=8)
        self.log_text.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
        # 预定义日志标签
        self.log_text.tag_config("error", foreground="red")
        self.log_text.tag_config("info", foreground="black")
        self.log_text.tag_config("success", foreground="green")

        # 1.7 缩放和平移控制子区域(左侧)
        self.zoom_pan_subframe_left = ttk.LabelFrame(self.left_frame, text="视图控制(左侧)", padding=5)
        self.zoom_pan_subframe_left.pack(fill=tk.X, padx=5, pady=5)

        # 缩放按钮
        ttk.Label(self.zoom_pan_subframe_left, text="缩放控制:").grid(row=0, column=0, columnspan=3, padx=5, pady=3,
                                                                      sticky=tk.W)
        self.zoom_in_btn = ttk.Button(self.zoom_pan_subframe_left, text="缩小", command=lambda: self.adjust_scale(0.1))
        self.zoom_in_btn.grid(row=1, column=0, padx=3, pady=3)
        self.zoom_out_btn = ttk.Button(self.zoom_pan_subframe_left, text="放大",
                                       command=lambda: self.adjust_scale(-0.1))
        self.zoom_out_btn.grid(row=1, column=1, padx=3, pady=3)
        self.zoom_reset_btn = ttk.Button(self.zoom_pan_subframe_left, text="重置缩放", command=self.reset_scale)
        self.zoom_reset_btn.grid(row=1, column=2, padx=3, pady=3)

        # 平移滑动条(左侧)
        ttk.Label(self.zoom_pan_subframe_left, text="平移控制:").grid(row=2, column=0, columnspan=3, padx=5, pady=3,
                                                                      sticky=tk.W)

        # X轴平移
        ttk.Label(self.zoom_pan_subframe_left, text="X轴:").grid(row=3, column=0, padx=5, pady=2, sticky=tk.W)
        self.x_slider_left = ttk.Scale(
            self.zoom_pan_subframe_left,
            from_=-5, to=5,
            variable=self.x_offset,
            orient=tk.HORIZONTAL,
            command=self.on_x_offset_changed
        )
        self.x_slider_left.grid(row=3, column=1, columnspan=2, padx=5, pady=2, sticky=tk.EW)

        # Y轴平移
        ttk.Label(self.zoom_pan_subframe_left, text="Y轴:").grid(row=4, column=0, padx=5, pady=2, sticky=tk.W)
        self.y_slider_left = ttk.Scale(
            self.zoom_pan_subframe_left,
            from_=-5, to=5,
            variable=self.y_offset,
            orient=tk.HORIZONTAL,
            command=self.on_y_offset_changed
        )
        self.y_slider_left.grid(row=4, column=1, columnspan=2, padx=5, pady=2, sticky=tk.EW)

        # ========== 字体大小控制组件 ==========
        ttk.Label(self.zoom_pan_subframe_left, text="字体大小:").grid(row=5, column=0, columnspan=3, padx=5, pady=3,
                                                                      sticky=tk.W)
        # 字体大小滑块
        self.font_size_scale = ttk.Scale(
            self.zoom_pan_subframe_left,
            from_=self.min_font_size,
            to=self.max_font_size,
            variable=self.font_size_var,
            orient=tk.HORIZONTAL,
            command=self.on_font_size_changed
        )
        self.font_size_scale.grid(row=6, column=0, columnspan=2, padx=5, pady=3, sticky=tk.EW)
        # 字体大小重置按钮
        self.font_size_reset_btn = ttk.Button(
            self.zoom_pan_subframe_left,
            text="重置字体",
            command=self.reset_font_size
        )
        self.font_size_reset_btn.grid(row=6, column=2, padx=3, pady=3)

        # 2. 右侧主区域(可视化+平移滑动条)
        self.right_main_frame = ttk.Frame(root)
        self.right_main_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)

        # 2.1 可视化区域
        self.fig, self.ax = plt.subplots(figsize=(9, 7), dpi=100)
        self.fig.tight_layout(pad=0)
        self.canvas = FigureCanvasTkAgg(self.fig, master=self.right_main_frame)
        self.canvas_widget = self.canvas.get_tk_widget()
        self.canvas_widget.pack(side=tk.TOP, fill=tk.BOTH, expand=True, padx=5, pady=5)

        # 2.2 缩放和平移控制子区域(右侧)
        self.zoom_pan_subframe_right = ttk.LabelFrame(self.right_main_frame, text="视图控制(右侧)", padding=5)
        self.zoom_pan_subframe_right.pack(fill=tk.X, padx=5, pady=5)

        # 上下左右快捷平移按钮
        ttk.Label(self.zoom_pan_subframe_right, text="快捷平移:").grid(row=0, column=0, padx=5, pady=3, sticky=tk.W)
        self.up_btn = ttk.Button(self.zoom_pan_subframe_right, text="上", command=lambda: self.pan_view(0, 0.5))
        self.up_btn.grid(row=0, column=1, padx=2, pady=2)
        self.down_btn = ttk.Button(self.zoom_pan_subframe_right, text="下", command=lambda: self.pan_view(0, -0.5))
        self.down_btn.grid(row=0, column=2, padx=2, pady=2)
        self.left_btn = ttk.Button(self.zoom_pan_subframe_right, text="左", command=lambda: self.pan_view(-0.5, 0))
        self.left_btn.grid(row=0, column=3, padx=2, pady=2)
        self.right_btn = ttk.Button(self.zoom_pan_subframe_right, text="右", command=lambda: self.pan_view(0.5, 0))
        self.right_btn.grid(row=0, column=4, padx=2, pady=2)
        self.pan_reset_btn = ttk.Button(self.zoom_pan_subframe_right, text="重置平移", command=self.reset_pan)
        self.pan_reset_btn.grid(row=0, column=5, padx=2, pady=2)

        # 平移滑动条(右侧,垂直+水平)
        # 水平X轴平移
        ttk.Label(self.zoom_pan_subframe_right, text="水平平移:").grid(row=1, column=0, padx=5, pady=2, sticky=tk.W)
        self.x_slider_right = ttk.Scale(
            self.zoom_pan_subframe_right,
            from_=-5, to=5,
            variable=self.x_offset,
            orient=tk.HORIZONTAL,
            command=self.on_x_offset_changed
        )
        self.x_slider_right.grid(row=1, column=1, columnspan=5, padx=5, pady=2, sticky=tk.EW)

        # 垂直Y轴平移(垂直滑动条)
        ttk.Label(self.zoom_pan_subframe_right, text="垂直平移:").grid(row=0, column=6, padx=5, pady=2, sticky=tk.N)
        self.y_slider_right_vertical = ttk.Scale(
            self.zoom_pan_subframe_right,
            from_=5, to=-5,  # 反转方向,上滑=+Y,下滑=-Y
            variable=self.y_offset,
            orient=tk.VERTICAL,
            command=self.on_y_offset_changed
        )
        self.y_slider_right_vertical.grid(row=0, column=7, rowspan=2, padx=5, pady=2, sticky=tk.NS)

        # 初始化画布
        self.init_plot()

        # 绑定窗口关闭事件,清理资源
        self.root.protocol("WM_DELETE_WINDOW", self.on_closing)

    def on_closing(self):
        """窗口关闭时清理资源"""
        if self.after_id:
            self.root.after_cancel(self.after_id)
        plt.close(self.fig)
        self.root.destroy()

    def init_plot(self):
        """初始化绘图区域(适配缩放和平移)"""
        self.ax.clear()
        self.update_axes_limits()  # 更新坐标轴范围
        self.ax.axis("off")
        self.ax.set_aspect("equal")
        self.canvas.draw_idle()

    def update_axes_limits(self):
        """根据缩放因子和平移更新坐标轴范围"""
        try:
            # 获取当前的缩放和平移值
            scale = self.scale_factor.get()
            x_off = self.x_offset.get()
            y_off = self.y_offset.get()

            # 计算缩放后的基础范围
            scaled_x_min = self.base_x_range[0] * scale
            scaled_x_max = self.base_x_range[1] * scale
            scaled_y_min = self.base_y_range[0] * scale
            scaled_y_max = self.base_y_range[1] * scale

            # 应用平移偏移
            final_x_min = scaled_x_min + x_off
            final_x_max = scaled_x_max + x_off
            final_y_min = scaled_y_min + y_off
            final_y_max = scaled_y_max + y_off

            self.ax.set_xlim(final_x_min, final_x_max)
            self.ax.set_ylim(final_y_min, final_y_max)
        except Exception as e:
            self.log(f"更新坐标轴范围出错:{str(e)}", "error")

    def adjust_scale(self, delta):
        """调整缩放因子"""
        try:
            new_scale = self.scale_factor.get() + delta
            # 限制缩放范围
            if 0.2 <= new_scale <= 3.0:
                self.scale_factor.set(new_scale)
                self.update_axes_limits()
                self.safe_draw_graph(init=True)  # 安全重绘
                self.log(f"🔍 视图缩放:{new_scale:.1f}倍", "info")
        except Exception as e:
            self.log(f"调整缩放出错:{str(e)}", "error")

    def reset_scale(self):
        """重置缩放因子"""
        try:
            self.scale_factor.set(1.0)
            self.update_axes_limits()
            self.safe_draw_graph(init=True)
            self.log(f"🔍 缩放已重置为1.0倍", "info")
        except Exception as e:
            self.log(f"重置缩放出错:{str(e)}", "error")

    def pan_view(self, dx, dy):
        """快捷平移视图"""
        try:
            # 计算新的偏移值
            new_x = self.x_offset.get() + dx
            new_y = self.y_offset.get() + dy

            # 限制平移范围
            new_x = max(-4, min(4, new_x))
            new_y = max(-4, min(4, new_y))

            # 设置新值(自动同步所有滑动条)
            self.x_offset.set(new_x)
            self.y_offset.set(new_y)

            # 更新视图
            self.update_axes_limits()
            self.safe_draw_graph(init=True)
            self.log(f"📱 视图平移:X={new_x:.1f}, Y={new_y:.1f}", "info")
        except Exception as e:
            self.log(f"快捷平移出错:{str(e)}", "error")

    def reset_pan(self):
        """重置平移偏移"""
        try:
            self.x_offset.set(0.0)
            self.y_offset.set(0.0)
            self.update_axes_limits()
            self.safe_draw_graph(init=True)
            self.log(f"📱 平移已重置为(0.0, 0.0)", "info")
        except Exception as e:
            self.log(f"重置平移出错:{str(e)}", "error")

    def on_x_offset_changed(self, value):
        """X轴偏移变化时的统一处理"""
        try:
            # 限制平移范围
            x_val = float(value)
            x_val = max(-4, min(4, x_val))

            # 确保值在范围内
            if abs(self.x_offset.get() - x_val) > 0.01:
                self.x_offset.set(x_val)

            self.update_axes_limits()
            self.safe_draw_graph(init=True)
        except Exception as e:
            self.log(f"X轴平移出错:{str(e)}", "error")

    def on_y_offset_changed(self, value):
        """Y轴偏移变化时的统一处理"""
        try:
            # 限制平移范围
            y_val = float(value)
            y_val = max(-4, min(4, y_val))

            # 确保值在范围内
            if abs(self.y_offset.get() - y_val) > 0.01:
                self.y_offset.set(y_val)

            self.update_axes_limits()
            self.safe_draw_graph(init=True)
        except Exception as e:
            self.log(f"Y轴平移出错:{str(e)}", "error")

    # ========== 新增:字体大小控制回调函数 ==========
    def on_font_size_changed(self, value):
        """字体大小滑块变化时更新图形"""
        try:
            # 转换并限制字体大小在合法范围
            font_size = int(float(value))
            font_size = max(self.min_font_size, min(self.max_font_size, font_size))
            self.font_size_var.set(font_size)

            # 仅在非运行状态下更新图形(避免干扰着色动画)
            if not self.is_running and self.n > 0:
                self.safe_draw_graph(init=True)
                self.log(f"📝 节点字体大小已调整为:{font_size}号", "info")
        except Exception as e:
            self.log(f"调整字体大小出错:{str(e)}", "error")

    def reset_font_size(self):
        """重置字体大小为默认值(10号)"""
        try:
            self.font_size_var.set(10)
            if not self.is_running and self.n > 0:
                self.safe_draw_graph(init=True)
                self.log(f"📝 字体大小已重置为默认10号", "info")
        except Exception as e:
            self.log(f"重置字体大小出错:{str(e)}", "error")

    def safe_draw_graph(self, init=False):
        """安全的绘图方法,防止递归调用"""
        if self.is_drawing:
            return

        try:
            self.is_drawing = True
            self.draw_graph(init=init)
        finally:
            self.is_drawing = False

    def show_help(self):
        """显示使用帮助弹窗"""
        help_text = """
=== 图着色动态可视化APP 使用帮助 ===

一、基础操作
1. 输入图信息:
   - 顶点数(n):输入正整数(≥1)
   - 边数(m):输入非负整数(≥0)
   - 边信息:每行输入一对顶点(如"1 2"),表示两点间有边

2. 布局选择:
   - 圆形:顶点按圆形排列(默认)
   - 螺旋:顶点按螺旋状排列
   - 网格:顶点按网格状排列

3. 颜色自定义:
   - 点击颜色按钮可自定义8种颜色
   - 颜色0为未着色(固定白色)

二、动画控制
1. 开始着色:执行图着色回溯算法
2. 暂停/继续:暂停或继续动画演示
3. 重置:清空所有输入和状态
4. 速度调节:调整每步演示的间隔(100-2000ms)

三、视图控制
1. 缩放:放大/缩小/重置视图比例
2. 平移:
   - 滑动条:精确调整X/Y轴偏移
   - 快捷按钮:上/下/左/右快速平移
   - 重置平移:恢复默认位置
3. 字体大小:
   - 滑块:拖动调整节点内数字的字体大小(4-24号)
   - 重置字体:恢复默认10号字体

四、数据管理
1. 保存配置:将图信息、颜色、布局保存为JSON文件
2. 加载配置:从JSON文件恢复配置
3. 导出图片:将当前图形保存为PNG/JPG
4. 导出文本:将着色结果保存为TXT文件

五、算法说明
- 采用回溯法寻找最少颜色数的图着色方案
- 进度条显示当前处理进度
- 日志区显示每步操作详情

六、参考代码
1. C++代码
#include <iostream>
#include <vector>
using namespace std;

/**
 * 回溯验证是否能用k种颜色完成着色
 * @param v 当前处理的顶点编号(从1开始)
 * @param color 颜色数组,color[i]表示顶点i的颜色(0为未着色),传引用避免拷贝
 * @param adj 邻接矩阵(bool类型,adj[u][v]=true表示u和v相邻),传引用
 * @param k 尝试的颜色数
 * @param n 总顶点数
 * @return 能否用k种颜色完成着色
 */
bool is_valid(int v, vector<int>& color, vector<vector<bool>>& adj, int k, int n) {
    // 所有顶点(1~n)都完成着色,找到可行解
    if (v == n + 1) {
        return true;
    }

    // 尝试为当前顶点分配1~k的颜色
    for (int c = 1; c <= k; ++c) {
        bool conflict = false;
        // 检查已着色的相邻顶点(仅需检查前v-1个,后序顶点未着色)
        for (int i = 1; i < v; ++i) {
            // 若顶点i和v相邻,且i的颜色和当前尝试的颜色c相同,则冲突
            if (adj[v][i] && color[i] == c) {
                conflict = true;
                break;
            }
        }

        // 无冲突则分配颜色,并递归处理下一个顶点
        if (!conflict) {
            color[v] = c;
            // 递归处理下一个顶点,找到可行解则直接返回true
            if (is_valid(v + 1, color, adj, k, n)) {
                return true;
            }
            // 回溯:撤销当前顶点的颜色分配
            color[v] = 0;
        }
    }

    // 所有颜色都尝试过,无可行解
    return false;
}

int main() {
    // 输入顶点数n和边数m
    int n, m;
    cin >> n >> m;

    // 构建邻接矩阵(顶点编号1~n,初始化全为false)
    vector<vector<bool>> adj(n + 1, vector<bool>(n + 1, false));
    for (int i = 0; i < m; ++i) {
        int u, v;
        cin >> u >> v;
        adj[u][v] = true;
        adj[v][u] = true; // 无向图,双向标记邻接
    }

    // 枚举颜色数k(从1到n,n种颜色必能为每个顶点分配不同颜色)
    for (int k = 1; k <= n; ++k) {
        vector<int> color(n + 1, 0); // 初始化颜色数组,0表示未着色
        // 检查k种颜色是否能完成着色
        if (is_valid(1, color, adj, k, n)) {
            cout << k << endl;
            return 0; // 找到最小颜色数,直接退出程序
        }
    }

    // 理论上不会执行到此处(n种颜色一定可行)
    cout << n << endl;
    return 0;
}

2. Python代码
def is_valid(v, color, adj, k, n):
    '''
    回溯验证是否能用k种颜色完成着色
    v: 当前处理的顶点编号
    color: 颜色数组,color[i]表示顶点i的颜色(0为未着色)
    adj: 邻接矩阵
    k: 尝试的颜色数
    n: 总顶点数
    '''
    if v == n + 1:  # 所有顶点着色完成
        return True
    # 尝试为当前顶点分配1~k的颜色
    for c in range(1, k + 1):
        # 检查已着色的相邻顶点是否冲突(仅需检查前v-1个顶点,后序未着色)
        conflict = False
        for i in range(1, v):
            if adj[v][i] and color[i] == c:
                conflict = True
                break
        if not conflict:
            color[v] = c  # 分配颜色
            if is_valid(v + 1, color, adj, k, n):
                return True
            color[v] = 0  # 回溯,撤销颜色
    return False

# 输入处理
n, m = map(int, input().split())
# 构建邻接矩阵(顶点编号1~n)
adj = [[False] * (n + 1) for _ in range(n + 1)]
for _ in range(m):
    u, v = map(int, input().split())
    adj[u][v] = True
    adj[v][u] = True

# 枚举颜色数k(从1到n,n种颜色必可行)
for k in range(1, n + 1):
    color = [0] * (n + 1)  # 初始化颜色数组
    if is_valid(1, color, adj, k, n):
        print(k)
        exit()

# 理论上不会执行到此处(n种颜色可给每个顶点分配不同颜色)
print(n)

3. Java代码
import java.util.Scanner;

public class GraphColoring {
    /**
     * 回溯验证是否能用k种颜色完成着色(Java驼峰命名规范)
     * @param v 当前处理的顶点编号(从1开始)
     * @param color 颜色数组,color[i]表示顶点i的颜色(0为未着色),数组为引用传递
     * @param adj 邻接矩阵,adj[u][v]=true表示u和v相邻
     * @param k 尝试的颜色数
     * @param n 总顶点数
     * @return 能否用k种颜色完成着色
     */
    public static boolean isValid(int v, int[] color, boolean[][] adj, int k, int n) {
        // 所有顶点(1~n)都完成着色,找到可行解
        if (v == n + 1) {
            return true;
        }

        // 尝试为当前顶点分配1~k的颜色(对应Python的range(1, k+1))
        for (int c = 1; c <= k; c++) {
            boolean conflict = false;
            // 检查已着色的相邻顶点(仅需检查前v-1个,后序顶点未着色)
            for (int i = 1; i < v; i++) {
                // 若顶点i和v相邻,且i的颜色和当前尝试的颜色c相同,则冲突
                if (adj[v][i] && color[i] == c) {
                    conflict = true;
                    break;
                }
            }

            // 无冲突则分配颜色,并递归处理下一个顶点
            if (!conflict) {
                color[v] = c;
                // 递归处理下一个顶点,找到可行解则直接返回true
                if (isValid(v + 1, color, adj, k, n)) {
                    return true;
                }
                // 回溯:撤销当前顶点的颜色分配
                color[v] = 0;
            }
        }

        // 所有颜色都尝试过,无可行解
        return false;
    }

    public static void main(String[] args) {
        // 创建Scanner对象读取输入
        Scanner scanner = new Scanner(System.in);
        
        // 读取顶点数n和边数m
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        
        // 构建邻接矩阵(顶点编号1~n,Java数组默认初始值为false)
        boolean[][] adj = new boolean[n + 1][n + 1];
        for (int i = 0; i < m; i++) {
            int u = scanner.nextInt();
            int v = scanner.nextInt();
            adj[u][v] = true;
            adj[v][u] = true; // 无向图,双向标记邻接关系
        }
        scanner.close(); // 关闭输入流

        // 枚举颜色数k(从1到n,n种颜色必能为每个顶点分配不同颜色)
        for (int k = 1; k <= n; k++) {
            int[] color = new int[n + 1]; // 初始化颜色数组,默认值0(未着色)
            // 检查k种颜色是否能完成着色
            if (isValid(1, color, adj, k, n)) {
                System.out.println(k);
                return; // 找到最小颜色数,直接退出程序
            }
        }

        // 理论上不会执行到此处(n种颜色一定可行)
        System.out.println(n);
    }
}

=== 注意事项 ===
1. 顶点编号从1开始
2. 边信息需严格按"u v"格式输入
3. 缩放、平移、字体调整仅在非运行状态下生效
4. 建议顶点数不超过20,保证演示流畅
        """
        # 创建帮助窗口
        help_window = tk.Toplevel(self.root)
        help_window.title("使用帮助")
        help_window.geometry("600x700")
        help_window.resizable(True, True)
        help_window.transient(self.root)  # 置顶

        # 添加帮助文本
        help_text_widget = scrolledtext.ScrolledText(help_window, wrap=tk.WORD, font=("SimHei", 10))
        help_text_widget.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
        help_text_widget.insert(tk.END, help_text)
        help_text_widget.config(state=tk.DISABLED)  # 只读

        # 关闭按钮
        close_btn = ttk.Button(help_window, text="关闭", command=help_window.destroy)
        close_btn.pack(pady=10)

    def update_progress(self):
        """更新进度条和进度标签"""
        try:
            if self.n == 0:
                progress = 0.0
                label_text = "0% (0/0)"
            else:
                # 进度计算:当前处理顶点数/总顶点数 * 100%
                progress = (self.current_v / self.n) * 100.0
                # 限制进度在0-100之间
                progress = max(0.0, min(100.0, progress))
                label_text = f"{progress:.1f}% ({self.current_v}/{self.n})"

            self.progress_var.set(progress)
            self.progress_label.config(text=label_text)
            self.root.update_idletasks()
        except Exception as e:
            self.log(f"更新进度出错:{str(e)}", "error")

    # ====================== 布局相关函数 ======================
    def generate_circle_positions(self, n):
        """生成圆形布局坐标"""
        positions = {}
        radius = 2 if n <= 10 else 3  # 动态调整半径
        for i in range(1, n + 1):
            angle = 2 * math.pi * (i - 1) / n
            positions[i] = (math.cos(angle) * radius, math.sin(angle) * radius)
        return positions

    def generate_spiral_positions(self, n):
        """生成螺旋布局坐标"""
        positions = {}
        start_radius = 0.5
        radius_step = 0.3
        angle_step = math.pi / 4  # 45度步长
        current_angle = 0
        current_radius = start_radius

        for i in range(1, n + 1):
            x = current_radius * math.cos(current_angle)
            y = current_radius * math.sin(current_angle)
            positions[i] = (x, y)
            current_angle += angle_step
            current_radius += radius_step
        return positions

    def generate_grid_positions(self, n):
        """生成网格布局坐标"""
        positions = {}
        # 计算网格行列数(接近正方形)
        cols = math.ceil(math.sqrt(n))
        rows = math.ceil(n / cols)
        # 网格间距
        spacing = 1.0
        # 居中偏移
        offset_x = (cols - 1) * spacing / 2
        offset_y = (rows - 1) * spacing / 2

        for i in range(1, n + 1):
            row = (i - 1) // cols
            col = (i - 1) % cols
            x = col * spacing - offset_x
            y = row * spacing - offset_y
            positions[i] = (x, y)
        return positions

    def get_position_generator(self):
        """根据选择的布局类型返回对应的坐标生成函数"""
        layout = self.layout_type.get()
        if layout == "圆形":
            return self.generate_circle_positions
        elif layout == "螺旋":
            return self.generate_spiral_positions
        elif layout == "网格":
            return self.generate_grid_positions
        else:
            return self.generate_circle_positions

    def change_layout(self, event=None):
        """切换布局并重新绘制图形"""
        try:
            if self.n > 0 and not self.is_running:
                self.vertex_pos = self.get_position_generator()(self.n)
                self.safe_draw_graph(init=True)
                self.log(f"🔄 布局已切换为:{self.layout_type.get()}", "info")
        except Exception as e:
            self.log(f"切换布局出错:{str(e)}", "error")

    # ====================== 保存/加载配置 ======================
    def save_config(self):
        """保存配置到JSON文件"""
        try:
            # 收集配置数据
            config = {
                "n": self.n_entry.get().strip(),
                "m": self.m_entry.get().strip(),
                "edges": self.edge_text.get(1.0, tk.END).strip(),
                "color_map": self.color_map,
                "layout": self.layout_type.get(),
                "font_size": self.font_size_var.get()  # 新增:保存字体大小配置
            }

            # 选择保存路径
            file_path = filedialog.asksaveasfilename(
                defaultextension=".json",
                filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")],
                title="保存配置文件"
            )
            if not file_path:
                return

            # 写入文件
            with open(file_path, "w", encoding="utf-8") as f:
                json.dump(config, f, ensure_ascii=False, indent=4)

            self.log(f"✅ 配置已保存到:{os.path.basename(file_path)}", "success")

        except Exception as e:
            self.log(f"❌ 保存配置失败:{str(e)}", "error")

    def load_config(self):
        """从JSON文件加载配置"""
        try:
            # 选择文件
            file_path = filedialog.askopenfilename(
                filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")],
                title="加载配置文件"
            )
            if not file_path:
                return

            # 读取文件
            with open(file_path, "r", encoding="utf-8") as f:
                config = json.load(f)

            # 填充UI
            self.n_entry.delete(0, tk.END)
            self.n_entry.insert(0, config.get("n", ""))
            self.m_entry.delete(0, tk.END)
            self.m_entry.insert(0, config.get("m", ""))
            self.edge_text.delete(1.0, tk.END)
            self.edge_text.insert(1.0, config.get("edges", ""))

            # 恢复颜色映射
            if "color_map" in config:
                self.color_map = config["color_map"]
                for c_id in self.color_btns:
                    # 关键修复3:加载配置时正确更新style
                    style_name = f"Color{c_id}.TButton"
                    self.style.configure(style_name, background=self.color_map.get(c_id, DEFAULT_COLOR_MAP[c_id]))

            # 恢复布局
            if "layout" in config:
                self.layout_type.set(config["layout"])

            # ========== 恢复字体大小配置 ==========
            if "font_size" in config:
                font_size = int(config["font_size"])
                font_size = max(self.min_font_size, min(self.max_font_size, font_size))
                self.font_size_var.set(font_size)

            self.log(f"✅ 配置已从:{os.path.basename(file_path)} 加载", "success")

        except Exception as e:
            self.log(f"❌ 加载配置失败:{str(e)}", "error")

    # ====================== 导出结果 ======================
    def export_image(self):
        """导出当前图形为图片"""
        try:
            if self.n == 0:
                self.log("❌ 暂无图形可导出", "error")
                return

            # 选择保存路径
            file_path = filedialog.asksaveasfilename(
                defaultextension=".png",
                filetypes=[("PNG图片", "*.png"), ("JPG图片", "*.jpg"), ("所有文件", "*.*")],
                title="导出图形为图片"
            )
            if not file_path:
                return

            # 保存图片
            self.fig.savefig(file_path, dpi=150, bbox_inches="tight", pad_inches=0.1)
            self.log(f"✅ 图形已导出到:{os.path.basename(file_path)}", "success")

        except Exception as e:
            self.log(f"❌ 导出图片失败:{str(e)}", "error")

    def export_text(self):
        """导出着色结果为文本文件"""
        try:
            if self.n == 0:
                self.log("❌ 暂无结果可导出", "error")
                return

            # 收集结果数据
            result_text = f"图着色结果\n"
            result_text += f"====================\n"
            result_text += f"顶点数:{self.n}\n"
            result_text += f"边数:{self.m}\n"
            result_text += f"最少颜色数:{self.current_k if self.current_v == self.n + 1 else '未完成'}\n"
            result_text += f"节点字体大小:{self.font_size_var.get()}号\n"  # 新增:导出字体大小信息
            result_text += f"边信息:\n{self.edge_text.get(1.0, tk.END).strip()}\n"
            result_text += f"顶点颜色分配:\n"
            for i in range(1, self.n + 1):
                color_name = self.color_map.get(self.color[i], "未着色")
                result_text += f"顶点{i}:颜色{self.color[i]}({color_name})\n"

            # 选择保存路径
            file_path = filedialog.asksaveasfilename(
                defaultextension=".txt",
                filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
                title="导出结果为文本"
            )
            if not file_path:
                return

            # 写入文件
            with open(file_path, "w", encoding="utf-8") as f:
                f.write(result_text)

            self.log(f"✅ 结果已导出到:{os.path.basename(file_path)}", "success")

        except Exception as e:
            self.log(f"❌ 导出文本失败:{str(e)}", "error")

    # ====================== 核心功能 ======================
    def reset(self):
        """重置所有状态(包含视图控制+字体大小)"""
        try:
            # 停止调度和执行
            if self.after_id:
                self.root.after_cancel(self.after_id)
                self.after_id = None
            self.is_running = False
            self.is_paused = False

            # 清空输入和日志
            self.n_entry.delete(0, tk.END)
            self.m_entry.delete(0, tk.END)
            self.edge_text.delete(1.0, tk.END)
            self.log_text.delete(1.0, tk.END)

            # 重置核心数据
            self.n = 0
            self.m = 0
            self.adj = []
            self.color = []
            self.vertex_pos = {}
            self.current_k = 1
            self.current_v = 1
            self.current_c = 1
            self.vertex_patches = {}

            # 重置视图控制
            self.scale_factor.set(1.0)
            self.x_offset.set(0.0)
            self.y_offset.set(0.0)

            # ========== 重置字体大小 ==========
            self.font_size_var.set(10)

            # 重置进度条
            self.progress_var.set(0.0)
            self.progress_label.config(text="0% (0/0)")

            # 重置颜色映射为默认
            self.color_map = DEFAULT_COLOR_MAP.copy()
            for c_id in self.color_btns:
                # 重置时正确更新style
                style_name = f"Color{c_id}.TButton"
                self.style.configure(style_name, background=self.color_map[c_id])

            # 重置布局为圆形
            self.layout_type.set("圆形")

            # 重置画布和按钮状态
            self.init_plot()
            self.start_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)
            self.resume_btn.config(state=tk.DISABLED)

            self.log("🔄 所有状态已重置", "info")
        except Exception as e:
            self.log(f"重置出错:{str(e)}", "error")

    def choose_color(self, color_id):
        """自定义选择颜色"""
        try:
            # 关键修复5:选择颜色时正确更新style
            color = colorchooser.askcolor(title=f"选择颜色{color_id}", initialcolor=self.color_map[color_id])[1]
            if color:
                self.color_map[color_id] = color
                style_name = f"Color{color_id}.TButton"
                self.style.configure(style_name, background=color)
                if self.vertex_patches and self.n > 0:
                    self.safe_draw_graph()
                self.log(f"🎨 颜色{color_id}已更新为:{color}", "info")
        except Exception as e:
            self.log(f"选择颜色出错:{str(e)}", "error")

    def parse_input(self):
        """解析输入,根据选择的布局生成坐标"""
        try:
            self.n = int(self.n_entry.get())
            self.m = int(self.m_entry.get())
            if self.n < 1 or self.m < 0:
                raise ValueError("顶点数≥1,边数≥0")

            # 初始化邻接矩阵
            self.adj = [[False] * (self.n + 1) for _ in range(self.n + 1)]
            self.color = [0] * (self.n + 1)

            # 解析边
            edge_lines = self.edge_text.get(1.0, tk.END).strip().split("\n")
            edge_lines = [line for line in edge_lines if line.strip()]
            if len(edge_lines) != self.m:
                raise ValueError(f"边数输入错误!应输入{self.m}条,实际输入{len(edge_lines)}条")

            for line in edge_lines:
                u, v = map(int, line.strip().split())
                if u < 1 or u > self.n or v < 1 or v > self.n:
                    raise ValueError(f"顶点{u}/{v}超出范围(1~{self.n})")
                self.adj[u][v] = True
                self.adj[v][u] = True

            # 根据选择的布局生成坐标
            self.vertex_pos = self.get_position_generator()(self.n)
            self.log(f"✅ 使用{self.layout_type.get()}布局,顶点数:{self.n}", "info")

            # 绘制初始图
            self.safe_draw_graph(init=True)
            # 初始化进度条
            self.progress_var.set(0.0)
            self.progress_label.config(text=f"0.0% (0/{self.n})")
            return True

        except ValueError as e:
            self.log(f"输入错误:{str(e)}", "error")
            self.start_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)
            self.resume_btn.config(state=tk.DISABLED)
            return False
        except Exception as e:
            self.log(f"解析输入出错:{str(e)}", "error")
            self.start_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)
            self.resume_btn.config(state=tk.DISABLED)
            return False

    def draw_graph(self, init=False):
        """绘图(适配多布局+缩放平移+自定义字体大小)"""
        try:
            if init:
                self.ax.clear()
                self.update_axes_limits()  # 应用缩放和平移
                self.ax.axis("off")
                self.ax.set_aspect("equal")

                # 绘制边
                for u in range(1, self.n + 1):
                    for v in range(u + 1, self.n + 1):
                        if self.adj[u][v]:
                            x1, y1 = self.vertex_pos[u]
                            x2, y2 = self.vertex_pos[v]
                            self.ax.plot([x1, x2], [y1, y2], color="gray", linewidth=1.5, zorder=1)

                # 初始化顶点(动态调整大小+自定义字体)
                self.vertex_patches = {}
                # 根据缩放因子调整顶点大小
                scale = self.scale_factor.get()
                base_radius = 0.15 if self.n > 15 else 0.25 if self.n > 10 else 0.35
                radius = base_radius * scale

                # ========== 使用自定义字体大小 ==========
                # 获取手动设置的字体大小,结合缩放因子适配(保证缩放时字体同步)
                manual_font_size = self.font_size_var.get()
                fontsize = manual_font_size * scale
                # 最终限制字体范围,避免极端值
                fontsize = max(self.min_font_size, min(self.max_font_size, fontsize))

                for i in range(1, self.n + 1):
                    x, y = self.vertex_pos[i]
                    color = self.color_map[self.color[i]] if self.color[i] in self.color_map else "white"
                    # 将color改为facecolor,避免覆盖edgecolor的警告
                    circle = plt.Circle((x, y), radius, facecolor=color, edgecolor="black", linewidth=2, zorder=2)
                    self.ax.add_patch(circle)
                    self.vertex_patches[i] = circle
                    self.ax.text(x, y, str(i), ha="center", va="center", fontsize=fontsize, fontweight="bold", zorder=3)
            else:
                # 仅更新顶点颜色
                for i in range(1, self.n + 1):
                    target_color = self.color_map[self.color[i]] if self.color[i] in self.color_map else "white"
                    # 将颜色名称转为RGBA数组(统一格式)
                    current_rgba = self.vertex_patches[i].get_facecolor()
                    target_rgba = mcolors.to_rgba(target_color)
                    # 浮点数数组比较(容错1e-6,避免精度问题)
                    if not np.allclose(current_rgba, target_rgba, atol=1e-6):
                        self.vertex_patches[i].set_facecolor(target_color)

            self.canvas.draw_idle()
        except Exception as e:
            self.log(f"绘图出错:{str(e)}", "error")

    def log(self, msg, level="info"):
        """日志输出"""
        try:
            timestamp = time.strftime("%H:%M:%S")
            self.log_text.insert(tk.END, f"[{timestamp}] {msg}\n")
            self.log_text.tag_add(level, self.log_text.index("end-2l"), self.log_text.index("end-1l"))
            self.log_text.see(tk.END)
            self.log_text.update_idletasks()
        except Exception as e:
            print(f"日志输出出错:{e}")

    def check_conflict(self, v, c):
        """检查颜色冲突"""
        try:
            for i in range(1, v):
                if self.adj[v][i] and self.color[i] == c:
                    return True
            return False
        except Exception as e:
            self.log(f"检查颜色冲突出错:{str(e)}", "error")
            return True

    def color_step(self):
        """分步执行着色逻辑(新增进度更新)"""
        try:
            if not self.is_running or self.is_paused:
                return

            # 更新进度条
            self.update_progress()

            # 1. 所有顶点处理完成
            if self.current_v == self.n + 1:
                self.log(f"✅ 找到可行解!最少颜色数:{self.current_k}", "success")
                self.progress_var.set(100.0)
                self.progress_label.config(text=f"100.0% ({self.n}/{self.n})")
                self.is_running = False
                self.start_btn.config(state=tk.NORMAL)
                self.pause_btn.config(state=tk.DISABLED)
                self.resume_btn.config(state=tk.DISABLED)
                return

            # 2. 尝试当前颜色
            if self.current_c <= self.current_k:
                conflict = self.check_conflict(self.current_v, self.current_c)
                if conflict:
                    self.log(f"❌ 顶点{self.current_v}尝试颜色{self.current_c}:与相邻顶点冲突")
                    self.current_c += 1
                else:
                    self.color[self.current_v] = self.current_c
                    self.log(f"✅ 顶点{self.current_v}分配颜色{self.current_c}:无冲突")
                    self.safe_draw_graph()
                    # 处理下一个顶点
                    self.current_v += 1
                    self.current_c = 1
            else:
                # 3. 回溯逻辑
                self.log(f"🔙 顶点{self.current_v}回溯:撤销所有颜色,返回上一层")
                self.color[self.current_v] = 0
                self.safe_draw_graph()
                self.current_v -= 1
                if self.current_v < 1:
                    # 尝试下一个颜色数
                    self.log(f"❌ 颜色数k={self.current_k}不可行,尝试k={self.current_k + 1}")
                    self.current_k += 1
                    self.current_v = 1
                    self.current_c = 1
                    self.color = [0] * (self.n + 1)
                    self.safe_draw_graph()
                else:
                    self.current_c = self.color[self.current_v] + 1
                    self.color[self.current_v] = 0

            # 异步调度下一步(添加异常处理)
            try:
                if self.is_running and not self.is_paused:
                    self.after_id = self.root.after(self.speed_var.get(), self.color_step)
            except Exception as e:
                self.log(f"调度下一步出错:{str(e)}", "error")
        except Exception as e:
            self.log(f"着色步骤出错:{str(e)}", "error")
            self.is_running = False
            self.start_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)
            self.resume_btn.config(state=tk.DISABLED)

    def start_coloring(self):
        """启动着色"""
        try:
            self.start_btn.config(state=tk.DISABLED)
            self.pause_btn.config(state=tk.NORMAL)
            self.resume_btn.config(state=tk.DISABLED)

            if not self.parse_input():
                self.start_btn.config(state=tk.NORMAL)
                self.pause_btn.config(state=tk.DISABLED)
                return

            self.is_running = True
            self.is_paused = False
            self.current_k = 1
            self.current_v = 1
            self.current_c = 1
            self.color = [0] * (self.n + 1)

            self.log("开始执行图着色算法...")
            self.log(f"图信息:顶点数={self.n},边数={self.m}")
            self.log(f"\n===== 尝试颜色数k={self.current_k} =====")

            self.after_id = self.root.after(100, self.color_step)
        except Exception as e:
            self.log(f"启动着色出错:{str(e)}", "error")
            self.start_btn.config(state=tk.NORMAL)
            self.pause_btn.config(state=tk.DISABLED)

    def pause_coloring(self):
        """暂停着色"""
        try:
            if self.is_running and not self.is_paused:
                self.is_paused = True
                self.pause_btn.config(state=tk.DISABLED)
                self.resume_btn.config(state=tk.NORMAL)
                self.log("⏸️  动画已暂停", "info")
        except Exception as e:
            self.log(f"暂停出错:{str(e)}", "error")

    def resume_coloring(self):
        """继续着色"""
        try:
            if self.is_running and self.is_paused:
                self.is_paused = False
                self.pause_btn.config(state=tk.NORMAL)
                self.resume_btn.config(state=tk.DISABLED)
                self.log("▶️  动画已继续", "info")
                self.color_step()
        except Exception as e:
            self.log(f"继续出错:{str(e)}", "error")


if __name__ == "__main__":
    # 高DPI优化
    plt.rcParams['figure.dpi'] = 100
    plt.rcParams['savefig.dpi'] = 150
    plt.rcParams['font.sans-serif'] = ['SimHei']  # 解决中文显示问题
    plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题

    root = tk.Tk()
    try:
        root.tk.call('tk', 'scaling', 1.2)
    except:
        pass

    # 设置matplotlib后端
    matplotlib.use('TkAgg')

    app = GraphColoringApp(root)
    root.mainloop()

四、程序运行部分截图展示

最终执行日志

bash 复制代码
[20:36:33] ✅ 配置已保存到:color_config1.json
[20:36:40] 🎨 颜色1已更新为:#ff0000
[20:37:51] ✅ 使用圆形布局,顶点数:5
[20:37:51] 开始执行图着色算法...
[20:37:51] 图信息:顶点数=5,边数=8
[20:37:51] 
===== 尝试颜色数k=1 =====
[20:37:52] ✅ 顶点1分配颜色1:无冲突
[20:37:52] ❌ 顶点2尝试颜色1:与相邻顶点冲突
[20:37:53] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:37:54] 🔙 顶点1回溯:撤销所有颜色,返回上一层
[20:37:54] ❌ 颜色数k=1不可行,尝试k=2
[20:37:55] ✅ 顶点1分配颜色1:无冲突
[20:37:56] ❌ 顶点2尝试颜色1:与相邻顶点冲突
[20:37:56] ✅ 顶点2分配颜色2:无冲突
[20:37:57] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:37:58] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:37:59] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:00] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:38:00] ✅ 顶点1分配颜色2:无冲突
[20:38:01] ✅ 顶点2分配颜色1:无冲突
[20:38:02] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:38:03] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:38:04] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:05] ❌ 顶点2尝试颜色2:与相邻顶点冲突
[20:38:05] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:38:06] 🔙 顶点1回溯:撤销所有颜色,返回上一层
[20:38:06] ❌ 颜色数k=2不可行,尝试k=3
[20:38:07] ✅ 顶点1分配颜色1:无冲突
[20:38:08] ❌ 顶点2尝试颜色1:与相邻顶点冲突
[20:38:09] ✅ 顶点2分配颜色2:无冲突
[20:38:09] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:38:10] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:38:11] ✅ 顶点3分配颜色3:无冲突
[20:38:12] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:13] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:13] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:14] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:15] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:16] ✅ 顶点2分配颜色3:无冲突
[20:38:17] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:38:18] ✅ 顶点3分配颜色2:无冲突
[20:38:18] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:19] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:20] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:21] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:22] ❌ 顶点3尝试颜色3:与相邻顶点冲突
[20:38:22] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:23] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:38:24] ✅ 顶点1分配颜色2:无冲突
[20:38:25] ✅ 顶点2分配颜色1:无冲突
[20:38:26] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:38:26] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:38:27] ✅ 顶点3分配颜色3:无冲突
[20:38:28] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:29] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:30] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:30] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:31] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:32] ❌ 顶点2尝试颜色2:与相邻顶点冲突
[20:38:33] ✅ 顶点2分配颜色3:无冲突
[20:38:34] ✅ 顶点3分配颜色1:无冲突
[20:38:35] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:35] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:36] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:37] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:38] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:38:39] ❌ 顶点3尝试颜色3:与相邻顶点冲突
[20:38:39] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:40] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:38:41] ✅ 顶点1分配颜色3:无冲突
[20:38:42] ✅ 顶点2分配颜色1:无冲突
[20:38:43] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:38:43] ✅ 顶点3分配颜色2:无冲突
[20:38:44] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:45] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:46] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:47] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:47] ❌ 顶点3尝试颜色3:与相邻顶点冲突
[20:38:48] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:49] ✅ 顶点2分配颜色2:无冲突
[20:38:50] ✅ 顶点3分配颜色1:无冲突
[20:38:51] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:38:52] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:38:52] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:38:53] 🔙 顶点4回溯:撤销所有颜色,返回上一层
[20:38:54] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:38:55] ❌ 顶点3尝试颜色3:与相邻顶点冲突
[20:38:56] 🔙 顶点3回溯:撤销所有颜色,返回上一层
[20:38:56] ❌ 顶点2尝试颜色3:与相邻顶点冲突
[20:38:57] 🔙 顶点2回溯:撤销所有颜色,返回上一层
[20:38:58] 🔙 顶点1回溯:撤销所有颜色,返回上一层
[20:38:58] ❌ 颜色数k=3不可行,尝试k=4
[20:38:59] ✅ 顶点1分配颜色1:无冲突
[20:39:00] ❌ 顶点2尝试颜色1:与相邻顶点冲突
[20:39:01] ✅ 顶点2分配颜色2:无冲突
[20:39:01] ❌ 顶点3尝试颜色1:与相邻顶点冲突
[20:39:02] ❌ 顶点3尝试颜色2:与相邻顶点冲突
[20:39:03] ✅ 顶点3分配颜色3:无冲突
[20:39:04] ❌ 顶点4尝试颜色1:与相邻顶点冲突
[20:39:05] ❌ 顶点4尝试颜色2:与相邻顶点冲突
[20:39:05] ❌ 顶点4尝试颜色3:与相邻顶点冲突
[20:39:06] ✅ 顶点4分配颜色4:无冲突
[20:39:07] ✅ 顶点5分配颜色1:无冲突
[20:39:08] ✅ 找到可行解!最少颜色数:4
[20:40:28] 🔍 视图缩放:1.1倍
[20:40:29] 🔍 视图缩放:1.2倍
[20:40:29] 🔍 视图缩放:1.3倍
[20:40:29] 🔍 视图缩放:1.4倍
[20:40:29] 🔍 视图缩放:1.5倍
[20:40:34] 🔍 视图缩放:1.6倍
[20:41:03] 🔍 视图缩放:1.5倍
[20:41:04] 🔍 视图缩放:1.4倍
[20:41:04] 🔍 视图缩放:1.3倍
[20:41:04] 🔍 视图缩放:1.2倍
[20:41:04] 🔍 视图缩放:1.1倍
[20:41:04] 🔍 视图缩放:1.0倍
[20:41:05] 🔍 视图缩放:0.9倍
[20:41:05] 🔍 视图缩放:0.8倍
[20:41:05] 🔍 视图缩放:0.7倍
[20:41:05] 🔍 视图缩放:0.6倍
[20:41:05] 🔍 视图缩放:0.5倍
[20:41:06] 🔍 视图缩放:0.4倍
[20:41:06] 🔍 视图缩放:0.3倍
[20:41:11] 🔍 视图缩放:0.4倍
[20:41:50] 🔍 视图缩放:0.5倍
[20:41:53] 🔍 视图缩放:0.4倍
[20:41:54] 🔍 视图缩放:0.3倍
[20:41:58] 🔍 视图缩放:0.4倍
[20:41:58] 🔍 视图缩放:0.5倍
[20:41:59] 🔍 视图缩放:0.4倍
[20:44:07] ✅ 图形已导出到:graph_color1.png
[20:44:43] ✅ 结果已导出到:graph_color1.txt

五、总结

本文介绍了一个基于Python+Tkinter+Matplotlib的图着色算法动态可视化工具。该工具采用回溯法求解无向图的最小顶点着色数,通过交互式界面实现算法过程的可视化展示。核心功能包括:

(1) 输入图信息并构建邻接矩阵;

(2) 实现回溯法分步着色;

(3) 提供圆形、螺旋和网格三种布局;

(4) 支持8种颜色自定义;

(5) 动画控制(暂停/继续/调速);

(6) 视图缩放平移;

(7) 配置保存/加载和结果导出。

开发过程遵循模块化设计,解决了TTK按钮背景色、递归算法可视化等关键技术难点,最终实现了一个直观展示图着色算法原理的教学工具。

相关推荐
youngee112 小时前
hot100-53搜索旋转排序数组
数据结构·算法·leetcode
南风微微吹2 小时前
【2026年3月最新】计算机二级Python题库下载安装教程~共19套真题
开发语言·python·计算机二级python
烟雨梵兮2 小时前
-刷题小结19
算法
阿蔹2 小时前
Python基础语法三---函数和数据容器
开发语言·python
xingzhemengyou12 小时前
Python 多线程同步
开发语言·python
爱学大树锯2 小时前
1361 · 文字并排
算法
Tisfy3 小时前
LeetCode 2483.商店的最少代价:两次遍历 -> 一次遍历
算法·leetcode·题解·遍历
YGGP3 小时前
【Golang】LeetCode 279. 完全平方数
算法·leetcode
3824278273 小时前
python3网络爬虫开发实战 第二版:绑定回调
开发语言·数据库·python