Matlab求解CUMCM2004A临时超市选址:整数规划与优化实践
2026/9/16 14:24:52 网站建设 项目流程

简介:面向全国大学生数学建模竞赛参赛者及运筹优化学习者,这份资源围绕2004年高教社杯CUMCM A题“奥运会临时超市网点设计”给出完整参赛解决方案。压缩包内共6个文件,包含4个Matlab脚本(.m)、1个Access数据库(.mdb)和1个赛题文档(.doc),整体仅1.5MB,轻量但内容紧凑,同时兼顾建模代码、原始数据与题目说明。已有292人浏览学习,适合备赛参考或复现经典赛题。资源不仅提供可运行的求解程序,还覆盖线性规划、遗传算法、模拟退火等关键优化思想,并结合需求预测、选址建模、结果可视化等环节,一步步还原从原始数据到最终布局方案的建模过程,帮助读者深入理解运筹优化模型的构建与Matlab实现,切实提升数学建模与编程实战能力。

1. 从CUMCM2004A题看Matlab求解临时超市选址的完整路径

拿到高教社杯全国大学生数学建模竞赛(CUMCM2004A题)时,最常见的误区是立刻在网上搜“奥运会临时超市网点设计Matlab源码”,然后直接改数跑结果。真正让这道题从“解出来”变成“能获奖”的,是把题目给出的观众小区分布、场馆坐标、购物概率和超市容量约束,转成一组可以被Matlab优化工具箱处理的稀疏矩阵和整数规划模型。这道题表面是商业选址,本质上是带容量约束的集合覆盖与需求分配问题。下面按“建模—数据预处理—求解—验证”的顺序,给出一套用Matlab复现时可落地的代码路径,包含每个参数为什么这么设,以及替换数据后需要改哪些变量。

2. 先把题面翻译成数学模型:变量、目标函数与约束条件

2.1 从“网点设计”到集合覆盖:定义下标集合

常见做法是把问题拆成两类实体:需求点和候选网点。题目通常给出若干个观众居住小区(或观众来源区),每个小区有固定人数,并给出每个小区到各个场馆的人流概率;临时超市则安排在候选位置,比如场馆周边的空地或道路节点。为了建立可求解模型,需要先定义以下集合:

  • (S):观众需求点集合,大小为 (N_s),每个点 (s) 有需求量 (d_s)。
  • (J):候选超市网点集合,大小为 (N_j),每个候选点 (j) 有最大服务容量 (c_j)。
  • (K):场馆或热点区域集合,用于把需求点与场馆之间的距离纳入目标。

题目给的“人流数据”往往不是现成的需求点,而是“小区—场馆”的OD量。所以第一步是把OD矩阵按概率折算成每个小时的总购物人数,再聚合到需求点坐标上。这个过程不只是读Excel,还要做空间重采样。在Matlab里,建议用结构体或table保存原始数据,而不是散落的工作区变量:

% 读取题目附带的观众小区数据 raw = readtable('demand_points.csv'); % 假设有两列坐标x,y和人数pop s.x = raw.x; s.y = raw.y; s.demand = raw.pop; % 若人数是全天总量,需要按题目给的购物时间窗口折算 N_s = height(raw);

这里把需求点直接存成结构体 (s),后面的距离计算和矩阵构建都基于这个结构体。注意需求量不是人口普查人数,而是“有购物意愿且在该时段到场馆的人数”,题目往往给出比例,常见做法是乘以一个购物系数 (\alpha),这个系数在后面会作为敏感性分析的变量。

2.2 目标函数:覆盖需求最大化与总步行距离最小化

CUMCM2004A题常见的评阅标准会让选手权衡两个指标:一个是满足的购物需求总量,另一个是观众到最近超市的平均距离。在数学上可以写成双目标,但为了用Matlab的整数线性规划求解,通常把其中一个转成约束,或者线性加权。

如果以“最小化总步行距离”为主目标,可以写成:

[ \min \sum_{s \in S}\sum_{j \in J} t_{s,j} \cdot y_{s,j} \cdot d_s ]

其中 (y_{s,j}) 是0-1变量,表示需求点 (s) 是否被分配给候选网点 (j),(t_{s,j}) 是两者步行距离。同时需要引入另一个0-1变量 (x_j) 表示是否在候选点 (j) 建超市。

如果以“最大化覆盖需求”为目标,则目标函数为:

[ \max \sum_{s \in S}\sum_{j \in J} y_{s,j} \cdot d_s ]

实际建模中我一般取前者,因为它对“临时超市”场景更直观:临时网点本身是服务设施,观众不会跑太远。下面的推导都按最小化总距离展开,但切换目标时只要改Matlab的f向量即可。

2.3 三类核心约束:容量、唯一分配与覆盖半径

