VRPTW(带时间窗的车辆路径问题)是物流配送领域的经典优化难题,简单来说就是如何在满足客户时间窗要求和车辆容量限制的前提下,规划出总成本最低的配送路线。这个问题看似简单,但随着客户数量增加,可能的解空间会呈指数级增长,传统方法很难在合理时间内找到满意解。
我第一次接触这个问题是在为某生鲜电商做配送优化时,当时用遗传算法折腾了两周都没得到理想结果,直到发现了ALNS(自适应大邻域搜索)算法。ALNS的核心思想很巧妙:通过动态调整多种破坏和修复算子的使用概率,在搜索过程中不断探索新的解空间区域。这就好比我们在迷宫中寻找出口时,既要有大胆尝试新路径的勇气(破坏算子),也要有及时修正路线的智慧(修复算子)。
Python实现ALNS有几个关键优势:一是NumPy等库提供了高效的矩阵运算能力,二是Matplotlib可以直观展示算法收敛过程,三是整体代码结构清晰易于调试。下面这段代码展示了VRPTW的基础数据结构:
python复制class Node:
def __init__(self):
self.id = 0 # 节点ID
self.x_coord = 0.0 # 横坐标
self.y_coord = 0.0 # 纵坐标
self.demand = 0.0 # 需求量
self.start_time = 0 # 最早服务时间
self.end_time = 0 # 最晚服务时间
self.service_time = 0 # 服务时长
class Sol:
def __init__(self):
self.obj = 0 # 目标函数值
self.node_id_list = [] # 节点ID序列
self.route_list = [] # 车辆路径集合
破坏算子的作用是从当前解中移除部分节点,为后续修复创造优化空间。在实际项目中我发现,破坏程度和算子选择会直接影响算法性能。随机破坏就像撒网捕鱼,覆盖面广但可能漏掉重点区域;而最坏破坏则像精准打击,专门针对成本高的节点。
随机破坏算子需要重点关注两个参数:
python复制def random_destroy(model):
d = random.uniform(model.rand_d_min, model.rand_d_max)
remove_num = int(d * len(model.demand_id_list))
return random.sample(range(len(model.demand_id_list)), remove_num)
最坏破坏算子的实现更有讲究。我的经验是不仅要考虑节点移除带来的即时成本变化,还要结合时间窗约束:
python复制def worst_destroy(model, sol):
delta_f = []
for node_id in sol.node_id_list:
temp_sol = copy.deepcopy(sol)
temp_sol.node_id_list.remove(node_id)
calObj(temp_sol, model)
# 综合考虑距离成本和时间窗违反程度
cost = (sol.obj - temp_sol.obj) * 0.7 + \
calculate_time_window_violation(temp_sol) * 0.3
delta_f.append(cost)
sorted_ids = sorted(range(len(delta_f)),
key=lambda k: delta_f[k], reverse=True)
d = random.randint(model.worst_d_min, model.worst_d_max)
return sorted_ids[:d]
实测中发现,将破坏比例控制在30%-40%时效果最佳。太保守会导致搜索空间有限,太激进又会丢失好的解结构。建议初期使用较高破坏比例(0.4-0.5)进行全局探索,后期逐渐降低到0.2-0.3进行局部优化。
修复算子负责将移除的节点重新插入到路径中,好的修复策略能显著提升解的质量。经过多次项目实践,我总结出几种修复算子的适用场景:
后悔值修复是我最喜欢用的算子,它通过计算次优选择的代价来做出更明智的决策。下面这个改进版的实现考虑了时间窗约束:
python复制def regret_repair(remove_list, model, sol, n=3):
unassigned = [sol.node_id_list[i] for i in remove_list]
assigned = [n for i,n in enumerate(sol.node_id_list) if i not in remove_list]
while unassigned:
best_node, best_pos, max_regret = None, None, -float('inf')
for node in unassigned:
costs = []
for i in range(len(assigned)+1):
temp = assigned[:i] + [node] + assigned[i:]
temp_sol = Sol()
temp_sol.node_id_list = temp
calObj(temp_sol, model)
costs.append((i, temp_sol.obj))
# 按成本排序并计算后悔值
costs.sort(key=lambda x: x[1])
regret = sum(costs[i][1]-costs[0][1] for i in range(1, min(n, len(costs))))
if regret > max_regret:
max_regret = regret
best_node = node
best_pos = costs[0][0]
assigned.insert(best_pos, best_node)
unassigned.remove(best_node)
new_sol = Sol()
new_sol.node_id_list = assigned
return new_sol
在实际物流配送案例中,我发现这样的算子组合策略效果很好:
ALNS最精妙的部分是其自适应机制,它能根据算子历史表现动态调整使用概率。这个机制主要涉及三个核心参数:
奖励分数(r1, r2, r3):
权重衰减系数rho(建议0.1-0.3):
控制历史表现对新权重的影响程度
调整频率pu(建议20-50次迭代):
更新算子权重的间隔周期
python复制def update_weights(model):
# 更新破坏算子权重
for i in range(len(model.d_weight)):
if model.d_select[i] > 0:
model.d_weight[i] = model.d_weight[i]*(1-model.rho) + \
model.rho*model.d_score[i]/model.d_select[i]
# 更新修复算子权重
for i in range(len(model.r_weight)):
if model.r_select[i] > 0:
model.r_weight[i] = model.r_weight[i]*(1-model.rho) + \
model.rho*model.r_score[i]/model.r_select[i]
在参数调优方面,我建议采用网格搜索结合人工经验的方式。例如在某次冷链物流项目中,我们通过实验发现以下参数组合效果最佳:
| 参数 | 推荐值 | 作用 |
|---|---|---|
| rand_d_min | 0.2 | 最小随机破坏比例 |
| rand_d_max | 0.5 | 最大随机破坏比例 |
| worst_d_min | 5 | 最小最坏破坏节点数 |
| worst_d_max | 15 | 最大最坏破坏节点数 |
| regret_n | 3 | 后悔值计算深度 |
| r1 | 40 | 最优解奖励 |
| r2 | 15 | 改进解奖励 |
| r3 | 5 | 接受解奖励 |
| rho | 0.1 | 权重衰减系数 |
为了验证算法效果,我在Solomon标准测试集的R101实例上进行了实验。通过对比不同算子组合的表现,得出一些实用结论:
收敛速度对比:
计算效率分析:
python复制# 性能测试代码示例
def benchmark():
cases = ['R101', 'C201', 'RC105']
operators = [
('random', 'greedy'),
('worst', 'regret'),
('adaptive', 'adaptive')
]
results = []
for case in cases:
for destroy, repair in operators:
start = time.time()
obj = run_case(case, destroy, repair)
elapsed = time.time() - start
results.append((case, destroy, repair, obj, elapsed))
return pd.DataFrame(results,
columns=['Case', 'Destroy', 'Repair', 'Objective', 'Time(s)'])
基于多个项目的实战经验,我总结出以下建议:
在最近的一个医药配送项目中,通过调整破坏算子的时间窗权重,我们将准时交付率从82%提升到了95%。关键是在计算节点破坏价值时,不仅考虑距离成本,还加入了时间紧迫度因子:
python复制def time_criticality(node, current_time):
""" 计算节点时间紧迫度 """
remaining = node.end_time - current_time
total_window = node.end_time - node.start_time
return 0.5 + (total_window - remaining) / (2 * total_window)
在实际应用ALNS算法时,我遇到过不少坑,这里分享几个典型问题的解决方法:
问题1:算法早期收敛过快
问题2:解的质量波动大
python复制def adaptive_threshold(iter, max_iter):
base = 0.2 # 初始接受阈值
decay = 0.5 # 衰减系数
return base * (decay ** (iter / max_iter))
问题3:时间窗约束经常违反
python复制def is_time_feasible(route, new_node, model):
current_time = 0
for i in range(len(route)):
from_node = route[i] if i==0 else route[i-1]
to_node = route[i]
travel_time = model.time_matrix[from_node, to_node]
current_time = max(current_time + travel_time,
model.demand_dict[to_node].start_time)
service_time = model.demand_dict[to_node].service_time
if new_node == to_node:
arrival = current_time
if arrival > model.demand_dict[new_node].end_time:
return False
current_time += service_time
return True
对于大规模VRPTW问题(>200节点),建议采用以下优化策略:
经过多个项目的积累,我总结出几个提升ALNS性能的高级技巧:
1. 混合初始解生成
不要完全依赖随机初始解,可以结合以下方法:
python复制def generate_initial_solution(model, method='hybrid'):
if method == 'random':
return random.sample(model.demand_id_list, len(model.demand_id_list))
elif method == 'nearest_neighbor':
# 实现最近邻策略
...
elif method == 'hybrid':
# 组合多种策略
part1 = nearest_neighbor_part(model, 0.3)
part2 = time_window_priority_part(model, 0.3)
part3 = random.sample(model.demand_id_list, int(0.4*len(model.demand_id_list)))
return part1 + part2 + part3
2. 精英解保护机制
维护一个精英解集合,定期用精英解片段替换当前解中的低效部分:
python复制elite_pool = []
def update_elite_pool(sol, max_size=5):
if len(elite_pool) < max_size:
elite_pool.append(copy.deepcopy(sol))
else:
worst_idx = max(range(len(elite_pool)), key=lambda i: elite_pool[i].obj)
if sol.obj < elite_pool[worst_idx].obj:
elite_pool[worst_idx] = copy.deepcopy(sol)
def elite_repair(remove_list, model, sol):
if not elite_pool or random.random() > 0.3:
return createRegretRepair(remove_list, model, sol)
elite = random.choice(elite_pool)
# 从精英解中提取优质路径片段
best_fragment = find_best_fragment(elite, model)
# 将片段插入当前解
new_sol = insert_fragment(sol, best_fragment)
return new_sol
3. 动态参数调整
根据搜索进度自动调整算法参数:
python复制def adaptive_parameters(iter, max_iter):
progress = iter / max_iter
return {
'random_d_min': max(0.1, 0.3 - progress*0.2),
'random_d_max': max(0.3, 0.6 - progress*0.3),
'temperature': 0.2 * (1 - progress) # 模拟退火温度
}
4. 多目标优化处理
当需要同时优化距离成本和时间成本时,可以采用加权法或帕累托前沿法:
python复制def multi_objective_eval(sol, model):
distance_cost = sol.cost_of_distance
time_cost = sol.cost_of_time
# 加权法
if model.opt_type == 0: # 最小化距离
return distance_cost * 0.7 + time_cost * 0.3
else: # 最小化时间
return distance_cost * 0.3 + time_cost * 0.7
在代码实现时,建议采用模块化设计,将不同功能的代码分离:
alns_framework.py:算法主流程destroy_operators.py:各种破坏算子repair_operators.py:各种修复算子utils.py:辅助函数和工具类visualization.py:结果可视化这样的结构既方便调试单个算子,也便于团队协作开发。我在GitHub上看到一个不错的ALNS实现框架,虽然需要根据具体问题修改,但架构设计值得参考。