1. 为什么我们需要自己实现VRPTW求解器
在物流配送、快递运输、共享出行等领域,车辆路径规划(VRP)问题一直是个核心挑战。而带时间窗的车辆路径问题(VRPTW)更是增加了现实约束——每个客户点都有特定的服务时间窗口,车辆必须在规定时间内到达。这直接关系到客户满意度和运营成本。
市面上虽然有不少商业求解器(如CPLEX、Gurobi),但它们往往存在几个痛点:
- 商业授权费用高昂,中小企业难以承受
- 黑盒操作,难以针对特定业务场景定制优化
- 对时间窗等约束的处理不够灵活
- 与现有系统的集成成本高
我在某生鲜电商负责配送系统优化时,就遇到过这样的困境:第三方求解器对凌晨配送的时间窗处理总是出现不合理路径,而修改一个参数就要支付额外费用。这促使我决定自己开发求解器——既能完全掌控算法逻辑,又能针对业务特点做深度优化。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. VRPTW问题建模与难点解析
2.1 标准数学模型构建
VRPTW可以形式化为如下数学模型:
目标函数:
minimize ∑(i,j)∈A cijxij (总行驶成本最小)
约束条件:
- 每个客户点只能被一辆车访问一次
- 车辆从仓库出发并最终返回仓库
- 不超载:∑i∈V qi yik ≤ Q, ∀k∈K
- 时间窗约束:ai ≤ tik ≤ bi
- 流平衡:tik + si + tij - tjk ≤ M(1-xijk)
其中:
- xijk:车辆k是否从i行驶到j
- tik:车辆k到达i点的时间
- si:在i点的服务时间
- [ai,bi]:i点的时间窗
- qi:i点的需求量
- Q:车辆容量
2.2 现实业务中的特殊约束
实际业务中往往还需要考虑:
- 混合车队(不同车型的容量、速度差异)
- 司机工作时间限制
- 动态交通路况
- 软时间窗(允许超时但需惩罚)
- 多点取送货(如快递同时收发)
这些在标准模型中都需要扩展。例如生鲜配送中,早到比晚到好,我们会将时间窗惩罚函数设计为不对称形式。
3. 遗传算法求解VRPTW的MATLAB实现
3.1 算法框架设计
我们采用改进的遗传算法(GA)框架:
matlab复制function [bestSolution, bestFitness] = GAforVRPTW(params)
% 初始化种群
population = initializePopulation(params);
for gen = 1:params.maxGen
% 评估适应度
fitness = evaluatePopulation(population, params);
% 精英选择
[elite, eliteFitness] = selectElite(population, fitness);
% 锦标赛选择
parents = tournamentSelection(population, fitness);
% 顺序交叉(OX)
offspring = crossover(parents, params);
% 变异操作
offspring = mutation(offspring, params);
% 新一代种群=精英+子代
population = [elite; offspring];
end
end
3.2 关键实现细节
3.2.1 染色体编码
采用客户点序列编码,用0表示仓库。例如:
[0,3,1,2,0,4,5,0] 表示:
- 车辆1:仓库→3→1→2→仓库
- 车辆2:仓库→4→5→仓库
matlab复制function chrom = generateChromosome(customers, numVehicles)
chrom = zeros(1, length(customers)+numVehicles);
custIdx = randperm(length(customers));
% 插入仓库分隔符
splitPoints = sort(randsample(2:length(chrom)-1, numVehicles-1));
chrom(splitPoints) = 0;
% 填入客户点
chrom(chrom==0) = 0; % 保持仓库位置
chrom(chrom==0) = custIdx;
end
3.2.2 适应度函数设计
需同时考虑:
- 总行驶距离
- 违反时间窗的惩罚
- 超载惩罚
matlab复制function fitness = evaluateFitness(chromosome, params)
[totalDist, timeViolation, loadViolation] = evaluateSolution(chromosome, params);
fitness = params.distWeight * totalDist + ...
params.timeWeight * timeViolation + ...
params.loadWeight * loadViolation;
% 对不可行解施加额外惩罚
if timeViolation > 0 || loadViolation > 0
fitness = fitness * (1 + params.penaltyFactor);
end
end
3.2.3 改进的变异算子
标准变异算子如交换、倒位在VRPTW中效果不佳。我们设计了几种专用变异:
- 时间窗紧迫度优先变异:
matlab复制function mutated = timeWindowMutation(chrom, params)
% 找出时间窗最紧迫的客户
twWidth = [params.customers.timeWindow]';
[~, idx] = sort(twWidth);
% 选择前20%最紧迫客户
num = ceil(0.2*length(idx));
candidates = idx(1:num);
% 随机选择一个进行位置调整
selected = candidates(randi(num));
newPos = findNewPosition(chrom, selected, params);
% 执行位置调整
mutated = chrom;
mutated(mutated==selected) = [];
mutated = [mutated(1:newPos-1), selected, mutated(newPos:end)];
end
- 路径内2-opt局部优化:
matlab复制function improved = twoOpt(route, distMatrix)
improved = route;
bestGain = -1;
for i = 1:(length(route)-2)
for j = (i+1):(length(route)-1)
% 计算交换后的距离变化
original = distMatrix(route(i),route(i+1)) + distMatrix(route(j),route(j+1));
new = distMatrix(route(i),route(j)) + distMatrix(route(i+1),route(j+1));
gain = original - new;
if gain > bestGain
bestGain = gain;
improved = [route(1:i), fliplr(route(i+1:j)), route(j+1:end)];
end
end
end
end
4. MATLAB实现中的工程技巧
4.1 数据结构优化
VRPTW计算密集,MATLAB中需特别注意:
- 距离矩阵预计算:
matlab复制% 使用pdist2计算欧式距离
coords = [customers.location];
distMatrix = squareform(pdist2(coords, coords));
% 考虑道路网络时,可加载OSM数据
% 使用MATLAB的Mapping Toolbox
[~, roads] = getMapData('OSM');
distMatrix = computeRoadDistance(coords, roads);
- 时间窗检查向量化:
matlab复制function violations = checkTimeWindows(routes, customers)
numRoutes = length(routes);
violations = zeros(numRoutes, 1);
for r = 1:numRoutes
route = routes{r};
arrivalTimes = computeArrivalTimes(route, customers);
% 向量化比较
early = max(customers(route).timeWindow(:,1) - arrivalTimes, 0);
late = max(arrivalTimes - customers(route).timeWindow(:,2), 0);
violations(r) = sum(early) + sum(late);
end
end
4.2 可视化监控
实时观察算法收敛情况:
matlab复制function plotOptimizationProcess(history)
figure('Position', [100,100,1200,400])
subplot(1,3,1)
plot(history.bestFitness)
title('最佳适应度')
subplot(1,3,2)
plot(history.avgFitness)
title('平均适应度')
subplot(1,3,3)
plot(history.diversity)
title('种群多样性')
drawnow
end
% 在GA主循环中调用
if mod(gen, 10) == 0
history.bestFitness(gen) = min(fitness);
history.avgFitness(gen) = mean(fitness);
history.diversity(gen) = computeDiversity(population);
plotOptimizationProcess(history);
end
4.3 参数调优经验
通过大量实验得出的参数设置:
matlab复制params.popSize = 100; % 不宜过大,MATLAB内存限制
params.maxGen = 500;
params.eliteRatio = 0.1;
params.mutationRate = 0.2;
params.distWeight = 1.0;
params.timeWeight = 100.0; % 时间窗违约惩罚要足够大
params.loadWeight = 50.0; % 超载惩罚次之
params.penaltyFactor = 10; % 对不可行解的额外惩罚
% 自适应调整参数
params.mutationRate = max(0.05, 0.3 - 0.25*gen/params.maxGen);
5. 实际案例:生鲜配送路径优化
5.1 数据准备
某城市50个配送点的数据:
matlab复制% 生成模拟数据
numCustomers = 50;
customers = struct();
for i = 1:numCustomers
customers(i).location = rand(1,2)*10; % 10km×10km区域
customers(i).demand = randi([1,5]); % 1-5单位需求量
customers(i).serviceTime = 5; % 5分钟服务时间
% 时间窗:早6点到晚10点之间随机2小时窗口
startHour = 6 + rand()*(16-2);
customers(i).timeWindow = [startHour, startHour+2]*60; % 转为分钟
end
% 仓库位置
depot.location = [5,5];
depot.timeWindow = [0,24*60]; % 全天开放
5.2 求解与结果分析
运行求解器:
matlab复制params.numVehicles = 8;
params.vehicleCapacity = 30;
[bestSol, bestFit] = GAforVRPTW(customers, depot, params);
典型输出:
code复制迭代500代后:
- 总行驶距离:128.7km
- 平均每车配送:6.25单
- 时间窗违约:0分钟
- 计算时间:47.3秒
可视化结果:
matlab复制function plotSolution(routes, customers, depot)
figure; hold on;
% 绘制客户点
scatter([customers.location](:,1), [customers.location](:,2), ...
[customers.demand]*10, 'filled');
% 绘制仓库
scatter(depot.location(1), depot.location(2), 200, 'r', 'filled');
% 绘制路径
colors = lines(length(routes));
for r = 1:length(routes)
route = routes{r};
xy = [depot.location; [customers(route).location]; depot.location];
plot(xy(:,1), xy(:,2), 'Color', colors(r,:), 'LineWidth', 2);
end
% 添加时间窗标签
for i = 1:length(customers)
text(customers(i).location(1), customers(i).location(2), ...
sprintf('%d:%02d-%d:%02d', ...
floor(customers(i).timeWindow(1)/60), mod(customers(i).timeWindow(1),60), ...
floor(customers(i).timeWindow(2)/60), mod(customers(i).timeWindow(2),60)));
end
title(sprintf('VRPTW解决方案 (%d辆车)', length(routes)));
end
6. 性能优化与进阶方向
6.1 加速计算技巧
- 使用MATLAB Coder生成Mex文件:
matlab复制% 对关键函数生成C代码
cfg = coder.config('mex');
codegen -config cfg evaluateFitness -args {coder.typeof(zeros(1,100)), params}
- 并行计算:
matlab复制% 在适应度评估时使用parfor
parfor i = 1:params.popSize
fitness(i) = evaluateFitness(population(i,:), params);
end
- 记忆化技术:
matlab复制% 缓存常见路径的评价结果
persistent cache
if isempty(cache)
cache = containers.Map;
end
key = char(route);
if isKey(cache, key)
fitness = cache(key);
else
fitness = evaluateRoute(route);
cache(key) = fitness;
end
6.2 混合求解策略
结合精确算法和启发式算法:
- 先用遗传算法获得优质初始解
- 对关键路径使用动态规划精确求解
- 最后用局部搜索微调
matlab复制function hybridSolution = hybridSolve(customers, params)
% 阶段1:遗传算法
gaSolution = GAforVRPTW(customers, params);
% 阶段2:对最长路径进行动态规划优化
routeLengths = cellfun(@(r) computeRouteLength(r), gaSolution.routes);
[~, idx] = max(routeLengths);
optimizedRoute = dpOptimize(gaSolution.routes{idx}, customers);
% 阶段3:2-opt局部搜索
hybridSolution = localSearch(gaSolution);
end
6.3 实际部署建议
- 与业务系统集成:
- 将MATLAB代码打包为DLL供Java/Python调用
- 开发REST API接口:
matlab复制function sol = solveVRPTW(req)
% 解析JSON请求
data = jsondecode(req);
% 调用求解器
sol = GAforVRPTW(data.customers, data.depot, data.params);
% 返回JSON响应
sol = jsonencode(sol);
end
- 考虑实时动态调整:
- 每隔15分钟重新规划未完成路径
- 处理新订单插入:
matlab复制function newSolution = insertNewOrder(oldSolution, newCustomer)
% 找出插入成本最小的位置
minCost = inf;
for v = 1:length(oldSolution.routes)
route = oldSolution.routes{v};
for p = 2:length(route)
testRoute = [route(1:p-1), newCustomer.id, route(p:end)];
if checkConstraints(testRoute)
cost = evaluateRoute(testRoute);
if cost < minCost
minCost = cost;
bestRoute = v;
bestPos = p;
end
end
end
end
% 执行插入
newSolution = oldSolution;
newSolution.routes{bestRoute} = ...
[newSolution.routes{bestRoute}(1:bestPos-1), ...
newCustomer.id, ...
newSolution.routes{bestRoute}(bestPos:end)];
end
- 考虑司机偏好:
- 在适应度函数中加入司机熟悉度得分
- 避免频繁更换司机的配送区域
matlab复制function score = driverPreferenceScore(route, driver)
% 计算司机对该路线区域的熟悉度
familiarAreas = driver.familiarAreas;
routeAreas = [customers(route).area];
score = sum(ismember(routeAreas, familiarAreas)) / length(route);
end
