在电商爆发式增长的今天,物流配送效率直接决定企业竞争力。如何用最少的车辆、最短的路径、最低的成本完成订单交付?这背后隐藏着一个经典的组合优化问题 ——带时间窗的车辆路径问题(VRPTW)。今天我们就来聊聊如何用遗传算法(GA)攻克这个难题,代码已备好,一起开启智能调度之旅吧~🚀
一、问题建模:还原真实物流场景 📦
1. 场景设定与参数说明
假设我们有一个配送中心(坐标[0,0]
)和 5 个客户点,需要解决以下核心问题:
- 车辆限制:最多使用 3 辆货车,每车载重≤100 吨,单次行程≤200 公里
- 客户需求:每个客户有固定需求量(如客户 1 需要 30 吨货物)
- 时间窗约束:客户要求货物在特定时间段送达(如客户 2 要求 9-13 点送达)
- 成本构成:包括车辆启用费、运输费、冷藏费、货损费、时间窗惩罚等
2. 关键参数解读
参数 | 含义 | 示例值 |
---|---|---|
q | 客户需求量(吨) | [30,20,25,15,10] |
time_windows | 时间窗(开始 / 结束时间) | [8,12;9,13;…] |
K/Q/D | 最大车辆数 / 载重限制 / 里程限制 | 3/100/200 |
V1-V4 | 各类成本系数 | 如车辆启用费 200 元 / 辆 |
d | 距离矩阵(配送中心与客户间距离) | 由squareform(pdist) 生成 |
这些参数就像拼图的碎片🧩,只有准确建模,才能拼出最优解~
二、遗传算法:模拟自然进化的优化引擎 🌿
1. 染色体编码:用客户顺序代表配送路径
我们用排列编码表示染色体:每个染色体是一个客户顺序的排列(如[2,5,3,1,4]
代表配送顺序为客户 2→5→3→1→4)。这种编码方式直观反映路径顺序,便于交叉和变异操作~
% 初始化种群:生成100条随机客户顺序
population = zeros(pop_size, num_customers);
for i = 1:pop_size
population(i,:) = randperm(num_customers); % randperm生成随机排列
end
2. 适应度计算:解码染色体为实际路径
decode_chromosome
函数是算法的 “翻译官”,负责将客户顺序转换为具体的车辆路径,并计算总成本。核心逻辑如下:
- 路径分割:按载重和里程限制将客户分配到不同车辆
- 成本计算:
- C1:车辆启用费(用几辆车花多少钱🚗)
- C2:运输成本(里程 × 单价,跑越远越贵🏎️)
- C3:冷藏成本(运输时间 + 卸货时间,生鲜配送必备🧊)
- C4:货损成本(时间越长损耗越高,水果运输痛点🍎)
- C5:时间窗惩罚(早到 / 迟到都要扣钱,准时是王道⏰)
% 计算时间窗惩罚示例
t_arrive = cust_times(j); % 到达时间
te = time_windows(j,1); tl = time_windows(j,2);
if t_arrive < te, C5 += V3*(te - t_arrive); % 早到罚等待费
elseif t_arrive > tl, C5 += V4*(t_arrive - tl); % 迟到罚违约金
end
3. 遗传操作:模拟生物进化的三大法宝
① 锦标赛选择(Tournament Selection)
- 规则:每次从种群中随机选 3 个个体,选成本最低的作为 “父母”👨👩
- 作用:避免优秀个体被淘汰,类似选秀节目中的 “晋级保护机制”✨
② OX 交叉(Order Crossover)
- 步骤:
- 随机选择两个切点,保留父代 1 的中间段(如客户 3→5)
- 按父代 2 的顺序填充剩余客户,保持相对顺序不变
- 效果:继承父代优质路径结构,类似 “基因重组”🧬
% OX交叉示例:父代1=[2,5,3,1,4],父代2=[4,1,5,3,2]
% 切点选2和4,中间段为5,3,1
% 子代1=剩余客户按父代2顺序填充:4, [5,3,1], 2 → [4,5,3,1,2]
③ 交换变异(Swap Mutation)
- 操作:随机交换染色体中两个客户的位置(如客户 2 和 4 互换)
- 意义:增加种群多样性,避免陷入局部最优,像给算法 “打补丁”🔄
三、实战运行:从随机搜索到最优路径 🛣️
1. 算法迭代过程
我们设定种群大小 100,迭代 200 次,算法会在每次迭代中输出当前最优成本:
2. 最优路径结果
解码最优染色体后,得到 3 条车辆路径:
路径特点:
- 载重均衡:每辆车载重均≤100 吨,避免浪费
- 时间窗合规:通过
cust_times
计算,所有客户到达时间均在时间窗内✅ - 成本最优:综合考虑各类成本,实现总费用最小化
四、调参技巧与常见问题 🛠️
1. 参数调整策略
参数 | 作用 | 调优建议 |
---|---|---|
pop_size | 种群规模 | 太小易早熟,太大耗时长,建议 50-200 |
crossover_rate | 交叉概率 | 过高破坏优质个体,过低降低进化速度,建议 0.7-0.9 |
mutation_rate | 变异概率 | 过低缺乏创新,过高变 “随机搜索”,建议 0.01-0.2 |
max_gen | 最大迭代次数 | 观察收敛曲线,稳定后可提前终止 |
2. 约束处理技巧
- 硬约束(载重 / 里程 / 车辆数):违反则设为无穷大成本(
total_cost = Inf
),强制淘汰无效解❌ - 软约束(时间窗):用惩罚函数转化为成本,允许一定程度违反,平衡解的可行性与优化性⚖️
3. 算法改进方向
- 混合算法:结合局部搜索(如 2-opt)提升解质量,类似 “遗传算法 + 精装修”🏠
- 动态建模:考虑实时交通数据、客户临时需求,开发在线调度系统📱
- 多目标优化:同时优化成本、碳排放、客户满意度,生成帕累托前沿解集🌍
五、总结:遗传算法的物流应用启示 📚
通过这个案例,我们看到遗传算法在解决复杂组合优化问题时的强大能力:
- 灵活性:适用于多种约束条件(时间窗、载重、里程),适配不同物流场景
- 可扩展性:轻松扩展到上百个客户点,只需调整参数即可应对大规模问题
- 工程价值:相比人工调度,可降低成本 10%-30%,显著提升企业效率💼
未来,随着物联网(IoT)和自动驾驶技术的发展,车辆路径优化将与实时数据深度融合。想象一下:货车通过传感器实时获取路况,算法动态调整路径,真正实现 “智慧物流”~🚛💨
动手实践:试着将客户数增加到 20 个,调整num_customers
和locations
,观察算法收敛速度变化~欢迎在评论区分享你的实验结果!👇
延伸阅读:
- 车辆路径问题经典文献:《The Vehicle Routing Problem》
- MATLAB 优化工具箱:
ga
函数官方文档 - 物流优化案例:DHL 如何用算法降低最后一公里成本🚀
完整代码(省流版)
%% 参数初始化
data = readmatrix("模糊.xlsx");
data(:, 1) = []; data(2: 3, :) = []; data(5, :) = [];
locations = data(:, 1: 2);
num_customers = length(locations) - 1;
aq_fuzzy = data(2: end, 3: 5);
w = [1 / 6, 4 / 6, 1 / 6];
q = sum(aq_fuzzy.*w,2)';
load time_window
time_windows = [te, tl]; % 随机生成两组向量
K = 5; % 最大车辆数
Q = 3; % 车辆载重限制
D = 500; % 车辆里程限制
speed = 20; % 车速(km/h)
t_per_unit = 1; % 卸货时间/单位需求
% 成本参数
V1 = 100; V2 = 1; P1 = 0.2; P2 = 0.5;
alpha1 = 0.01; alpha2 = 0.02; P = 10; % 货损系数与单价
V3 = 2; V4 = 5;
% 经纬网上任意两点间距离(km)
locations=locations.*pi/180;
m=size(locations,1);
for i=1:m
for j=1:m
h(i,j)=acos(cos(locations(i,2))*cos(locations(j,2))*cos(locations(i,1)-locations(j,1))+sin(locations(i,2))*sin(locations(j,2)));
d=6378.137.*h;
end
end
% 遗传算法参数
pop_size = 50; max_gen = 200;
crossover_rate = 0.8; mutation_rate = 0.1;
%% 初始化种群
population = zeros(pop_size, num_customers);
for i = 1:pop_size
population(i,:) = randperm(num_customers);
end
%% 遗传算法主循环
best_cost = Inf; best_chrom = [];
cost_history = zeros(max_gen, 1);
for gen = 1:max_gen
% 计算适应度
costs = zeros(pop_size, 1);
for i = 1:pop_size
[costs(i), comps, ~] = decode_chromosome(population(i,:), d, q, time_windows, ...
K, Q, D, speed, t_per_unit, V1, V2, P1, P2, alpha1, alpha2, P, V3, V4);
if costs(i) < best_cost
best_cost = costs(i);
best_chrom = population(i,:);
best_components = comps;
end
end
cost_history(gen) = best_cost;
% 锦标赛选择
parents = zeros(pop_size, num_customers);
for i = 1:pop_size
candidates = randperm(pop_size, 3);
[~, idx] = min(costs(candidates));
parents(i,:) = population(candidates(idx),:);
end
% OX交叉
offspring = [];
for i = 1:2:pop_size
p1 = parents(i,:); p2 = parents(i+1,:);
if rand < crossover_rate
[c1, c2] = ox_crossover(p1, p2);
else
c1 = p1; c2 = p2;
end
offspring = [offspring; c1; c2];
end
% 交换变异
for i = 1:pop_size
if rand < mutation_rate
pos = randperm(num_customers, 2);
offspring(i, pos) = offspring(i, fliplr(pos));
end
end
population = offspring;
fprintf('Generation %d: Best Cost = %.2f\n', gen, best_cost);
end
%% 结果展示
[~, comps, routes] = decode_chromosome(best_chrom, d, q, time_windows, ...
K, Q, D, speed, t_per_unit, V1, V2, P1, P2, alpha1, alpha2, P, V3, V4);
fprintf('最优总成本: %.2f\n', best_cost);
fprintf(' 车辆固定成本 C1: %.2f\n', comps(1));
fprintf(' 运输里程成本 C2: %.2f\n', comps(2));
fprintf(' 冷藏成本 C3: %.2f\n', comps(3));
fprintf(' 货损成本 C4: %.2f\n', comps(4));
fprintf(' 时间窗惩罚 C5: %.2f\n', comps(5));
disp('最优路径:');
for k = 1:length(routes)
if ~isempty(routes{k})
fprintf('车辆 %d: %s\n', k, mat2str(routes{k} + 1));
end
end
% 绘制路线图
% 使用 cellfun 和 isempty 检测空元素
emptyIndices = cellfun(@isempty, routes);
% 使用逻辑索引删除空元素
routes(emptyIndices) = [];
figure; hold on;
plot(locations(1,1),locations(1,2),'rs','MarkerSize',8,'DisplayName','配送中心');
plot(locations(2:end,1),locations(2:end,2),'bo','DisplayName','客户点');
colors = lines(numel(routes));
for i = 1:numel(routes)
route = routes{i}; xy = locations(route + 1,:);
plot(xy(:,1), xy(:,2),'-','LineWidth',1.5,'Color',colors(i,:), 'DisplayName',sprintf('车%d',i));
end
xlabel('X 坐标'); ylabel('Y 坐标'); title('最优配送路径'); legend('show'); hold off;
% 收敛曲线
figure;
plot(cost_history);
xlabel('迭代次数'); ylabel('总成本');
title('收敛曲线');
function [total_cost, comps, routes] = decode_chromosome(chrom, d, q, time_windows, ...
K, Q, D, speed, t_per_unit, V1, V2, P1, P2, alpha1, alpha2, P, V3, V4)
routes = cell(1, K); current_route = 0; current_load = 0; current_dist = 0;
used_vehicles = 1; cust_times = zeros(1, length(q));
% 分配客户到路径
for i = 1:length(chrom)
cust = chrom(i);
new_load = current_load + q(cust);
temp_route = [current_route, cust, 0];
% 计算临时距离
temp_dist = 0;
for j = 1:length(temp_route)-1
temp_dist = temp_dist + d(temp_route(j)+1, temp_route(j+1)+1);
end
if new_load <= Q && temp_dist <= D
current_route = [current_route, cust];
current_load = new_load;
current_dist = temp_dist;
else
% 结束当前车辆路径
routes{used_vehicles} = [current_route, 0];
used_vehicles = used_vehicles + 1;
if used_vehicles > K
total_cost = Inf; return;
end
current_route = [0, cust];
current_load = q(cust);
current_dist = d(1, cust+1) + d(cust+1,1);
end
end
routes{used_vehicles} = [current_route, 0];
% 计算各项成本
C1 = used_vehicles * V1;
C2 = 0; C3 = 0; C4 = 0; C5 = 0;
for k = 1:used_vehicles
route = routes{k};
if length(route) < 3, continue; end
% 运输成本
dist = sum(d(sub2ind(size(d), route(1:end-1)+1, route(2:end)+1)));
C2 = C2 + dist* V2;
% 冷藏成本
transport_time = dist / speed;
unload_time = sum(q(route(2:end-1))) * t_per_unit;
C3 = C3 + P1*transport_time + P2*unload_time;
% 货损成本
time = 0;
for j = 2:length(route)-1
from = route(j-1); to = route(j);
time = time + d(from+1, to+1)/speed;
cust_times(to) = time;
unload = q(to)*t_per_unit;
A = alpha1*time; B = alpha2*unload;
C4 = C4 + (1-exp(-A) + 1-exp(-B)) * P*q(to);
time = time + unload;
end
end
% 时间窗惩罚
for j = 1:length(q)
t_arrive = cust_times(j);
[te, tl] = deal(time_windows(j,1), time_windows(j,2));
if t_arrive < te
C5 = C5 + V3*(te - t_arrive);
elseif t_arrive > tl
C5 = C5 + V4*(t_arrive - tl);
end
end
total_cost = C1 + C2 + C3 + C4 + C5;
comps = [C1, C2, C3, C4, C5];
end
%% OX交叉函数
function [c1, c2] = ox_crossover(p1, p2)
n = length(p1);
cut = sort(randperm(n, 2));
segment = p1(cut(1):cut(2));
% 生成子代1
remain = p2(~ismember(p2, segment));
c1 = [remain(1:cut(1)-1), segment, remain(cut(1):end)];
% 生成子代2
segment = p2(cut(1):cut(2));
remain = p1(~ismember(p1, segment));
c2 = [remain(1:cut(1)-1), segment, remain(cut(1):end)];
end