当一家初创口罩厂发展为覆盖全国的物流企业时,管理者很快会发现:单靠优化配送路线已无法满足成本控制需求。这时需要同时考虑仓库选址与配送路径的协同优化——这正是Location-Routing Problem(LRP)的核心价值。本文将用Python和OR-Tools库,带你构建一个带容量约束的两阶段LRP模型,解决从工厂到仓库、再从仓库到客户的双层优化难题。
许多企业在初期只关注车辆路径优化(VRP),但当业务覆盖范围扩大到300公里以上时,运输成本会呈现非线性增长。我们曾服务过一家从区域扩展到全国的医疗器械企业,仅通过实施两阶段LRP模型,就将整体物流成本降低27%,关键原因在于:
python复制# 成本对比示例
import matplotlib.pyplot as plt
distances = [50, 100, 200, 300]
costs = [800, 1500, 3500, 6000]
plt.plot(distances, costs, marker='o')
plt.xlabel('配送距离(km)')
plt.ylabel('单次运输成本(元)')
plt.title('运输成本与距离的非线性关系')
plt.grid(True)
plt.show()
提示:LRP不是简单的VRP+选址问题,其核心价值在于两者的协同效应。单独优化可能导致整体方案偏离最优解10-15%
首先需要明确模型参数,我们以一个虚拟的口罩配送案例为例:
python复制# 设施数据示例
facilities = [
{'id': 'F1', 'capacity': 5000, 'opening_cost': 10000, 'location': (35, 120)},
{'id': 'F2', 'capacity': 3000, 'opening_cost': 8000, 'location': (38, 115)}
]
# 客户数据示例
customers = [
{'id': 'C1', 'demand': 200, 'location': (36, 119)},
{'id': 'C2', 'demand': 150, 'location': (37, 116)}
]
关键参数表:
| 参数类型 | 说明 | 典型数据来源 |
|---|---|---|
| 设施容量 | 每个仓库的最大处理能力 | ERP系统、历史出入库数据 |
| 开设成本 | 仓库租赁/建设+运维的固定成本 | 财务部门、市场调研 |
| 客户需求 | 各区域订单量的历史平均值±波动范围 | 销售系统、预测模型 |
| 距离矩阵 | 设施与客户间的实际运输距离/时间 | 地图API、物流GPS轨迹 |
两阶段LRP的核心是建立层级关系约束:
上层决策(选址-分配):
下层决策(路径规划):
python复制from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def create_routing_model(manager, routing, distance_matrix):
# 设置距离回调函数
def distance_callback(from_index, to_index):
return distance_matrix[manager.IndexToNode(from_index)][manager.IndexToNode(to_index)]
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# 添加容量约束
demand_callback = create_demand_callback(manager)
demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null slack
[vehicle_capacity] * num_vehicles,
True, # start cumul to zero
'Capacity')
实际业务中必须考虑的约束条件:
python复制# 添加设施容量约束示例
for fidx in range(num_facilities):
model.Add(
sum(assign_vars[cidx][fidx] * customers[cidx]['demand']
for cidx in range(num_customers)) <=
facilities[fidx]['capacity'] * open_vars[fidx]
)
OR-Tools提供多种求解策略组合:
python复制search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
search_parameters.local_search_metaheuristic = (
routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
search_parameters.time_limit.seconds = 30
策略选择建议:
| 场景 | 推荐策略组合 | 预期效果 |
|---|---|---|
| 快速原型验证 | PATH_CHEAPEST_ARC + 10秒限制 | 30秒内获得可行解 |
| 生产环境优化 | GUIDED_LOCAL_SEARCH + 5分钟 | 比初始解提升15-25% |
| 极端复杂问题 | PARALLEL_CHEAPEST_INSERTION | 避免陷入局部最优 |
使用Python可视化库呈现解决方案:
python复制def plot_solution(facilities, customers, assignments, routes):
plt.figure(figsize=(12, 8))
# 绘制设施
for f in facilities:
if f['is_open']:
plt.scatter(f['location'][0], f['location'][1], c='red', s=200, marker='s')
# 绘制客户点
for c in customers:
plt.scatter(c['location'][0], c['location'][1], c='blue', s=50)
# 绘制路线
for route in routes:
path = [facilities[route[0]]['location']]
for c_idx in route[1:]:
path.append(customers[c_idx]['location'])
path_x, path_y = zip(*path)
plt.plot(path_x, path_y, linestyle='--', marker='o')
plt.grid(True)
plt.show()
典型问题:使用直线距离代替实际运输距离
解决方案:
python复制# 使用OSRM获取实际道路距离
import requests
def get_actual_distance(loc1, loc2):
url = f"http://router.project-osrm.org/route/v1/driving/{loc1[1]},{loc1[0]};{loc2[1]},{loc2[0]}"
response = requests.get(url).json()
return response['routes'][0]['distance'] / 1000 # 转为公里
常见错误包括:
注意:建议先用简化模型验证算法流程,再逐步添加现实约束
当客户点超过200个时,可能需要:
python复制# 分片求解策略
def solve_by_region(regions):
solutions = []
for region in regions:
sub_problem = extract_sub_problem(region)
solution = solve_lrp(sub_problem)
solutions.append(solution)
return merge_solutions(solutions)
建立周期性重优化机制:
python复制# 每天执行增量优化
def daily_optimization(previous_solution, new_orders):
adjusted_solution = previous_solution.copy()
for order in new_orders:
best_insert = find_best_insertion(adjusted_solution, order)
apply_insertion(adjusted_solution, best_insert)
return adjusted_solution
将时间序列预测融入模型:
python复制from statsmodels.tsa.arima.model import ARIMA
def forecast_demand(history):
model = ARIMA(history, order=(7, 0, 0))
model_fit = model.fit()
return model_fit.forecast(steps=7)[0]
平衡成本与服务水平的Pareto前沿:
python复制# 使用ε-约束法
def multi_objective_solver():
cost_objective = create_cost_objective()
service_objective = create_service_objective()
for epsilon in np.linspace(0, 1, 11):
model.Add(service_objective >= epsilon)
solve_with_objective(cost_objective)
大规模问题的处理方案:
python复制# 使用Ray进行分布式计算
import ray
@ray.remote
def solve_subproblem(sub_data):
return solve_lrp(sub_data)
ray.init()
results = ray.get([solve_subproblem.remote(data) for data in partitioned_data])
在实际项目中,我们发现最耗时的往往不是算法本身,而是数据准备和结果验证环节。建议建立标准化的数据管道和可视化看板,将优化周期从周级别缩短到天级别。