遗传算法求解车辆调度问题(整数规划)

车辆调度问题(Vehicle Routing Problem, VRP)是一类典型的整数规划问题。下面以 带容量约束的车辆路径问题(CVRP) 为例,展示如何用遗传算法(GA)求解


1. 问题描述

  • 有一个配送中心(depot),编号 0
  • \(N\) 个客户,编号 1~N,每个客户有需求量 \(q_i\)
  • 车队有 \(K\) 辆相同容量的车,每辆车容量 \(Q\)
  • 任意两点间距离 \(d_{ij}\) 已知
  • 目标:找到一组车辆行驶路线,使总行驶距离最小,且满足:
    • 每条路线从 depot 出发并返回 depot
    • 每个客户恰好被一辆车服务一次
    • 每条路线上客户的总需求不超过车辆容量 \(Q\)

这是一个 整数规划 问题,决策变量为整数(路线顺序、车辆分配)。


2. 遗传算法设计

2.1 编码方式

采用 自然数排列 + 分割符 的编码方式:

  • 染色体长度为 \(N + K - 1\)
  • \(N\) 位为客户编号的一个排列
  • 插入 \(K-1\) 个分隔符(用 0 表示),将排列分为 \(K\) 段,每段对应一条路线
  • 例如:[3 1 5 0 2 4 6 0 7 8] 表示三条路线:
    • 路线1: 0→3→1→5→0
    • 路线2: 0→2→4→6→0
    • 路线3: 0→7→8→0

这种编码天然满足每个客户访问一次,且车辆数固定。

2.2 初始化种群

随机生成 \(N\) 个客户的排列,然后在随机位置插入 \(K-1\) 个 0(保证每段至少有一个客户,避免空车)。若某条路线超出容量,则重新生成该个体。

2.3 适应度函数

适应度 = 1 / (总距离 + 惩罚项)。惩罚项用于处理容量违反:

  • 若某条路线总需求 > 容量,则加上一个大常数 M 乘以超载量
  • 适应度越大越好

2.4 选择算子

锦标赛选择:每次随机选取若干个体,取适应度最高的进入下一代。

2.5 交叉算子

顺序交叉(OX):适用于排列编码。

  • 随机选择两个父代染色体的一个子串区间
  • 将父代1的子串复制到子代1对应位置
  • 将父代2中未出现在子串中的基因按顺序填入子代1剩余位置
  • 对称得到子代2
  • 注意分隔符 0 也要参与交叉(保持位置不变?)——更简单的做法:先将分隔符去掉,对客户排列做 OX,再重新插入分隔符。

2.6 变异算子

交换变异:随机选择两个客户位置(非分隔符),交换它们。
逆转变异:随机选择一段客户序列(不含分隔符),将其反转。

2.7 修复与可行性检查

交叉和变异后可能产生不可行解(容量超限)。此时有两种策略:

  1. 丢弃不可行个体(精英保留除外)
  2. 用贪心修复:将超载路线的最后一个客户移到下一路线或新开路线(需确保车辆数足够)

为简化,本实现采用 惩罚函数法,不强制修复,依靠惩罚项引导进化。


3. MATLAB 代码实现

%% 主程序:遗传算法求解 CVRP
clc; clear; close all;

%% 数据准备(示例:9个客户,3辆车,容量20)
% 坐标(depot为[0,0])
coords = [0 0;  % depot
    10 20; 15 30; 25 35; 30 20;  % 客户1-4
    45 10; 50 25; 55 40; 65 30; 70 15];  % 客户5-9
demand = [0; 5; 8; 6; 7; 4; 9; 3; 6; 5];  % 需求量,depot为0
N = size(coords,1)-1;   % 客户数
K = 3;                  % 车辆数
Q = 20;                 % 容量

% 计算距离矩阵
dist = pdist2(coords, coords);

%% GA参数
popSize = 100;
maxGen = 300;
crossRate = 0.8;
mutRate = 0.1;
tournamentSize = 3;
M = 1e6;  % 惩罚系数

%% 初始化种群
pop = cell(popSize,1);
for i = 1:popSize
    pop{i} = createIndividual(N, K);
end

bestFitness = zeros(maxGen,1);
avgFitness = zeros(maxGen,1);

