目录
[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表示顶点u和v是相邻的;adj[u][v] = False表示顶点u和v不相邻。
步骤 2:枚举颜色数k
从最小的可能值(k=1)开始尝试,直到k=n(n是顶点数,因为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 骨架 + 核心数据初始化)
目标:完成窗口布局和核心数据定义,为后续功能打基础。
- 窗口初始化:设置窗口标题、尺寸,启用高 DPI 适配(解决模糊问题);
- 布局拆分:将界面分为 "左侧功能区" 和 "右侧可视化区",左侧进一步拆分为输入、布局、颜色、控制、进度、日志、视图控制 7 个子区域,保证 UI 层次清晰;
- 核心数据初始化 :定义顶点数
n、边数m、邻接矩阵adj、颜色数组color、视图参数(缩放因子scale_factor、平移偏移x/y_offset)等变量,设置默认值; - Matplotlib 嵌入 :创建
Figure和Canvas,将绘图区域嵌入 Tkinter 右侧窗口,初始化空画布。
阶段 2:核心算法实现(回溯法图着色)
目标:将递归的图着色算法改为 "分步执行",适配可视化需求。
- 算法核心逻辑(回溯法) :
- 核心思路:从 1 种颜色开始尝试,若无法完成所有顶点着色则递增颜色数,直到找到最小可行颜色数;
- 关键方法:
check_conflict(v, c)检查顶点v分配颜色c是否与相邻顶点冲突; - 适配可视化:放弃递归直接执行,改为
color_step分步执行(每一步仅处理一个顶点 / 颜色),通过root.after()调度下一步,实现 "动画效果"。
- 输入解析 :
parse_input方法校验顶点数 / 边数合法性,解析边信息构建邻接矩阵,抛出友好的错误提示; - 进度与日志 :
update_progress实时更新进度条(按当前处理顶点数 / 总顶点数计算);log方法输出带时间戳的日志,区分error/info/success标签,提升可读性。
阶段 3:可视化功能开发(绘图 + 布局 + 颜色自定义)
目标:将算法执行过程 "可视化",让每一步操作可见。
- 基础绘图 :
draw_graph方法分为 "初始化绘图"(绘制边、顶点)和 "增量更新"(仅更新顶点颜色),避免重复绘制提升性能;- 顶点用
plt.Circle绘制,边用plt.plot绘制,通过zorder控制层级(边 < 顶点 < 文字);
- 多布局支持 :
- 实现
generate_circle/spiral/grid_positions三个布局函数,根据顶点数动态计算坐标(如圆形布局按角度均分,网格布局接近正方形); change_layout方法绑定下拉框事件,切换布局时重新计算坐标并重绘;
- 实现
- 颜色自定义 :
- 初始化
DEFAULT_COLOR_MAP作为默认颜色配置,支持用户通过colorchooser修改; - 解决 TTK 按钮背景色自定义问题:通过
ttk.Style配置按钮样式(Color{c_id}.TButton),绑定颜色选择事件。
- 初始化
阶段 4:动画与交互控制(核心体验优化)
目标:让可视化过程可控制、易操作。
- 动画控制 :
start_coloring:解析输入→初始化状态→启动分步着色(after调度color_step);pause/resume_coloring:通过is_paused状态变量控制动画暂停 / 继续;- 速度控制:用
Scale组件绑定speed_var,调整after的延迟时间;
- 视图控制 :
- 缩放:通过
scale_factor调整顶点大小和坐标轴范围,限制缩放范围(0.2~3.0)避免极端效果; - 平移:通过
x/y_offset调整坐标轴偏移,支持滑动条和快捷按钮(上下左右)两种操作方式; - 字体大小:新增
font_size_var,绑定滑块事件,适配缩放因子保证字体同步调整;
- 缩放:通过
- 重置功能 :
reset方法清空所有输入、状态、视图参数,恢复初始状态,避免重启程序。
阶段 5:数据管理与结果导出(完善功能闭环)
目标:支持配置复用和结果留存。
- 配置保存 / 加载 :
save_config:将顶点数、边数、边信息、颜色映射、布局、字体大小序列化为 JSON,通过filedialog保存到本地;load_config:读取 JSON 文件,恢复 UI 控件值和核心数据,重新绘制图形;
- 结果导出 :
export_image:调用 Matplotlib 的savefig保存当前可视化图形为 PNG/JPG;export_text:将图信息、着色结果、字体大小等写入 TXT 文件;
- 使用帮助 :
show_help方法创建置顶弹窗,提供详细的操作指南和算法参考代码(C++/Python/Java),降低使用门槛。
阶段 6:异常处理与边界条件优化
目标:提升程序稳定性和用户体验。
- 输入校验:检查顶点数(≥1)、边数(≥0)、边格式(u v)、顶点范围(1~n),抛出明确的错误提示;
- 防递归调用 :添加
is_drawing锁,避免视图操作时重复调用绘图函数导致卡顿; - 颜色精度处理 :用
np.allclose比较 RGBA 颜色数组(容错 1e-6),避免浮点数精度问题导致的无效更新; - 边界限制:缩放 / 平移 / 字体大小设置上下限,防止参数越界;
- 资源清理 :
on_closing方法取消after调度、关闭 Matplotlib 画布,避免内存泄漏; - 日志容错:日志输出添加 try-except,防止日志报错导致主程序崩溃。
5. 关键技术难点与解决方案
开发过程中遇到的核心问题及解决思路:
难点 1:TTK 按钮无法直接设置背景色
- 问题:TTK 控件为了适配系统主题,默认屏蔽了
background参数; - 解决方案:创建自定义
Style(如Color{c_id}.TButton),通过style.configure配置按钮背景色,绑定到对应按钮。
难点 2:递归算法改为分步动画
- 问题:原生回溯法是递归执行,无法分步可视化;
- 解决方案:
- 将递归逻辑拆解为
color_step分步函数,用状态变量(current_v当前顶点、current_c当前颜色、current_k当前尝试颜色数)记录执行位置; - 用
root.after()异步调度下一步执行,实现 "一步一绘" 的动画效果; - 用
is_running/is_paused控制执行状态,支持暂停 / 继续。
- 将递归逻辑拆解为
难点 3:Matplotlib 嵌入 Tkinter 后的视图控制
- 问题:缩放 / 平移需要同步更新图形元素(顶点大小、字体、坐标轴);
- 解决方案:
- 维护
scale_factor(缩放因子)和x/y_offset(平移偏移); - 每次视图变化时调用
update_axes_limits更新坐标轴范围; - 绘图时根据缩放因子调整顶点半径、字体大小,保证视觉一致性。
- 维护
难点 4:颜色更新的无效渲染
- 问题:每次着色步骤都重绘整个图形,导致卡顿;
- 解决方案:
- 绘图函数
draw_graph区分init=True(全量绘制)和init=False(仅更新顶点颜色); - 仅当顶点颜色实际变化时,才更新
Circle的facecolor,减少无效绘制。
- 绘图函数
6. 迭代优化:从功能可用到体验优秀
最终版本在基础功能上的优化点:
- 中文适配 :设置 Matplotlib 字体为
SimHei,解决中文日志 / 标签显示乱码; - 动态顶点大小:根据顶点数调整基础半径(顶点数越多,半径越小);
- 进度条精准度:按当前处理顶点数计算进度,而非颜色尝试次数;
- 操作反馈:所有交互(如布局切换、缩放)都输出日志,告知用户操作结果;
- 高 DPI 适配 :调用
root.tk.call('tk', 'scaling', 1.2)提升高分辨率屏幕下的 UI 清晰度。
7. 总结
这个图着色可视化工具的开发过程遵循 "需求驱动→模块化设计→分阶段实现→难点攻坚→体验优化" 的思路,核心是:
- 将复杂算法拆解为可可视化的分步逻辑,平衡算法完整性和交互体验;
- 围绕 "用户操作" 设计 UI 和交互,兼顾新手友好性和功能灵活性;
- 注重细节优化(如颜色精度、资源清理、边界限制),提升程序稳定性。
最终实现的工具不仅能直观展示图着色算法的核心逻辑(回溯、冲突检查、最小颜色数求解),还通过丰富的交互功能降低了算法学习和演示的门槛,符合 "可视化教学工具" 的核心定位。
三、图着色动态可视化 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按钮背景色、递归算法可视化等关键技术难点,最终实现了一个直观展示图着色算法原理的教学工具。