数学建模实战:TOPSIS综合评价与模拟退火算法在旅游线路规划中的应用

📅 发布时间:2026/8/22 20:50:52
数学建模实战:TOPSIS综合评价与模拟退火算法在旅游线路规划中的应用 1. 项目概述一次从问题到代码的完整建模实战去年带队参加长三角高校数学建模竞赛的经历至今记忆犹新。那年的A题“Go!Fun游长三角”本质上是一个典型的“资源受限下的多目标路径规划与评价”问题。题目要求参赛者为游客设计一条在长三角多个城市间游览的线路不仅要考虑时间、预算等硬性约束还要兼顾游玩体验、文化特色等软性指标。这听起来像是个旅行规划问题但在数学建模的语境下它迅速被我们拆解为几个核心子问题景点吸引力综合评价、旅行商路径优化、以及在动态预算下的行程调整。最终我们融合了TOPSIS综合评价、TSP问题优化和动态规划思想交出了一份逻辑自洽的解决方案。今天我就把当时的解题全过程包括核心思路、模型构建的纠结与取舍、以及那些在论文里不会写的调试“血泪史”完整地复盘出来。无论你是正在备战数模的新手还是对运筹优化感兴趣的朋友相信这份从“一张白纸”到“可运行程序”的一手经验都能给你带来一些实实在在的启发。2. 核心问题拆解与建模思路确立面对“设计旅游线路”这样一个开放性问题第一步也是最关键的一步就是将其“翻译”成数学语言。我们团队拿到题目后没有急于寻找算法而是花了近一个小时进行“问题界定”。2.1 需求解析从游客视角到数学模型题目描述中隐含了多重且可能冲突的目标。游客既想“玩得好”体验最大化又想“花钱少”、“时间省”。此外长三角景点众多不可能全部游览必须有所选择。因此我们首先明确了问题的几个核心维度景点评价维度如何量化一个景点的“好坏”这不仅仅是名气大小还需综合考虑门票价格、预计游玩时间、历史文化价值、自然风光、交通便利度等多个指标。这些指标量纲不同价格是元时间是小时价值是评分且有正向价值越高越好和负向价格越低越好之分。这直接指向了多指标综合评价问题。路径规划维度选定一系列心仪的景点后如何安排游览顺序使得总交通耗时或成本最低这本质上是一个**旅行商问题TSP**的变种。但与传统TSP不同我们的“城市”是景点且景点间距离需考虑实际的交通方式高铁、汽车等和班次。资源分配维度在总时间和总预算的双重约束下如何对初步规划的线路进行“精修”例如当某个景点耗时超预期或某个城市消费过高时是替换景点、调整顺序还是压缩在其他项目上的时间这需要一种灵活的决策与优化方法。基于以上拆解我们确立了“先评价后规划再优化”的三阶段建模策略。这个策略的优势在于模块化每个阶段相对独立可以使用最合适的模型最后再将结果串联起来。2.2 模型选型背后的“为什么”为什么用AHP/熵权TOPSIS而不是简单加权平均为什么用动态规划处理预算而不是线性规划这些选择背后都有具体的考量。对于景点评价AHP 熵权TOPSIS最初我们考虑过简单的加权评分法但很快否定了。因为各指标权重的主观性太强直接给定权重缺乏说服力。层次分析法AHP的优势在于它通过两两比较判断矩阵将人的主观判断以相对科学的方式量化尤其适合处理“文化价值”、“体验感”这类难以直接测量的指标。我们用它来确定“体验类”指标如文化、风景的权重。 但同时我们也意识到像“价格”、“时间”这类客观数据本身包含信息量。熵权法正好可以根据各指标数据的离散程度自动赋权离散程度越大即该指标在不同景点间差异越大其权重越高说明该指标在区分景点优劣时越有效。这是一种客观赋权法。 因此我们采用了主客观组合赋权用AHP得到主观权重W_subjective用熵权法得到客观权重W_objective最终综合权重W α * W_subjective (1-α) * W_objective其中α是一个平衡系数我们通过灵敏度分析设为0.6。最后将综合权重带入TOPSIS逼近理想解排序法计算每个景点与正理想解虚构的最佳景点和负理想解虚构的最差景点的距离从而得到每个景点的相对贴近度作为最终评分。TOPSIS的好处是直观结果易于解释且能充分利用原始数据信息。注意AHP中判断矩阵的一致性检验CR0.1必须通过否则需要调整判断。这是我们遇到的第一个坑后面会详细说。对于路径规划基于图论的启发式算法经典的TSP是一个NP-hard问题。对于长三角这种可能涉及20-30个景点的规模精确算法如分支定界耗时太长。我们选择了模拟退火算法SA来求解。SA的优势在于能有效避免陷入局部最优且对初始解不敏感。我们以景点间的交通时间作为边权构建完全图SA的目标函数就是总旅行时间最小化。 这里的一个关键细节是邻域搜索操作的设计。我们采用了“2-opt”局部搜索即随机反转路径中一段子序列的顺序作为产生新解的方式。实践证明这在路径优化中非常高效。对于预算与时间优化动态规划DP思想在初步路径生成后我们需要检查是否超预算或超时。如果超了就需要调整。我们将每个景点视为一个“阶段”在每个阶段景点游客有两种“状态”当前已花费的金额和已使用的时间。我们从起点开始逐步“决策”是否深入游玩该景点花费更多时间和金钱获得更高体验评分还是浅尝辄止基础消费目标是最终到达终点时在总预算和总时间限制内获得的体验评分总和最大。 这构成了一个双约束条件的资源分配问题。我们采用动态规划的思想用dp[i][cost][time]表示游览到第i个景点、累计花费cost、累计用时time时所能获得的最大体验评分之和。通过状态转移最终找到最优解。虽然严格实现二维DP状态可能较多但通过合理的离散化如将金钱按100元离散时间按0.5小时离散和剪枝剔除明显不可行的状态问题规模是可控的。3. 核心模型构建与实现细节思路清晰后就进入了具体的模型实现环节。这里充斥着大量的细节和“坑”。3.1 数据预处理与指标体系的建立我们收集了长三角地区约50个知名景点的数据包括门票价格、建议游览时间、交通枢纽距离、以及在各大旅游平台的评分拆分为风景、文化、设施等子项。数据来源主要是公开的旅游网站和地图API。第一步是数据标准化。对于效益型指标评分越高越好我们采用(x - min) / (max - min)对于成本型指标价格、时间越低越好采用(max - x) / (max - min)。这一步至关重要它消除了量纲影响使所有指标处于同一尺度。第二步是AHP权重的确定。我们团队三人背对背地填写判断矩阵然后汇总讨论。例如在“体验”维度下比较“文化价值”和“自然风光”哪个稍微重要我们采用1-9标度法。这个过程很容易出现不一致。比如A认为文化比风光重要3倍B认为风光比设施重要2倍C认为文化比设施重要4倍那么从A和B的判断推导文化应比设施重要6倍这与C的4倍矛盾。这就需要反复调整直到一致性比率CR满足要求。我们使用了一个简单的Python函数来辅助计算特征向量和CR值。import numpy as np def ahp_weight(matrix): 计算AHP判断矩阵的权重向量和一致性指标 matrix: n*n 的判断矩阵 n matrix.shape[0] # 计算特征值和特征向量 eigenvalues, eigenvectors np.linalg.eig(matrix) max_eigenvalue np.max(eigenvalues.real) idx np.argmax(eigenvalues.real) weight_vector eigenvectors[:, idx].real weight_vector weight_vector / weight_vector.sum() # 归一化 # 一致性检验 CI (max_eigenvalue - n) / (n - 1) RI [0, 0, 0.58, 0.90, 1.12, 1.24, 1.32, 1.41, 1.45, 1.49] # 平均随机一致性指标 CR CI / RI[n-1] if n-1 len(RI) else 0 return weight_vector, CR3.2 TOPSIS综合评价的实现在得到组合权重W后TOPSIS的实现就相对直接了。核心是计算加权规范化矩阵然后找到正负理想解。def topsis(data, weights): data: m*n 矩阵m个评价对象n个评价指标已标准化 weights: n维权重向量 # 计算加权规范化矩阵 weighted_data data * weights # 确定正理想解和负理想解 ideal_best weighted_data.max(axis0) # 效益型取最大 ideal_worst weighted_data.min(axis0) # 效益型取最小 # 注意如果指标方向不一致这里需要根据指标类型调整 # 计算各方案到正负理想解的距离 dist_best np.sqrt(((weighted_data - ideal_best) ** 2).sum(axis1)) dist_worst np.sqrt(((weighted_data - ideal_worst) ** 2).sum(axis1)) # 计算相对贴近度 score dist_worst / (dist_best dist_worst) return score我们将50个景点的数据输入得到了每个景点的综合评分。排名前15的景点被选入后续的路径规划池。这个选择数量是一个平衡点太少则线路单薄太多则TSP问题规模过大且容易超出预算时间。3.3 模拟退火求解TSP我们以选出的15个景点作为节点。通过地图API获取景点间乘坐高铁或汽车的大致时间取较小值作为距离矩阵。模拟退火算法的参数设置是另一个需要调试的地方。import random import math import numpy as np def simulated_annealing_tsp(dist_matrix, initial_temperature1000, cooling_rate0.995, iterations_per_temp1000): dist_matrix: 距离矩阵 num_cities len(dist_matrix) # 生成初始解随机路径 current_path list(range(num_cities)) random.shuffle(current_path) current_cost calculate_total_distance(current_path, dist_matrix) best_path current_path[:] best_cost current_cost temperature initial_temperature while temperature 1e-3: for _ in range(iterations_per_temp): # 产生新解2-opt邻域操作随机交换一段路径 new_path current_path[:] i, j sorted(random.sample(range(num_cities), 2)) new_path[i:j1] reversed(new_path[i:j1]) new_cost calculate_total_distance(new_path, dist_matrix) # 计算成本差决定是否接受新解 delta_cost new_cost - current_cost if delta_cost 0 or random.random() math.exp(-delta_cost / temperature): current_path, current_cost new_path, new_cost if current_cost best_cost: best_path, best_cost current_path[:], current_cost # 降温 temperature * cooling_rate return best_path, best_cost def calculate_total_distance(path, dist_matrix): total 0 for i in range(len(path)): total dist_matrix[path[i-1]][path[i]] # 注意Python的负数索引这里实现了闭环 return total关键参数经验initial_temperature初始温度要设得足够高使得算法在初期有足够概率接受劣解从而跳出局部最优。我们通过实验发现从1000开始效果不错。cooling_rate冷却率通常在0.9到0.999之间。我们选择0.995这是一个较慢的冷却速度让算法有更充分的时间搜索。iterations_per_temp每个温度的迭代次数我们设为1000确保在每个温度下都能达到平衡状态。运行算法后我们得到了一条总旅行时间较短的初步环游路径。但此时还未考虑在每个景点的具体停留时间和消费。3.4 动态规划进行资源精细分配这是最复杂的一步。我们将TSP得到的路径固定假设游客将按此顺序访问这些景点。在每个景点i我们设计了两种游玩模式“深度游”体验评分高耗时time_deep[i]花费cost_deep[i]和“标准游”体验评分低耗时time_std[i]花费cost_std[i]。我们定义状态dp[i][c][t]但由于三维数组可能很大我们采用了滚动数组和字典来优化。dp[(c, t)]表示到达当前景点时累计花费为c、累计用时为t所能获得的最大体验分。def resource_optimization(path, scores_deep, scores_std, costs_deep, costs_std, times_deep, times_std, total_budget, total_time): 基于动态规划思想的资源分配优化 path: TSP得到的景点顺序列表索引 其他参数均为列表与path顺序对应 num_sites len(path) # 初始化状态字典key为(cost, time)元组value为最大得分 dp {(0, 0): 0} # 起点状态 for i in range(num_sites): site_idx path[i] new_dp {} for (cost, time), score in dp.items(): # 决策1: 标准游 new_cost cost costs_std[site_idx] new_time time times_std[site_idx] if new_cost total_budget and new_time total_time: key (new_cost, new_time) new_dp[key] max(new_dp.get(key, 0), score scores_std[site_idx]) # 决策2: 深度游 new_cost cost costs_deep[site_idx] new_time time times_deep[site_idx] if new_cost total_budget and new_time total_time: key (new_cost, new_time) new_dp[key] max(new_dp.get(key, 0), score scores_deep[site_idx]) # 简单的状态剪枝如果两个状态A和BA的花费和时间都小于等于B但得分更高则剔除B # 这里为了简化仅保留帕累托前沿上的状态 dp pareto_prune(new_dp) # 遍历最终所有状态找到得分最高的 max_score 0 best_final_state None for (cost, time), score in dp.items(): if score max_score: max_score score best_final_state (cost, time) # 注意这里需要反向回溯才能得到具体每个景点的游玩模式实际代码中需要记录前驱状态 return max_score, best_final_state def pareto_prune(state_dict): 简单的帕累托前沿剪枝 keys list(state_dict.keys()) keys.sort(keylambda x: (x[0], x[1])) # 按花费、时间排序 pruned {} for i in range(len(keys)): dominated False for j in range(len(keys)): if i ! j and keys[j][0] keys[i][0] and keys[j][1] keys[i][1] and state_dict[keys[j]] state_dict[keys[i]]: dominated True break if not dominated: pruned[keys[i]] state_dict[keys[i]] return pruned这个动态规划过程最终输出在给定总预算和总时间内能获得的最大体验总分以及对应的花费和时间。通过回溯我们可以知道在每个景点应该选择哪种游玩模式。如果最终得分不理想或资源仍有大量剩余我们可以调整TSP路径比如替换掉评分低且耗时的景点重新迭代。4. 编程实现、调试与结果分析模型是骨架程序是血肉。将上述数学模型转化为可运行的代码并调试出合理的结果是整个过程中最具挑战性也最收获成就感的部分。4.1 编程环境与工具链选择我们选择了Python作为主要实现语言原因有三一是其强大的科学计算库NumPy, Pandas便于数据处理二是算法原型实现快速三是Matplotlib等库能方便地进行结果可视化。IDE使用的是VSCode配合Jupyter Notebook进行分阶段的数据分析和模型验证。版本控制用Git这对于团队协作和回溯修改至关重要。数据处理阶段主要用Pandas读取和清洗CSV格式的景点数据。模型计算阶段NumPy负责矩阵运算自定义函数实现AHP、熵权法、TOPSIS和模拟退火。动态规划部分由于状态空间问题纯Python循环可能较慢我们对于关键循环尝试使用了Numba进行加速效果显著。4.2 关键代码模块的调试“血泪史”AHP一致性检验的坑最初我们手动填写的判断矩阵CR值高达0.25严重不合格。调试发现问题出在我们对“稍微重要”、“明显重要”等语言描述的理解不一致。后来我们制定了一个更精确的量化规则例如“A比B稍微重要”统一为3倍而不是2或4倍。同时我们写了一个小函数当CR0.1时自动提示判断矩阵中矛盾最严重的元素辅助我们进行针对性调整。模拟退火陷入局部最优初期参数设置不当冷却过快cooling_rate0.9导致算法很早就“冻结”得到的路径总是比随机打乱的好不了多少。通过绘制“温度-当前最优解”曲线我们发现温度下降太快。将冷却率调整为0.995并增加iterations_per_temp后算法有了更充分的搜索时间最终得到的结果稳定且优异。另一个技巧是多次运行取最优结果因为模拟退火具有随机性。动态规划的状态爆炸这是最大的性能瓶颈。最初我们直接使用三维数组dp[i][c][t]对于15个景点、预算上限5000元以元为单位、时间上限120小时以小时为单位状态数量是15 * 5000 * 120 9,000,000内存和计算都无法承受。解决方案是离散化将预算按100元为一档时间按0.5小时为一档。状态数锐减为15 * 50 * 240 180,000。滚动数组由于dp[i]只依赖于dp[i-1]我们只需要保存当前和上一个景点的状态空间复杂度降为2 * 50 * 240。帕累托剪枝如上节代码所示在每一步转移后剔除那些“花费更多、时间更长、得分却更低”的绝对劣势状态。这极大地减少了无效状态的扩张。 经过这些优化程序能在几秒内完成计算。结果的可视化与解释为了让论文和最终方案更直观我们用NetworkX和Matplotlib绘制了最优路径图用不同颜色和节点大小表示景点的评分和游玩模式。还用Plotly生成了交互式的三维散点图三个轴分别是“景点评分”、“所需时间”、“花费”可以清晰看到我们选择的景点集群在“高评分-适中时间-合理花费”的区域。4.3 最终方案与灵敏度分析经过模型串联计算我们最终得到了一条为期5天、总预算约4000元的长三角精华游线路。以上海为起点和终点依次游览苏州拙政园、博物馆、无锡鼋头渚、南京中山陵、夫子庙、杭州西湖、灵隐寺等地的核心景点并对部分景点如西湖、中山陵推荐了深度游模式对其他景点如某些博物馆推荐了标准游模式。在论文中灵敏度分析是体现模型稳健性的关键部分。我们主要做了以下几点调整组合权重系数α观察α从0完全客观熵权到1完全主观AHP变化时景点排名前10的名单变化。发现当α在0.4到0.8之间时核心景点如西湖、外滩、拙政园始终稳居前列说明我们的评价结果相对稳定。调整总预算和总时间模拟了预算从3000元到6000元时间从4天到7天的变化。结果显示在资源较紧张时3000元/4天模型会自动削减深度游景点数量并优先保留高性价比评分/花费比高的景点当资源充裕时则会加入更多深度游和评分稍低但特色鲜明的景点。这符合人的决策直觉验证了模型的有效性。模拟退火参数鲁棒性测试多次运行模拟退火算法记录每次得到的最优路径长度。其方差很小说明算法求解稳定。5. 参赛经验总结与给后来者的建议回顾整个解题过程从茫然到清晰从建模到调试是一次完整的科研训练。除了技术细节一些“软性”经验或许更有价值。5.1 团队协作与时间管理数学建模是团队作战。我们三人分工明确一人主攻建模与算法我一人负责数据收集、处理与可视化一人负责论文撰写与整合。每日固定时间开短会同步进度、讨论卡点是保证效率的关键。在最后24小时我们留出了充足的时间用于论文润色、图表美化而不是还在调试代码。时间分配建议拿到题目后至少用1/4的时间进行问题分析、文献查阅和思路确定不要急于敲代码。中间1/2的时间用于模型实现、计算和初步写作。最后1/4的时间必须留给论文的完善、结果的深度分析以及检查。5.2 论文写作的核心要点评委看论文的时间很短必须做到逻辑清晰、重点突出。摘要是重中之重。要用精炼的语言概括“用了什么方法”、“解决了什么问题”、“得到了什么结论”。我们的摘要结构是问题重述→模型概述AHP-熵权TOPSIS评价、SA-TSP路径规划、DP资源分配→主要结果→特色与创新。模型假设要合理且明确。例如我们假设“景点间的交通时间取高铁与汽车中的较小值”、“游客在各景点的消费仅考虑门票和市内交通”等。清晰的假设限定了模型的适用范围也体现了思考的严谨性。模型检验不能只展示结果必须检验。灵敏度分析、稳定性测试、与简单方法如随机选择、贪心算法的对比都是有力的证明。图表一图胜千言。路径图、评价结果雷达图、灵敏度分析曲线图都要清晰美观且有详细的图注说明。5.3 常见陷阱与避坑指南不要追求模型的复杂性而忽视适用性最初我们想过用复杂的神经网络来评价景点但考虑到数据量小、可解释性差最终放弃了。AHP-TOPSIS组合虽然传统但非常适合这个问题且原理清晰易于在论文中阐述。数据决定模型的上限如果景点数据本身不准比如交通时间严重低估那么再好的优化模型得出的线路也是空中楼阁。务必花时间验证核心数据的可靠性。编程时先写伪代码再实现特别是对于动态规划、模拟退火这类算法先在纸上把状态定义、转移方程、边界条件写清楚能避免很多逻辑错误。重视可复现性代码要加注释关键步骤要有输出日志。我们曾因为一个中间变量覆盖错误导致一夜的算白跑。后来我们养成了关键步骤输出检查点结果的习惯。这次长三角数学建模竞赛的解题经历让我深刻体会到建模的魅力不在于使用多么高深的算法而在于如何将一个复杂的现实问题通过合理的抽象、简化和数学工具转化为一个可分析、可求解、可解释的模型。从TOPSIS的指标标准化到模拟退火的“以一定概率接受劣解”再到动态规划的“最优子结构”每一个步骤都闪耀着数学思想解决实际问题的光芒。对于准备参加数模竞赛的同学我的建议是吃透几个经典模型如评价类、预测类、优化类的原理和实现然后大胆地去拆解问题、组合模型、编程实现。过程中遇到的每一个bug每一次对模型的调整都是比最终奖项更宝贵的收获。