
1. 从赛题到方案一个“老数模人”的解题心路每年看到“高教社杯”的赛题发布心里总会痒痒的。2023年的E题关于小浪底水库的最优监测方案初看题目一股熟悉的“味道”就扑面而来——这又是一个典型的资源优化配置问题但披上了一层环境监测与数据分析的外衣。题目要求我们设计一个监测方案用有限的传感器去覆盖一个动态变化的水域还要评估方案的优劣。这听起来是不是有点像“既要马儿跑又要马儿不吃草”但恰恰是这种约束下的最优解寻找才是数学建模的魅力所在。我拿到题目后第一反应不是立刻去翻算法书而是先问自己几个问题水库监测的核心矛盾是什么是监测点传感器的有限性与监测需求水域范围、关键区域的广泛性之间的矛盾。监测目标是什么是尽可能全面地掌握水库的水质、水文等关键指标的空间分布与变化。那么如何量化“全面”如何量化“有限”这就引出了我们建模的两个核心武器小波变换和背包模型。前者负责从纷繁复杂的监测数据中提炼出真正需要被重点关注的“关键区域”和“关键时段”后者则负责在有限的传感器“背包”容量下决定“装入”哪些监测点使得整体监测“价值”最大化。网上相关的讨论和代码很多但很多都停留在“调包”和“跑通”的层面。为什么用这个参数为什么这个目标函数有效模型结果在实际中意味着什么这些更深层的问题往往被一带而过。这篇分享我就想结合我们团队的获奖论文和代码实现掰开揉碎了讲讲我们是怎么一步步把“小波变换”和“背包模型”这两个看似不搭界的工具巧妙地拧成一股绳去解决这个实际问题的。我会重点讲清楚每一个技术选择背后的“为什么”以及我们在编程实现和论文写作中踩过的那些“坑”。2. 问题拆解把水库监测翻译成数学语言面对一个实际工程问题第一步也是最关键的一步就是完成从“自然语言”到“数学语言”的翻译。这一步如果跑偏了后面算法再精巧也是南辕北辙。2.1 核心矛盾与关键概念定义小浪底水库水域广阔水文条件复杂。我们不可能也没有必要在每一个坐标点都布设传感器。因此监测点传感器的数量是有限的这是我们的核心约束。假设我们有N个备选监测点位置但预算或物理条件只允许我们部署K个 (K N)。那么这K个点应该布在哪里评判标准是什么这就需要引入“监测价值”这个概念。一个监测点的价值不是恒定的它至少取决于两个方面空间重要性该点位本身是否处于关键区域例如靠近排污口、水库大坝、主要支流汇入口、饮用水取水口等区域的点位其基础价值就更高。时间动态性该点位的监测数据是否包含重要的变化信息有些点位可能长期稳定监测价值低有些点位则可能在某些时段如汛期、排污期出现剧烈波动这些波动里蕴含着关键信息监测价值高。所以我们的目标函数可以初步描述为从N个备选点中选出K个点使得这K个点的总监测价值最大化。2.2 如何量化“监测价值”——小波变换的登场这里就是小波变换大显身手的地方。我们通常能获得的是每个备选点位历史上的一段时序监测数据例如每日的氨氮浓度、浊度等。这些数据就是一维时间信号。小波变换能做什么简单来说它像一台“数学显微镜”可以同时看到信号在时间和频率上的细节。传统的傅里叶变换只能告诉你信号里有哪些频率成分但不知道这些成分什么时候出现。而小波变换通过一个可以缩放对应频率和平移对应时间的“小波”函数去扫描信号从而能定位出信号中突变、转折等瞬态特征发生的具体时刻和强度。在我们的问题里信号的“突变点”可能对应着污染事件的发生、降雨径流的汇入、水库调度操作等关键事件。不同尺度的波动大尺度的波动低频可能反映季节性变化小尺度的波动高频可能反映短时扰动。我们通过连续小波变换CWT或离散小波变换DWT对每个点位的时序数据进行分析可以得到一个小波系数矩阵或能量谱。其中能量高的区域就对应着该点位在特定时间、特定频率尺度上变化剧烈的时刻。那么如何用一个数值来代表一个点位的“总监测价值”呢我们团队的做法是计算每个点位小波变换后系数的绝对值的总和或平方和即能量再对其进行归一化处理。这个值越大说明该点位的历史数据中包含的“变化信息”越丰富其潜在的监测价值也就越高。我们记第i个点位的这个价值为v_i。实操心得小波基函数与尺度的选择这是第一个容易踩坑的地方。常用的母小波有 Haar、Daubechies (dbN)、Symlets等。对于水文数据这类可能具有突变和趋势的信号我们经过对比选择了db4小波它在光滑性和紧支撑性之间取得了较好的平衡。尺度的选择也至关重要尺度太小会引入大量噪声尺度太大会丢失细节信息。我们通过分析数据的主周期将尺度序列设置为scales np.arange(1, 129)具体上界根据数据采样频率和长度调整以确保能覆盖从日变化到季节变化的主要周期成分。在Python中可以使用pywt库方便地进行小波变换。import numpy as np import pywt def calculate_wavelet_value(time_series): 计算单一点位时序数据的小波能量价值 :param time_series: 一维时序数据数组 :return: 归一化的监测价值 v_i # 1. 选择小波基和尺度 wavelet db4 scales np.arange(1, 129) # 2. 进行连续小波变换示例也可用离散小波变换 # 注意pywt.cwt 返回系数矩阵 [scales, times] coefficients, frequencies pywt.cwt(time_series, scales, wavelet) # 3. 计算小波系数的总能量L2范数平方 energy np.sum(coefficients ** 2) # 4. 简单归一化例如除以所有点位中的最大能量值 # 这里先返回原始能量后续统一处理 return energy # 假设有N个点位的时序数据列表 data_list values [calculate_wavelet_value(series) for series in data_list] max_value max(values) normalized_values [v / max_value for v in values] # 归一化到[0,1]2.3 如何描述“有限资源下的选择”——背包模型的构建现在我们有了每个点位的“价值”v_i以及总点数限制K。这完美契合了经典的0-1背包模型的框架物品N个备选监测点位。物品重量每个点位部署一个传感器其“重量”可以视为1如果所有传感器成本相同。如果考虑成本差异重量可以是成本c_i。背包容量就是K或总预算B。物品价值就是我们上一步计算出的归一化监测价值v_i。我们的目标就是在总重量不超过背包容量的前提下选择一组物品监测点使得总价值最大。数学模型可以表述为 设决策变量x_i ∈ {0, 1}表示第i个点位是否被选中1为选中0为不选。 目标函数Maximize Σ (v_i * x_i) 其中i从1到N。 约束条件Σ (w_i * x_i) ≤ K 其中w_i为权重通常为1。注意这里的一个简化与一个深化简化我们暂时假设点位之间相互独立选择一个点不影响其他点的价值。这在实际中可能不完全成立例如两个很近的点信息冗余度高更复杂的模型可以考虑覆盖范围或信息相关性但这会大大增加模型复杂度。在竞赛有限时间内先解决独立点位的背包问题是合理的第一步。深化除了点数约束实际问题中可能还有“必须监测点”如国控断面、“互斥点”二选一等约束。这些都可以通过增加约束条件融入到背包模型中例如必须监测x_j 1互斥点点p和点q不能同时选x_p x_q ≤ 13. 模型求解从理论到代码的跨越模型建立好了接下来就是求解。0-1背包问题是NP-Hard问题但对于我们这种规模N通常在几十到几百采用动态规划DP算法可以高效求得精确最优解。3.1 动态规划求解背包问题动态规划的核心思想是“记住过去的结果”避免重复计算。我们定义一个二维数组dp[i][j]其含义是考虑前i个点位在恰好使用j个传感器容量时所能获得的最大总价值。状态转移方程如下dp[i][j] max(dp[i-1][j], dp[i-1][j-1] v[i]) 其中v[i]是第i个点位的价值。 解释对于第i个点位我们有两种选择不选它那么最大价值就是考虑前i-1个点位、使用j个容量时的最优值即dp[i-1][j]。选它那么需要为它预留1个容量。此时的最大价值是“考虑前i-1个点位、使用j-1个容量时的最优值”加上这个点位的价值v[i]即dp[i-1][j-1] v[i]。 我们取这两种选择中的最大值。初始化dp[0][...] 0考虑0个点位价值为0dp[...][0] 0使用0个容量价值为0。最终dp[N][K]就是我们要求的、使用不超过K个传感器的最大总价值。为了知道具体选了哪些点我们需要进行回溯。3.2 Python代码实现与细节剖析下面是我们团队实现的求解核心代码我加上了详细的注释和踩坑提醒。import numpy as np def solve_knapsack_dp(values, K): 使用动态规划求解0-1背包问题重量均为1 :param values: list of float, 每个点位的价值列表长度N :param K: int, 传感器数量上限背包容量 :return: (max_value, selected_indices) max_value: float, 最大总价值 selected_indices: list of int, 被选中的点位索引列表从0开始 N len(values) # 初始化dp数组维度 (N1) x (K1)多一行一列用于边界条件 dp np.zeros((N 1, K 1), dtypefloat) # 动态规划填表 for i in range(1, N 1): # i对应第i个物品点位索引i-1 current_value values[i - 1] for j in range(1, K 1): # j代表当前可用容量 # 默认情况不选第i个点位 dp[i][j] dp[i - 1][j] # 如果当前容量j至少为1即可以放得下当前物品因为重量为1 # 并且选择当前点位可能更优 if j 1: candidate_value dp[i - 1][j - 1] current_value if candidate_value dp[i][j]: dp[i][j] candidate_value # 最大价值存储在dp[N][K] max_value dp[N][K] # 回溯找出被选中的点位 selected_indices [] j K for i in range(N, 0, -1): # 从最后一个物品倒推 # 注意由于浮点数精度问题比较时使用一个很小的容差 if j 1 and abs(dp[i][j] - (dp[i - 1][j - 1] values[i - 1])) 1e-9: # 说明第i个物品被选中了 selected_indices.append(i - 1) # 记录原始索引 j - 1 # 容量减少 # 否则第i个物品未被选中j保持不变继续检查前一个物品 selected_indices.reverse() # 回溯得到的顺序是倒序将其反转 return max_value, selected_indices # 示例使用之前计算出的归一化价值 normalized_values K 10 # 假设最多部署10个传感器 max_val, selected_points solve_knapsack_dp(normalized_values, K) print(f最大监测价值: {max_val:.4f}) print(f选中的点位索引: {sorted(selected_points)})踩坑实录浮点数比较与价值缩放浮点数精度陷阱在回溯判断dp[i][j] dp[i-1][j-1] values[i-1]时由于values是浮点数直接使用比较可能因精度问题失败导致回溯路径错误。必须使用绝对值差小于一个极小容差如1e-9的方法来判断相等如上代码所示。价值为整数更稳妥为了避免浮点数带来的所有麻烦一个更稳健的做法是在前期就将归一化后的价值v_i乘以一个大的整数例如 10000 或 1000000然后取整将问题转化为整数价值背包问题。这样在DP和回溯过程中全部使用整数运算绝对精确。这在竞赛编程中是常用技巧。空间优化上面的代码使用了O(N*K)的二维数组。实际上DP的状态转移只依赖于上一行 (i-1)因此可以优化为两个一维数组甚至一个但需要倒序更新将空间复杂度降至O(K)。当N很大时这个优化是必要的。3.3 结果可视化与方案解读得到选中的点位索引后我们需要将其映射回实际的地理位置经纬度坐标并在水库地图上进行可视化。这里通常用到matplotlib和geopandas如果有矢量地图数据或folium生成交互式网页地图。import matplotlib.pyplot as plt import pandas as pd # 假设我们有一个DataFrame df_locations包含所有备选点位的经纬度和其他信息 # 列包括point_id, longitude, latitude, wavelet_value 等 df_locations pd.read_csv(candidate_locations.csv) # 根据求解结果标记选中的点位 df_locations[selected] False df_locations.loc[selected_points, selected] True # 简单可视化 plt.figure(figsize(10, 8)) # 绘制所有备选点位 plt.scatter(df_locations[longitude], df_locations[latitude], clightblue, s20, alpha0.6, label候选点位) # 高亮显示选中的点位 selected_df df_locations[df_locations[selected]] plt.scatter(selected_df[longitude], selected_df[latitude], cred, s50, markers, edgecolorsk, label最优监测点 (K%d) % K) plt.xlabel(经度) plt.ylabel(纬度) plt.title(小浪底水库最优监测点布设方案 (基于小波-背包模型)) plt.legend() plt.grid(True, alpha0.3) plt.tight_layout() plt.show() # 输出选中点位的详细信息 print(最优监测点位详情) print(selected_df[[point_id, longitude, latitude, wavelet_value]].to_string())可视化结果能直观展示模型给出的方案选中的点红色方块往往分布在历史数据变化剧烈小波价值高的区域并且由于背包容量限制模型自动在这些高价值点中做出了取舍选择了一个价值总和最大的组合。4. 模型评估、灵敏度分析与方案落地思考一个完整的数模论文不能只给出一个“黑箱”答案还需要对模型和结果进行多角度评估并讨论其实际意义。4.1 方案评估我们得到了什么除了“总价值最大化”这个模型自身的目标我们还需要从实际应用角度评估方案空间覆盖率选中的K个点在水库的主要功能区如库首、库中、库尾、主要支流是否有分布是否覆盖了所有已知的敏感点可以通过计算选中点与敏感点的最小距离来评估。价值分布选中的点其个体价值v_i的分布如何是全部接近1的高价值点还是包含了一些价值稍低但能填补空间盲区的点这反映了模型在“集中”与“分散”之间的权衡。与简单方法的对比将我们的方案与两种“朴素”方案对比随机选择随机选K个点计算其总价值。重复多次取平均作为基准。贪婪选择每次都选当前剩余点位中价值最高的点直到选满K个。这是一种启发式方法。 我们的动态规划最优解的总价值应该显著高于随机选择的平均值并且通常也优于或等于贪婪算法的结果贪婪算法对于0-1背包不一定得到最优解。def evaluate_solution(selected_indices, all_values, all_coords, sensitive_coords): 评估选中的方案 # 1. 计算总价值 total_value sum([all_values[i] for i in selected_indices]) # 2. 计算空间覆盖率到最近敏感点的平均距离 from scipy.spatial.distance import cdist selected_coords np.array([all_coords[i] for i in selected_indices]) if len(sensitive_coords) 0: dist_matrix cdist(selected_coords, sensitive_coords) min_distances np.min(dist_matrix, axis1) avg_min_distance np.mean(min_distances) else: avg_min_distance None # 3. 与贪婪算法对比 greedy_indices [] remaining_indices list(range(len(all_values))) remaining_values all_values.copy() for _ in range(K): if not remaining_indices: break # 找剩余中价值最高的 best_idx_in_remaining np.argmax(remaining_values) best_original_idx remaining_indices[best_idx_in_remaining] greedy_indices.append(best_original_idx) # 从剩余列表中移除 del remaining_indices[best_idx_in_remaining] del remaining_values[best_idx_in_remaining] greedy_value sum([all_values[i] for i in greedy_indices]) return { dp_total_value: total_value, greedy_total_value: greedy_value, avg_distance_to_sensitive: avg_min_distance, dp_selected_indices: selected_indices, greedy_selected_indices: greedy_indices }4.2 灵敏度分析如果条件变了会怎样灵敏度分析是体现模型稳健性和论文深度的重要环节。主要分析两个参数传感器数量K这是最关键的约束。我们可以绘制一条“价值-数量曲线”即横坐标是K从1到N纵坐标是对应的最大总价值dp[N][K]。这条曲线通常是凹的边际价值递减它能直观告诉我们增加第一个传感器价值提升最大增加到某个数量后再增加传感器带来的价值增益就很小了。这为决策者确定合理的传感器部署规模提供了定量依据。小波价值计算方式我们之前用了小波总能量。可以尝试其他指标如小波系数的方差、特定频带如高频的能量占比等重新计算v_i再运行背包模型。对比不同价值定义下选出的点位集合的重合度。如果重合度高说明模型结果对价值定义不敏感比较稳健如果差异大则需要结合物理意义讨论哪种价值定义更合理。4.3 从模型到现实方案的局限性与优化方向在论文的讨论部分必须坦诚地指出当前模型的局限性并指出可能的优化方向这能体现思考的全面性。局限性点位独立性假设模型假设点位价值独立未考虑空间相关性。两个很近的点监测信息可能高度冗余同时选中它们会造成资源浪费。静态历史数据依赖价值v_i基于历史数据计算。如果未来水库运行模式或污染源发生重大变化历史数据的代表性会下降。单一价值维度仅用小波变换的能量来表征价值可能忽略了其他重要因素如点位的可达性、建设维护成本差异、监测指标的重要性权重等。优化方向引入覆盖模型可以将问题转化为“最大覆盖问题”每个传感器有一定的监测半径目标是覆盖尽可能多的“重要区域”其重要性可由小波价值或其他地理信息定义。多目标优化同时考虑最大化监测价值、最小化建设总成本、最大化空间覆盖率等多个目标使用帕累托最优等概念。动态调整方案结合实时数据传输设计一种机制当某个未监测区域出现异常信号可通过流域模型推断时能动态调整监测重点例如使用移动监测设备。5. 论文写作与代码整合的实战技巧最后结合我们获奖的经验分享几点关于如何将上述所有工作整合成一篇优秀数模论文的实操技巧。5.1 论文叙述的逻辑主线论文不是代码说明书也不是数学公式的堆砌。它需要一条清晰的逻辑主线问题重述与分析用你自己的话把题目说清楚并明确指出核心矛盾有限传感器 vs. 全面监测和解决思路量化价值 - 优化选择。模型准备详细介绍小波变换的原理、为何适用于本问题、参数小波基、尺度如何选择。这部分需要一些公式和图示如小波时频图体现理论深度。价值量化模型给出计算每个点位监测价值v_i的数学公式或步骤。优化选择模型明确建立0-1背包模型的数学形式目标函数、约束条件并解释其如何对应实际问题。模型求解说明采用动态规划算法简述其原理给出算法流程图或伪代码。实例分析将模型应用于题目提供或自己构造的数据。展示小波分析的结果部分点位时序图及时频图。所有点位的价值分布图直方图或空间分布图。不同K值下的最优方案图空间布点图和“价值-数量曲线”。方案评估与对比结果表格形式呈现与随机法、贪婪法的对比。灵敏度分析结果不同K、不同价值指标下的方案对比。模型评价与推广总结模型的优点思路清晰、求解高效、结果直观客观指出局限性如前所述并提出可行的改进方向。5.2 代码与论文的协同代码注释代码要有清晰的注释关键步骤如小波变换、DP、回溯必须注明。变量名要有意义。生成图表论文中所有图表都应尽量由代码自动生成并保存为高分辨率图片如.png或.pdf格式。在代码中使用plt.savefig(figure1.png, dpi300, bbox_inchestight)。结果输出将关键结果如选中的点位ID、坐标、价值总价值对比数据等输出到文本文件或Excel中方便论文中制作表格。附录将完整的、可运行的源代码放在论文附录中。注意整理代码结构删除调试过程中的冗余代码确保评委或读者拿到后能复现主要结果。5.3 那些容易丢分的“坑”模型假设不明确必须在论文中清晰列出所有主要假设如点位价值独立、传感器成本相同、历史数据具有代表性等并简要说明其合理性。符号说明混乱在模型建立部分所有用到的数学符号必须集中在一个表格里进行说明包括符号、含义、单位。只有结果没有分析不要只扔出一张图或一个最优值。必须对结果进行解读“为什么这些点被选中”、“曲线为什么长这样”、“与预期有何异同”。灵敏度分析走过场不要只是简单地说“改变K值结果会变”。要展示变化趋势画图并解释其管理意义例如“当K15后边际效益显著降低建议部署数量不超过15个”。摘要不过关摘要是论文的门面。必须用精炼的语言在有限篇幅内说清楚针对什么问题、建立了什么模型、用了什么方法、得到了什么主要结论、有何特色。避免在摘要中出现公式和图表引用。回过头看2023年E题的求解过程是一次将信号处理小波变换与运筹学背包模型交叉应用的典型实践。其核心思想具有普适性从数据中挖掘“价值”指标在约束下进行“优化”选择。这个框架可以迁移到许多类似场景比如通信基站选址、物流配送中心规划、广告投放点位选择等。真正吃透这个题目收获的不仅仅是一篇获奖论文更是一种解决问题的结构化思维模式。在编程实现时务必注意浮点数处理和算法细节在论文写作时务必牢记逻辑清晰、分析深入。希望这篇超详细的拆解能对后来者有所帮助。