约束条件决定了模型能不能用线性规划解。第一类是每个候选超市的容量上限:

[ \sum_{s \in S} d_s \cdot y_{s,j} \le c_j \cdot x_j, \quad \forall j \in J ]

这里 (c_j) 是最大服务人数,和超市面积、平均购物时间、营业窗口时长相关。如果题目给出的容量单位不是“人/小时”,需要先换算。

第二类是每个需求点只能被一个已开放网点服务:

[ \sum_{j \in J} y_{s,j} = 1, \quad \forall s \in S ]

第三类是覆盖半径约束,只有距离小于阈值的需求点才能被分配:

[ y_{s,j} \le a_{s,j} ]

其中 (a_{s,j}) 是0-1可达矩阵,由距离阈值 (\tau) 生成。虽然第三类约束可以由距离下限隐含,但显式写出能减少变量组合,让整数规划求解更快。

这三类约束组合起来,就是一个典型的带容量约束的选址-分配问题。因为决策变量 (x_j) 和 (y_{s,j}) 都是整数,所以需要调用整数规划求解器。下面的表总结了所有符号和对应Matlab变量名:

符号含义Matlab变量备选值
(S)需求点集合s.x,s.y题目给出
(J)候选网点集合j.x,j.y网格剖分或题目给
(d_s)需求点需求量s.demand人口×购物概率
(c_j)候选点容量cap超市面积/人均面积
(t_{s,j})步行距离dist欧氏距离×路网系数
(x_j)是否建点x(j)0/1
(y_{s,j})是否分配y(s,j)0/1

2.4 用Matlab构建模型矩阵的骨架代码

把上面的模型转成intlinprog能接受的矩阵形式,最关键的是把双下标变量 (y_{s,j}) 展成一维向量。常见做法是按列展开,即idx(s,j) = (j-1)*N_s + s。下面这段代码构建目标函数和容量约束:

% 假设已有 dist 矩阵、 cap 向量、 demand 向量 N_s = length(s.demand); N_j = length(cap); % 决策变量顺序: x(1)..x(N_j) 为建点变量, y(s,j) 展开在后面 n_x = N_j; n_y = N_s * N_j; f = zeros(n_x + n_y, 1); % 目标:距离 * 需求量,只作用于 y 变量 for j = 1:N_j for k = 1:N_s idx_y = n_x + (j-1)*N_s + k; f(idx_y) = dist(k,j) * s.demand(k); end end % 容量约束: sum_s demand(s) * y(s,j) <= cap(j) * x(j) A = zeros(N_j, n_x + n_y); b = zeros(N_j, 1); for j = 1:N_j % 对应 x(j) 的系数是 -cap(j),因为 cap(j)*x(j) 移到左边为 -cap(j) A(j, j) = -cap(j); for k = 1:N_s idx_y = n_x + (j-1)*N_s + k; A(j, idx_y) = s.demand(k); end end

这段代码的意义是:用矩阵A同时表达容量约束和0-1变量的关系。注意A(j, j) = -cap(j)是让不等式变为 (\sum demand \cdot y - cap \cdot x \le 0)。这样写比直接在循环里拼intlinprog的输入更容易检查错误。实际调试时,我会先打印A(1,:)前10个非零元素,确认idx_y映射没有错位。

3. 数据预处理:从原始人流数据到可分块的稀疏矩阵

3.1 题目数据读入Matlab:先做清洗,再谈建模

CUMCM2004A题通常附带Excel或CSV文件,里面往往混合了文本表头、单位行和空行。如果直接readtable,会出现NaN或字符串变量混入数值列。推荐的做法是先读成table,再用ismissing找到缺测点,最后按列类型转换。下面代码处理常见格式:

T = readtable('CUMCM2004A_data.csv', 'VariableNamingRule', 'preserve'); % 去掉全空的行 T = rmmissing(T); % 把小区编号列转成字符串,如果原来是文本 if iscell(T{:,1}) id = string(T{:,1}); end % 取出坐标和人数列,列名按实际文件调整 coords = [T{:, 'x_coord'}, T{:, 'y_coord'}]; pop = T{:, 'population'}; % 有些题目会给出“购物概率”,这里直接乘上去 shopping_prob = 0.65; % 这个值来自题目附表或假设 demand = pop .* shopping_prob;

这里有一个容易踩的坑:题目给的人流量可能是“全天总客流”,但临时超市只在比赛前后营业3到4小时,所以必须把全天量除以营业时段数,否则容量约束会严重失配。我一般会先画出demand的直方图,如果最大值超过候选点容量的几十倍,就要检查是不是忘记换算时间窗口。

3.2 坐标转距离矩阵:pdist2、矩阵广播和循环的取舍