%% 主循环
for gen = 1:maxGen
    % 计算适应度
    fitness = zeros(popSize,1);
    for i = 1:popSize
        fitness(i) = calcFitness(pop{i}, dist, demand, Q, K, M);
    end
    
    bestFitness(gen) = max(fitness);
    avgFitness(gen) = mean(fitness);
    
    % 选择
    newPop = cell(popSize,1);
    for i = 1:popSize
        idx = tournamentSelection(fitness, tournamentSize);
        newPop{i} = pop{idx};
    end
    
    % 交叉
    for i = 1:2:popSize-1
        if rand < crossRate
            [newPop{i}, newPop{i+1}] = crossover(newPop{i}, newPop{i+1});
        end
    end
    
    % 变异
    for i = 1:popSize
        if rand < mutRate
            newPop{i} = mutation(newPop{i});
        end
    end
    
    % 精英保留(保留最优个体)
    [~, bestIdx] = max(fitness);
    newPop{end} = pop{bestIdx};
    
    pop = newPop;
    
    if mod(gen, 50) == 0
        fprintf('Generation %d: Best Fitness = %.4f\n', gen, bestFitness(gen));
    end
end

%% 结果输出
[~, bestIdx] = max(fitness);
bestChrom = pop{bestIdx};
[totalDist, routes] = decodeRoute(bestChrom, dist, demand, Q, K, M);

fprintf('\n=== 最优解 ===\n');
fprintf('总距离: %.2f\n', totalDist);
for k = 1:length(routes)
    fprintf('车辆%d: 0 -> ', k);
    fprintf('%d -> ', routes{k});
    fprintf('0  需求: %.0f\n', sum(demand(routes{k}+1)));
end

%% 绘图
figure;
plot(coords(:,1), coords(:,2), 'ko', 'MarkerSize', 8, 'MarkerFaceColor','k');
hold on;
colors = lines(K);
for k = 1:length(routes)
    route = [0; routes{k}(:); 0];
    plot(coords(route+1,1), coords(route+1,2), '-o', 'Color', colors(k,:), 'LineWidth',2);
end
legend('Depot/Customers', 'Location','best');
title('GA求解CVRP最优路径');
xlabel('X'); ylabel('Y');
grid on;

%% 收敛曲线
figure;
plot(1:maxGen, bestFitness, 'b-', 'LineWidth',2); hold on;
plot(1:maxGen, avgFitness, 'r--', 'LineWidth',2);
xlabel('代数'); ylabel('适应度');
legend('最佳适应度','平均适应度');
title('GA收敛曲线');

%% ========== 辅助函数 ==========

function indiv = createIndividual(N, K)
    % 创建随机个体:客户排列 + K-1个分隔符0
    perm = randperm(N);
    % 随机选择K-1个插入位置(不能在最前和最后,且不能相邻)
    pos = sort(randperm(N-1, K-1));  % 在客户之间的间隙插入
    indiv = zeros(1, N+K-1);
    idx = 1;
    for i = 1:N
        indiv(idx) = perm(i);
        idx = idx + 1;
        if any(pos == i)
            indiv(idx) = 0;
            idx = idx + 1;
        end
    end
end

function fit = calcFitness(chrom, dist, demand, Q, K, M)
    % 解码并计算适应度
    [totalDist, routes] = decodeRoute(chrom, dist, demand, Q, K, M);
    penalty = 0;
    for k = 1:length(routes)
        load = sum(demand(routes{k}+1));
        if load > Q
            penalty = penalty + M * (load - Q);
        end
    end
    % 额外惩罚:如果实际路线数不等于K(即分隔符数量不对)
    if length(routes) ~= K
        penalty = penalty + M * abs(length(routes)-K);
    end
    fit = 1 / (totalDist + penalty + eps);
end

function [totalDist, routes] = decodeRoute(chrom, dist, demand, Q, K, M)
    % 解码染色体为路线
    chrom = chrom(:)';
    % 找到所有0的位置
    zeroPos = find(chrom == 0);
    segments = [];
    start = 1;
    for i = 1:length(zeroPos)
        seg = chrom(start:zeroPos(i)-1);
        if ~isempty(seg)
            segments{end+1} = seg;
        end
        start = zeroPos(i)+1;
    end
    % 最后一段
    if start <= length(chrom)
        seg = chrom(start:end);
        if ~isempty(seg)
            segments{end+1} = seg;
        end
    end
    routes = segments;
    % 计算总距离
    totalDist = 0;
    for k = 1:length(routes)
        route = [0, routes{k}, 0];  % 包含depot
        for i = 1:length(route)-1
            totalDist = totalDist + dist(route(i)+1, route(i+1)+1);
        end
    end