需求点到候选点的距离矩阵是后续建模的核心。如果候选点数量不大(比如几百个),直接用pdist2最方便:

% 假设 demand_coords 是 N_s x 2, candidate_coords 是 N_j x 2 dist_matrix = pdist2(demand_coords, candidate_coords, 'euclidean');

pdist2计算的是直线距离,实际步行距离需要乘以一个绕行系数,常见取值在1.2到1.5之间。如果数据量达到几万,pdist2可能内存不够,这时用分块计算或者直接考虑用路网矩阵。三种方式的对比:

方法代码复杂度内存占用适用场景
pdist2(O(N_s N_j))数据量在 1e4 以内
广播dist = sqrt((x_s - x_j.').^2 + ...)同上,但更底层需要额外处理维度时
双重循环只需两行数据内存极小的教学环境,速度慢

实际参赛时不必追求极致的距离精度,因为题目给的坐标已经是平面投影坐标,线性距离和真实步行距离差别不会影响网点数量级。重点是把dist_matrix转成0-1可达矩阵reachable

cover_radius = 800; % 单位米,来自题目对“步行5分钟”的约束 reachable = dist_matrix <= cover_radius;

这里阈值是模型里最重要的一个超参数。后来做敏感性分析时,可以用这个阈值作为横轴观察结果变化。

3.3 生成候选网点:网格剖分与坐标聚类

题目不一定直接给候选点列表,有时候需要自己生成。最常见做法是把场馆周边区域划分成正方形网格,取每个网格中心作为候选点。这样做的好处是网点规模可控,且能覆盖整个区域。下面是生成候选点的代码:

% 由场馆坐标给出区域边界 x_min = min(stadium(:,1)) - 500; x_max = max(stadium(:,1)) + 500; y_min = min(stadium(:,2)) - 500; y_max = max(stadium(:,2)) + 500; grid_step = 300; % 每300米一个网格 [gx, gy] = meshgrid(x_min:grid_step:x_max, y_min:grid_step:y_max); candidate_coords = [gx(:), gy(:)];

网格步长直接决定候选点数量和求解难度。如果步长从300改为200,候选点数量会从约 (N_s) 级变成 (N_s \times 1.5^2),整数规划耗时通常指数上升。所以建议先跑粗网格,快速验证模型正确性,再细化网格。

还有一种做法是用kmeans对需求点聚类,把聚类中心作为候选点。它能保证候选点贴近人流密集处,但缺点是会漏掉一些交通枢纽位置。如果题目中明确提到“可以在场馆出入口附近建点”,那么网格法中再加入出入口坐标即可。

3.4 预处理阶段最容易错的三个细节

第一是坐标单位。有的题给经纬度,有的给直角坐标,如果直接混用,距离会差几个数量级。处理方法是用deg2kmutm2deg转换,但更简单的是先看题目配图里的比例尺,把单位统一成“百米”或“米”。

第二是重复点。需求点里可能有多个小区在同一个坐标,如果不去重,会在距离矩阵里产生大量全同行,导致intlinprog出现退化。用unique(coords,'rows')合并,需求量累加。

第三是demand为0的点。如果某小区没有购物需求,保留它会白增加变量数量。我通常把demand < 1e-6的行直接过滤掉,这样在后期求解数据规模更小。

4. 求解策略:从intlinprog到遗传算法的切换

4.1 线性整数规划:用Matlab优化工具箱的intlinprog落地

当模型只有线性目标和线性约束时,优先用intlinprog。它来自Matlab优化工具箱,内置分支定界算法,对小规模问题(变量数几千个)能在几十秒内返回全局最优。调用时需要指定决策变量中哪些是整数。在CUMCM2004A题里,所有决策变量都是0-1变量,所以intcon = 1:num_vars

intcon = 1:(n_x + n_y); lb = zeros(n_x + n_y, 1); ub = ones(n_x + n_y, 1); % 唯一分配约束:每个需求点只被一个网点服务 Aeq = zeros(N_s, n_x + n_y); beq = ones(N_s, 1); for k = 1:N_s for j = 1:N_j idx_y = n_x + (j-1)*N_s + k; Aeq(k, idx_y) = 1; end end % 调用求解器 options = optimoptions('intlinprog', 'Display', 'iter', 'MaxTime', 120); [x_opt, fval, exitflag] = intlinprog(f, intcon, A, b, Aeq, beq, lb, ub, options);

这段代码里A,b沿用上一章的容量约束,Aeq,beq实现唯一分配。运行后会得到x_opt向量,前N_j个是建点标志,后面是分配关系。注意exitflag不等于1时需要看x_opt中的NaN值,通常是没有可行解,而不是求解失败。

4.2 模型太大时,换成遗传算法ga找近似解

如果候选点超过500个,intlinprog可能会在分支定界时卡住。此时改用全局优化工具箱的ga来求次优解更实际。遗传算法的核心是写一个适应度函数,输入建点方案,输出总距离,并对违反容量约束的方案施加惩罚。

function total_cost = fitness_func(selected) % selected 是 0/1 向量,长度 N_j if sum(selected) == 0 total_cost = 1e8; return; end % 每个需求点找最近的开放网点 open_idx = find(selected); total_cost = 0; for s_idx = 1:N_s dist_to_open = dist_matrix(s_idx, open_idx); [mind, ~] = min(dist_to_open); total_cost = total_cost + demand(s_idx) * mind; end % 容量惩罚:开放网点总容量不够时加大惩罚 total_capacity = sum(cap(open_idx)); if total_capacity < sum(demand) total_cost = total_cost + 1e6 * (sum(demand) - total_capacity); end end

调用ga时需要设置种群大小和代数。适应度函数里的1e6是一个很大的惩罚系数,目的是让算法避开不可行解。注意ga默认求解最小化问题,所以适应度值越小越好。下面表格列出ga的关键参数及其推荐值:

参数推荐值作用
PopulationSize200种群越大越不容易早熟
MaxGenerations300迭代上限,看收敛曲线
CrossoverFraction0.8交叉比例太高会破坏好解
MigrationFraction0.1子种群间迁移率
Display'iter'打印每代最优值

4.3 整数规划与遗传算法的分工建议

从工程角度,我一般分两步走:先用intlinprog在放松网格密度下求一个精确解,作为上界参考;再把这个解转成initial_population喂给ga,让遗传算法在更细的网格上继续搜索。这样比直接让ga从随机解开始快很多。

需要注意ga的初始种群必须是矩阵,一行一个个体。可以把intlinprogx_opt(1:N_j)作为第一个个体,其余个体用随机01向量。这样遗传算法在早期就能集中在高质量解附近。

4.4 求解失败的排查清单

遇到intlinprog提示“No feasible solution”,先检查三处:第一,所有需求点的总需求是否大于所有候选点容量之和,如果是,问题本身无解,需要增加候选点或放宽容量;第二,覆盖半径是否太小,导致某些需求点不在任何候选点覆盖范围内,打印sum(reachable,2)可以找到那些全0行;第三,容量约束矩阵A是否写反了不等号方向。

如果ga收敛很快但结果明显不合理,往往是惩罚系数太大或太小。太大导致算法只关心可行性,太小则容易产生容量超限的解。常见的做法是让惩罚系数为容量超限量的1000倍,然后看多次运行是否稳定。

5. 结果验证与网点调整的实用技巧

5.1 用热力图检查覆盖盲区

求解完成后,把需求点和网点画在一起,颜色表示需求量,圈表示服务半径,可以立刻发现哪些区域没有被覆盖。下面的代码画出覆盖热力图:

figure; scatter(demand_coords(:,1), demand_coords(:,2), 30, demand, 'filled'); hold on; open_j = find(x_opt(1:N_j) > 0.5); viscircles(candidate_coords(open_j,:), cover_radius * ones(size(open_j)), ... 'Color', 'red', 'LineWidth', 1.2); colorbar; xlabel('x 坐标'); ylabel('y 坐标'); title('需求点与开放超市覆盖范围');

如果发现某个需求点被多个圆圈覆盖,说明容量分配可能不均匀。可以通过查看Aeq对应的y变量来检查每个开放网点的实际负荷,并把手动调整候选点坐标作为下一步的输入。

5.2 用敏感性分析选定最优覆盖半径

CUMCM2004A题评分时很看重对参数的讨论。最直接的做法是把覆盖半径从600米逐步改到1200米,步长100米,每次调用同一个求解脚本记录总步行距离和开放网点数。把这个循环写成脚本,最后用plot画折线图。如果曲线在某一半径后变得平缓,那这个点就是合理的阈值。

5.3 把求解结果整理成可直接放进论文的表格

最后一步是把x_opt转成表格,输出选中网点的坐标和服务人数占比。可以用writetable生成Excel,方便在论文里直接截图:

selected_j = find(x_opt(1:N_j) > 0.5); out_table = table(candidate_coords(selected_j,1), candidate_coords(selected_j,2), ... cap(selected_j), 'VariableNames', {'x', 'y', 'capacity'}); out_table.served = zeros(height(out_table), 1); for jj = 1:length(selected_j) out_table.served(jj) = sum(demand(find_allocated_to_j(selected_j(jj)))); end writetable(out_table, 'resolved_networks.xlsx');

这里find_allocated_to_j是从y变量中还原每个网点服务了哪些需求点的小函数。整个流程跑通后,换一套数据只需要改第2章的readtable文件名和第3章的cover_radius

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询