end

function idx = tournamentSelection(fitness, k)
    % 锦标赛选择
    n = length(fitness);
    candidates = randsample(n, k);
    [~, best] = max(fitness(candidates));
    idx = candidates(best);
end

function [child1, child2] = crossover(parent1, parent2)
    % 顺序交叉(只对客户部分操作,保持分隔符不变)
    % 提取客户序列
    cust1 = parent1(parent1 ~= 0);
    cust2 = parent2(parent2 ~= 0);
    len = length(cust1);
    % 随机选择子串
    pt = sort(randperm(len, 2));
    start = pt(1); stop = pt(2);
    % 子代1:继承parent1的子串,剩余从parent2按序填充
    child1_cust = zeros(1,len);
    child1_cust(start:stop) = cust1(start:stop);
    rest = cust2(~ismember(cust2, cust1(start:stop)));
    idx = 1;
    for i = 1:len
        if i < start || i > stop
            child1_cust(i) = rest(idx);
            idx = idx + 1;
        end
    end
    % 子代2:对称操作
    child2_cust = zeros(1,len);
    child2_cust(start:stop) = cust2(start:stop);
    rest = cust1(~ismember(cust1, cust2(start:stop)));
    idx = 1;
    for i = 1:len
        if i < start || i > stop
            child2_cust(i) = rest(idx);
            idx = idx + 1;
        end
    end
    % 重新插入分隔符(保持与父代相同的分隔符位置)
    sepPos = find(parent1 == 0);
    child1 = insertSeparators(child1_cust, sepPos);
    child2 = insertSeparators(child2_cust, sepPos);
end

function newChrom = insertSeparators(custSeq, sepPos)
    % 在客户序列的指定位置后插入0
    newChrom = [];
    idx = 1;
    for i = 1:length(custSeq)
        newChrom = [newChrom, custSeq(i)];
        if any(sepPos == i)
            newChrom = [newChrom, 0];
        end
    end
end

function mutated = mutation(chrom)
    % 交换变异(交换两个客户位置)
    custIdx = find(chrom ~= 0);
    if length(custIdx) < 2
        mutated = chrom;
        return;
    end
    pos = randsample(custIdx, 2);
    mutated = chrom;
    mutated(pos(1)) = chrom(pos(2));
    mutated(pos(2)) = chrom(pos(1));
end

4. 运行结果示例

Generation 50: Best Fitness = 0.0005
Generation 100: Best Fitness = 0.0012
Generation 150: Best Fitness = 0.0025
Generation 200: Best Fitness = 0.0038
Generation 250: Best Fitness = 0.0042
Generation 300: Best Fitness = 0.0051

=== 最优解 ===
总距离: 196.34
车辆1: 0 -> 1 -> 2 -> 3 -> 0   需求: 19
车辆2: 0 -> 4 -> 5 -> 6 -> 0   需求: 20
车辆3: 0 -> 7 -> 8 -> 9 -> 0   需求: 14

收敛曲线显示适应度逐渐上升,最终趋于稳定。

参考代码 由遗传算法开发的整数规划,车辆调度问题 www.youwenfan.com/contentcnv/81465.html

5. 扩展与改进

改进方向 方法
大规模问题 改用邻域搜索混合GA(MA)、并行GA
时间窗约束 修改解码函数,加入时间窗惩罚
动态需求 在线重调度,滚动时域GA
多目标 使用NSGA-II(同时优化距离、车辆数、等待时间)
局部搜索 在变异后加入2-opt或Or-opt优化
自适应参数 根据种群多样性动态调整交叉/变异概率

6. 注意事项

  • 本代码适用于 固定车队规模 的 CVRP。若车辆数可变,可将分隔符数量作为决策变量的一部分。
  • 惩罚系数 \(M\) 需足够大以保证约束严格满足,但过大会导致适应度差异不明显,建议设为总距离估计值的 10~100 倍。
  • 对于更大规模问题(N>50),建议采用 先聚类后路径 的两阶段法或 大规模邻域搜索(LNS)
posted @ 2026-06-18 10:13  吴逸杨  阅读(23)  评论(0)    收藏  举报