pickup-delivery-problem
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChinesePickup and Delivery Problem (PDP)
取送问题(PDP)
You are an expert in Pickup and Delivery Problems and paired request routing optimization. Your goal is to help design optimal routes where vehicles must pick up goods or passengers from origins and deliver them to destinations, respecting pairing constraints, precedence, and capacity throughout the route.
你是取送问题和配对请求路径优化方面的专家。你的目标是帮助设计最优路径,让车辆必须从起点取货或接乘客,然后送到目的地,同时在整个路径中遵守配对约束、优先级和容量限制。
Initial Assessment
初始评估
Before solving PDP instances, understand:
-
Problem Variant
- One-to-one (each pickup paired with delivery)?
- Many-to-many (multiple pickups/deliveries)?
- Dial-a-ride (passenger transportation)?
- Same-day courier service?
- Moving/relocation services?
-
Pairing Constraints
- Hard pairing (pickup i MUST precede delivery i)?
- Time window between pickup and delivery?
- Maximum ride time (dial-a-ride)?
- Can pickup/delivery be split across vehicles? (usually NO)
-
Capacity Considerations
- Is capacity consumed from pickup to delivery?
- LIFO (last-in-first-out) constraint?
- Vehicle capacity during entire route?
-
Temporal Constraints
- Time windows at pickup locations?
- Time windows at delivery locations?
- Maximum delivery lag after pickup?
- Service times at each location?
-
Problem Scale
- Small (< 20 requests): Exact methods possible
- Medium (20-100 requests): Advanced heuristics
- Large (100+ requests): Metaheuristics required
在解决PDP实例之前,需要了解以下内容:
-
问题变体
- 一对一(每个取货对应一个送货)?
- 多对多(多个取货/送货)?
- 呼叫接送(乘客运输)?
- 当日快递服务?
- 搬家/搬迁服务?
-
配对约束
- 强制配对(取货i必须在送货i之前)?
- 取货和送货之间的时间窗口?
- 最长乘车时间(呼叫接送)?
- 取货/送货能否拆分到多辆车?(通常不行)
-
容量考虑
- 从取货到送货期间会占用容量吗?
- 后进先出(LIFO)约束?
- 整个路径中的车辆容量?
-
时间约束
- 取货地点的时间窗口?
- 送货地点的时间窗口?
- 取货后最长送货延迟时间?
- 每个地点的服务时间?
-
问题规模
- 小型(<20个请求):可使用精确方法
- 中型(20-100个请求):需要高级启发式算法
- 大型(100+个请求):必须使用元启发式算法
Mathematical Formulation
数学模型
Pickup and Delivery VRP (PDVRP)
带取送的车辆路径问题(PDVRP)
Sets:
- N = {0, 1, ..., 2n}: Nodes (0 = depot, 1..n = pickups, n+1..2n = deliveries)
- P = {1, ..., n}: Pickup nodes
- D = {n+1, ..., 2n}: Delivery nodes
- K = {1, ..., m}: Vehicles
Parameters:
- c_{ij}: Cost/distance from node i to j
- t_{ij}: Travel time from i to j
- s_i: Service time at node i
- q_i: Load change at node i (positive for pickup, negative for delivery)
- [e_i, l_i]: Time window at node i
- Q: Vehicle capacity
Decision Variables:
- x_{ijk} ∈ {0,1}: 1 if vehicle k travels from i to j
- w_{ik} ≥ 0: Arrival time of vehicle k at node i
- u_{ik} ≥ 0: Load of vehicle k when leaving node i
Objective Function:
Minimize: Σ_{k∈K} Σ_{i∈N} Σ_{j∈N} c_{ij} * x_{ijk}Constraints:
1. Each pickup visited exactly once:
Σ_{k∈K} Σ_{j∈N, j≠i} x_{ijk} = 1, ∀i ∈ P
2. Each delivery visited exactly once:
Σ_{k∈K} Σ_{j∈N, j≠i} x_{ijk} = 1, ∀i ∈ D
3. Pickup and delivery on same vehicle:
Σ_{j∈N, j≠i} x_{ijk} = Σ_{j∈N, j≠(n+i)} x_{(n+i)jk}, ∀i ∈ P, ∀k ∈ K
4. Pickup before delivery (precedence):
w_{ik} + s_i + t_{i,n+i} ≤ w_{n+i,k}, ∀i ∈ P, ∀k ∈ K
5. Flow conservation:
Σ_{i∈N, i≠h} x_{ihk} = Σ_{j∈N, j≠h} x_{hjk}, ∀h ∈ N\{0}, ∀k ∈ K
6. Time consistency:
w_{ik} + s_i + t_{ij} ≤ w_{jk} + M*(1 - x_{ijk}), ∀i,j ∈ N, ∀k ∈ K
7. Time windows:
e_i ≤ w_{ik} ≤ l_i, ∀i ∈ N, ∀k ∈ K
8. Capacity tracking:
u_{jk} ≥ u_{ik} + q_j - Q*(1 - x_{ijk}), ∀i,j ∈ N, ∀k ∈ K
0 ≤ u_{ik} ≤ Q, ∀i ∈ N, ∀k ∈ K
9. Binary variables:
x_{ijk} ∈ {0,1}集合:
- N = {0, 1, ..., 2n}:节点(0 = 仓库,1..n = 取货点,n+1..2n = 送货点)
- P = {1, ..., n}:取货节点
- D = {n+1, ..., 2n}:送货节点
- K = {1, ..., m}:车辆集合
参数:
- c_{ij}:从节点i到j的成本/距离
- t_{ij}:从i到j的行驶时间
- s_i:节点i的服务时间
- q_i:节点i的负载变化(取货为正,送货为负)
- [e_i, l_i]:节点i的时间窗口
- Q:车辆容量
决策变量:
- x_{ijk} ∈ {0,1}:如果车辆k从i行驶到j则为1
- w_{ik} ≥ 0:车辆k到达节点i的时间
- u_{ik} ≥ 0:车辆k离开节点i时的负载
目标函数:
Minimize: Σ_{k∈K} Σ_{i∈N} Σ_{j∈N} c_{ij} * x_{ijk}约束条件:
1. 每个取货点恰好被访问一次:
Σ_{k∈K} Σ_{j∈N, j≠i} x_{ijk} = 1, ∀i ∈ P
2. 每个送货点恰好被访问一次:
Σ_{k∈K} Σ_{j∈N, j≠i} x_{ijk} = 1, ∀i ∈ D
3. 取货和送货使用同一辆车:
Σ_{j∈N, j≠i} x_{ijk} = Σ_{j∈N, j≠(n+i)} x_{(n+i)jk}, ∀i ∈ P, ∀k ∈ K
4. 取货在送货之前(优先级):
w_{ik} + s_i + t_{i,n+i} ≤ w_{n+i,k}, ∀i ∈ P, ∀k ∈ K
5. 流量守恒:
Σ_{i∈N, i≠h} x_{ihk} = Σ_{j∈N, j≠h} x_{hjk}, ∀h ∈ N\{0}, ∀k ∈ K
6. 时间一致性:
w_{ik} + s_i + t_{ij} ≤ w_{jk} + M*(1 - x_{ijk}), ∀i,j ∈ N, ∀k ∈ K
7. 时间窗口:
e_i ≤ w_{ik} ≤ l_i, ∀i ∈ N, ∀k ∈ K
8. 容量跟踪:
u_{jk} ≥ u_{ik} + q_j - Q*(1 - x_{ijk}), ∀i,j ∈ N, ∀k ∈ K
0 ≤ u_{ik} ≤ Q, ∀i ∈ N, ∀k ∈ K
9. 二进制变量:
x_{ijk} ∈ {0,1}Exact and Heuristic Algorithms
精确算法与启发式算法
1. Insertion Heuristic for PDP
1. PDP插入启发式算法
python
import numpy as np
import random
def pdp_insertion_heuristic(dist_matrix, time_matrix, requests,
vehicle_capacity, num_vehicles,
depot=0, max_route_time=480):
"""
Sequential insertion heuristic for PDP
Args:
dist_matrix: distance matrix
time_matrix: travel time matrix
requests: list of dicts with 'pickup_node', 'delivery_node',
'quantity', 'pickup_tw', 'delivery_tw'
vehicle_capacity: vehicle capacity
num_vehicles: number of vehicles
depot: depot index
max_route_time: maximum route duration
Returns:
solution dictionary
"""
def check_feasibility(route, pickup_idx, delivery_idx,
pickup_pos, delivery_pos):
"""
Check if inserting pickup and delivery is feasible
Must check:
- Capacity along entire route
- Time windows
- Precedence (pickup before delivery)
"""
# Build temporary route
temp_route = route.copy()
# Insert in correct order (pickup first)
if pickup_pos < delivery_pos:
temp_route.insert(pickup_pos, pickup_idx)
temp_route.insert(delivery_pos, delivery_idx)
else:
temp_route.insert(delivery_pos, delivery_idx)
temp_route.insert(pickup_pos, pickup_idx)
# Check capacity
current_load = 0
node_to_request = {}
for req_idx, req in enumerate(requests):
node_to_request[req['pickup_node']] = (req_idx, 'pickup')
node_to_request[req['delivery_node']] = (req_idx, 'delivery')
for node in temp_route[1:-1]: # Skip depot
if node in node_to_request:
req_idx, action = node_to_request[node]
if action == 'pickup':
current_load += requests[req_idx]['quantity']
else:
current_load -= requests[req_idx]['quantity']
if current_load > vehicle_capacity or current_load < 0:
return False
# Check time windows and precedence
current_time = 0
service_times = {} # Default service time
for i in range(len(temp_route) - 1):
current_node = temp_route[i]
next_node = temp_route[i+1]
# Travel to next node
current_time += time_matrix[current_node][next_node]
# Check time window
if next_node in node_to_request:
req_idx, action = node_to_request[next_node]
req = requests[req_idx]
if action == 'pickup':
tw = req['pickup_tw']
else:
tw = req['delivery_tw']
if current_time > tw[1]:
return False # Too late
# Wait if early
current_time = max(current_time, tw[0])
# Add service time
current_time += service_times.get(next_node, 10)
# Check total route time
if current_time > max_route_time:
return False
return True
def calculate_insertion_cost(route, pickup_idx, delivery_idx,
pickup_pos, delivery_pos):
"""Calculate cost increase of insertion"""
pickup_node = requests[pickup_idx]['pickup_node']
delivery_node = requests[delivery_idx]['delivery_node']
# Cost of inserting pickup
i = route[pickup_pos - 1]
j = route[pickup_pos]
pickup_cost = (dist_matrix[i][pickup_node] +
dist_matrix[pickup_node][j] -
dist_matrix[i][j])
# Cost of inserting delivery (accounting for pickup already inserted)
temp_route = route.copy()
temp_route.insert(pickup_pos, pickup_node)
i = temp_route[delivery_pos - 1]
j = temp_route[delivery_pos]
delivery_cost = (dist_matrix[i][delivery_node] +
dist_matrix[delivery_node][j] -
dist_matrix[i][j])
return pickup_cost + delivery_cost
# Initialize routes
routes = [[depot, depot] for _ in range(num_vehicles)]
unassigned_requests = list(range(len(requests)))
# Sort requests by some criterion (e.g., earliest pickup time)
unassigned_requests.sort(
key=lambda r: requests[r]['pickup_tw'][0])
# Insert requests one by one
for req_idx in unassigned_requests[:]:
best_route = None
best_pickup_pos = None
best_delivery_pos = None
best_cost = float('inf')
# Try inserting in each route
for route_idx, route in enumerate(routes):
# Try all valid insertion positions
for pickup_pos in range(1, len(route)):
for delivery_pos in range(pickup_pos + 1, len(route) + 1):
if check_feasibility(route, req_idx, req_idx,
pickup_pos, delivery_pos):
cost = calculate_insertion_cost(
route, req_idx, req_idx,
pickup_pos, delivery_pos)
if cost < best_cost:
best_cost = cost
best_route = route_idx
best_pickup_pos = pickup_pos
best_delivery_pos = delivery_pos
# Insert request in best position
if best_route is not None:
pickup_node = requests[req_idx]['pickup_node']
delivery_node = requests[req_idx]['delivery_node']
routes[best_route].insert(best_pickup_pos, pickup_node)
routes[best_route].insert(best_delivery_pos, delivery_node)
unassigned_requests.remove(req_idx)
# Calculate total distance
total_distance = sum(
sum(dist_matrix[route[i]][route[i+1]] for i in range(len(route)-1))
for route in routes if len(route) > 2
)
# Remove empty routes
routes = [r for r in routes if len(r) > 2]
return {
'routes': routes,
'total_distance': total_distance,
'num_vehicles': len(routes),
'unassigned': unassigned_requests
}python
import numpy as np
import random
def pdp_insertion_heuristic(dist_matrix, time_matrix, requests,
vehicle_capacity, num_vehicles,
depot=0, max_route_time=480):
"""
Sequential insertion heuristic for PDP
Args:
dist_matrix: distance matrix
time_matrix: travel time matrix
requests: list of dicts with 'pickup_node', 'delivery_node',
'quantity', 'pickup_tw', 'delivery_tw'
vehicle_capacity: vehicle capacity
num_vehicles: number of vehicles
depot: depot index
max_route_time: maximum route duration
Returns:
solution dictionary
"""
def check_feasibility(route, pickup_idx, delivery_idx,
pickup_pos, delivery_pos):
"""
Check if inserting pickup and delivery is feasible
Must check:
- Capacity along entire route
- Time windows
- Precedence (pickup before delivery)
"""
# Build temporary route
temp_route = route.copy()
# Insert in correct order (pickup first)
if pickup_pos < delivery_pos:
temp_route.insert(pickup_pos, pickup_idx)
temp_route.insert(delivery_pos, delivery_idx)
else:
temp_route.insert(delivery_pos, delivery_idx)
temp_route.insert(pickup_pos, pickup_idx)
# Check capacity
current_load = 0
node_to_request = {}
for req_idx, req in enumerate(requests):
node_to_request[req['pickup_node']] = (req_idx, 'pickup')
node_to_request[req['delivery_node']] = (req_idx, 'delivery')
for node in temp_route[1:-1]: # Skip depot
if node in node_to_request:
req_idx, action = node_to_request[node]
if action == 'pickup':
current_load += requests[req_idx]['quantity']
else:
current_load -= requests[req_idx]['quantity']
if current_load > vehicle_capacity or current_load < 0:
return False
# Check time windows and precedence
current_time = 0
service_times = {} # Default service time
for i in range(len(temp_route) - 1):
current_node = temp_route[i]
next_node = temp_route[i+1]
# Travel to next node
current_time += time_matrix[current_node][next_node]
# Check time window
if next_node in node_to_request:
req_idx, action = node_to_request[next_node]
req = requests[req_idx]
if action == 'pickup':
tw = req['pickup_tw']
else:
tw = req['delivery_tw']
if current_time > tw[1]:
return False # Too late
# Wait if early
current_time = max(current_time, tw[0])
# Add service time
current_time += service_times.get(next_node, 10)
# Check total route time
if current_time > max_route_time:
return False
return True
def calculate_insertion_cost(route, pickup_idx, delivery_idx,
pickup_pos, delivery_pos):
"""Calculate cost increase of insertion"""
pickup_node = requests[pickup_idx]['pickup_node']
delivery_node = requests[delivery_idx]['delivery_node']
# Cost of inserting pickup
i = route[pickup_pos - 1]
j = route[pickup_pos]
pickup_cost = (dist_matrix[i][pickup_node] +
dist_matrix[pickup_node][j] -
dist_matrix[i][j])
# Cost of inserting delivery (accounting for pickup already inserted)
temp_route = route.copy()
temp_route.insert(pickup_pos, pickup_node)
i = temp_route[delivery_pos - 1]
j = temp_route[delivery_pos]
delivery_cost = (dist_matrix[i][delivery_node] +
dist_matrix[delivery_node][j] -
dist_matrix[i][j])
return pickup_cost + delivery_cost
# Initialize routes
routes = [[depot, depot] for _ in range(num_vehicles)]
unassigned_requests = list(range(len(requests)))
# Sort requests by some criterion (e.g., earliest pickup time)
unassigned_requests.sort(
key=lambda r: requests[r]['pickup_tw'][0])
# Insert requests one by one
for req_idx in unassigned_requests[:]:
best_route = None
best_pickup_pos = None
best_delivery_pos = None
best_cost = float('inf')
# Try inserting in each route
for route_idx, route in enumerate(routes):
# Try all valid insertion positions
for pickup_pos in range(1, len(route)):
for delivery_pos in range(pickup_pos + 1, len(route) + 1):
if check_feasibility(route, req_idx, req_idx,
pickup_pos, delivery_pos):
cost = calculate_insertion_cost(
route, req_idx, req_idx,
pickup_pos, delivery_pos)
if cost < best_cost:
best_cost = cost
best_route = route_idx
best_pickup_pos = pickup_pos
best_delivery_pos = delivery_pos
# Insert request in best position
if best_route is not None:
pickup_node = requests[req_idx]['pickup_node']
delivery_node = requests[req_idx]['delivery_node']
routes[best_route].insert(best_pickup_pos, pickup_node)
routes[best_route].insert(best_delivery_pos, delivery_node)
unassigned_requests.remove(req_idx)
# Calculate total distance
total_distance = sum(
sum(dist_matrix[route[i]][route[i+1]] for i in range(len(route)-1))
for route in routes if len(route) > 2
)
# Remove empty routes
routes = [r for r in routes if len(r) > 2]
return {
'routes': routes,
'total_distance': total_distance,
'num_vehicles': len(routes),
'unassigned': unassigned_requests
}2. PDP with OR-Tools
2. 使用OR-Tools解决PDP
python
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def solve_pdp_ortools(locations, requests, vehicle_capacity,
num_vehicles, depot=0, time_limit=60):
"""
Solve PDP using Google OR-Tools
Args:
locations: list of (x, y) coordinates for all locations
requests: list of dicts:
- pickup: pickup location index
- delivery: delivery location index
- quantity: load quantity
- pickup_tw: (early, late) time window
- delivery_tw: (early, late) time window
vehicle_capacity: vehicle capacity
num_vehicles: number of vehicles
depot: depot index
time_limit: time limit in seconds
Returns:
solution dictionary
"""
import math
n_locations = len(locations)
# Build distance and time matrices
dist_matrix = np.zeros((n_locations, n_locations))
time_matrix = np.zeros((n_locations, n_locations))
for i in range(n_locations):
for j in range(n_locations):
dist = math.sqrt((locations[i][0] - locations[j][0])**2 +
(locations[i][1] - locations[j][1])**2)
dist_matrix[i][j] = dist
time_matrix[i][j] = dist / 40 * 60 # 40 km/h in minutes
# Create routing manager
manager = pywrapcp.RoutingIndexManager(n_locations, num_vehicles, depot)
# Create routing model
routing = pywrapcp.RoutingModel(manager)
# Distance callback
def distance_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return int(dist_matrix[from_node][to_node] * 100)
distance_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index)
# Time callback
def time_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return int(time_matrix[from_node][to_node] + 10) # +10 min service
time_callback_index = routing.RegisterTransitCallback(time_callback)
# Add time dimension
routing.AddDimension(
time_callback_index,
30, # allow waiting time
3000, # maximum time per vehicle
False,
'Time')
time_dimension = routing.GetDimensionOrDie('Time')
# Add capacity dimension with pickups and deliveries
def demand_callback(from_index):
"""Returns the demand at the node"""
from_node = manager.IndexToNode(from_index)
# Check if this is a pickup or delivery
for req in requests:
if from_node == req['pickup']:
return req['quantity']
elif from_node == req['delivery']:
return -req['quantity']
return 0
demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
[vehicle_capacity] * num_vehicles,
True, # start cumul to zero
'Capacity')
# Add pickup and delivery constraints
for request in requests:
pickup_index = manager.NodeToIndex(request['pickup'])
delivery_index = manager.NodeToIndex(request['delivery'])
# Pickup and delivery must be on same route
routing.solver().Add(
routing.VehicleVar(pickup_index) ==
routing.VehicleVar(delivery_index))
# Pickup must occur before delivery
routing.solver().Add(
time_dimension.CumulVar(pickup_index) <=
time_dimension.CumulVar(delivery_index))
# Add time windows
time_dimension.CumulVar(pickup_index).SetRange(
int(request['pickup_tw'][0]),
int(request['pickup_tw'][1]))
time_dimension.CumulVar(delivery_index).SetRange(
int(request['delivery_tw'][0]),
int(request['delivery_tw'][1]))
# Search parameters
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 = time_limit
# Solve
solution = routing.SolveWithParameters(search_parameters)
if solution:
routes = []
total_distance = 0
for vehicle_id in range(num_vehicles):
index = routing.Start(vehicle_id)
route = []
route_times = []
while not routing.IsEnd(index):
node = manager.IndexToNode(index)
time_var = time_dimension.CumulVar(index)
route.append(node)
route_times.append(solution.Value(time_var))
index = solution.Value(routing.NextVar(index))
route.append(manager.IndexToNode(index))
time_var = time_dimension.CumulVar(index)
route_times.append(solution.Value(time_var))
if len(route) > 2:
routes.append({
'vehicle_id': vehicle_id,
'route': route,
'times': route_times
})
# Calculate distance
route_distance = sum(dist_matrix[route[i]][route[i+1]]
for i in range(len(route)-1))
total_distance += route_distance
return {
'status': 'Optimal',
'routes': routes,
'total_distance': total_distance,
'num_vehicles': len(routes)
}
else:
return {
'status': 'No solution found',
'routes': None
}python
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
def solve_pdp_ortools(locations, requests, vehicle_capacity,
num_vehicles, depot=0, time_limit=60):
"""
Solve PDP using Google OR-Tools
Args:
locations: list of (x, y) coordinates for all locations
requests: list of dicts:
- pickup: pickup location index
- delivery: delivery location index
- quantity: load quantity
- pickup_tw: (early, late) time window
- delivery_tw: (early, late) time window
vehicle_capacity: vehicle capacity
num_vehicles: number of vehicles
depot: depot index
time_limit: time limit in seconds
Returns:
solution dictionary
"""
import math
n_locations = len(locations)
# Build distance and time matrices
dist_matrix = np.zeros((n_locations, n_locations))
time_matrix = np.zeros((n_locations, n_locations))
for i in range(n_locations):
for j in range(n_locations):
dist = math.sqrt((locations[i][0] - locations[j][0])**2 +
(locations[i][1] - locations[j][1])**2)
dist_matrix[i][j] = dist
time_matrix[i][j] = dist / 40 * 60 # 40 km/h in minutes
# Create routing manager
manager = pywrapcp.RoutingIndexManager(n_locations, num_vehicles, depot)
# Create routing model
routing = pywrapcp.RoutingModel(manager)
# Distance callback
def distance_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return int(dist_matrix[from_node][to_node] * 100)
distance_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(distance_callback_index)
# Time callback
def time_callback(from_index, to_index):
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return int(time_matrix[from_node][to_node] + 10) # +10 min service
time_callback_index = routing.RegisterTransitCallback(time_callback)
# Add time dimension
routing.AddDimension(
time_callback_index,
30, # allow waiting time
3000, # maximum time per vehicle
False,
'Time')
time_dimension = routing.GetDimensionOrDie('Time')
# Add capacity dimension with pickups and deliveries
def demand_callback(from_index):
"""Returns the demand at the node"""
from_node = manager.IndexToNode(from_index)
# Check if this is a pickup or delivery
for req in requests:
if from_node == req['pickup']:
return req['quantity']
elif from_node == req['delivery']:
return -req['quantity']
return 0
demand_callback_index = routing.RegisterUnaryTransitCallback(demand_callback)
routing.AddDimensionWithVehicleCapacity(
demand_callback_index,
0, # null capacity slack
[vehicle_capacity] * num_vehicles,
True, # start cumul to zero
'Capacity')
# Add pickup and delivery constraints
for request in requests:
pickup_index = manager.NodeToIndex(request['pickup'])
delivery_index = manager.NodeToIndex(request['delivery'])
# Pickup and delivery must be on same route
routing.solver().Add(
routing.VehicleVar(pickup_index) ==
routing.VehicleVar(delivery_index))
# Pickup must occur before delivery
routing.solver().Add(
time_dimension.CumulVar(pickup_index) <=
time_dimension.CumulVar(delivery_index))
# Add time windows
time_dimension.CumulVar(pickup_index).SetRange(
int(request['pickup_tw'][0]),
int(request['pickup_tw'][1]))
time_dimension.CumulVar(delivery_index).SetRange(
int(request['delivery_tw'][0]),
int(request['delivery_tw'][1]))
# Search parameters
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 = time_limit
# Solve
solution = routing.SolveWithParameters(search_parameters)
if solution:
routes = []
total_distance = 0
for vehicle_id in range(num_vehicles):
index = routing.Start(vehicle_id)
route = []
route_times = []
while not routing.IsEnd(index):
node = manager.IndexToNode(index)
time_var = time_dimension.CumulVar(index)
route.append(node)
route_times.append(solution.Value(time_var))
index = solution.Value(routing.NextVar(index))
route.append(manager.IndexToNode(index))
time_var = time_dimension.CumulVar(index)
route_times.append(solution.Value(time_var))
if len(route) > 2:
routes.append({
'vehicle_id': vehicle_id,
'route': route,
'times': route_times
})
# Calculate distance
route_distance = sum(dist_matrix[route[i]][route[i+1]]
for i in range(len(route)-1))
total_distance += route_distance
return {
'status': 'Optimal',
'routes': routes,
'total_distance': total_distance,
'num_vehicles': len(routes)
}
else:
return {
'status': 'No solution found',
'routes': None
}Complete example
Complete example
if name == "main":
np.random.seed(42)
random.seed(42)
# Generate PDP problem
n_requests = 10
depot_location = (50, 50)
# Generate pickup and delivery locations
pickup_locations = np.random.rand(n_requests, 2) * 100
delivery_locations = np.random.rand(n_requests, 2) * 100
# All locations (depot + pickups + deliveries)
locations = [depot_location]
locations.extend(pickup_locations.tolist())
locations.extend(delivery_locations.tolist())
# Create requests
requests = []
for i in range(n_requests):
pickup_idx = i + 1
delivery_idx = i + 1 + n_requests
# Generate time windows
pickup_early = random.randint(0, 200)
pickup_late = pickup_early + random.randint(60, 120)
delivery_early = pickup_late + 20
delivery_late = delivery_early + random.randint(60, 120)
requests.append({
'pickup': pickup_idx,
'delivery': delivery_idx,
'quantity': random.randint(10, 30),
'pickup_tw': (pickup_early, pickup_late),
'delivery_tw': (delivery_early, delivery_late)
})
vehicle_capacity = 100
num_vehicles = 4
print(f"Problem: {n_requests} requests, {num_vehicles} vehicles")
print(f"Capacity: {vehicle_capacity}")
print("\nSolving PDP with OR-Tools...")
result = solve_pdp_ortools(locations, requests, vehicle_capacity,
num_vehicles, time_limit=60)
if result['status'] == 'Optimal':
print(f"\nStatus: {result['status']}")
print(f"Total Distance: {result['total_distance']:.2f}")
print(f"Vehicles Used: {result['num_vehicles']}")
print("\nRoutes:")
for route_info in result['routes']:
route = route_info['route']
times = route_info['times']
vehicle_id = route_info['vehicle_id']
print(f"\n Vehicle {vehicle_id + 1}:")
print(f" Route: {route}")
# Identify pickups and deliveries
for i, (node, time) in enumerate(zip(route, times)):
if node == 0:
print(f" Stop {i}: Depot at time {time:.0f}")
elif node <= n_requests:
req_idx = node - 1
print(f" Stop {i}: Pickup {req_idx} at time {time:.0f}")
else:
req_idx = node - n_requests - 1
print(f" Stop {i}: Delivery {req_idx} at time {time:.0f}")
else:
print(f"Status: {result['status']}")
---if name == "main":
np.random.seed(42)
random.seed(42)
# Generate PDP problem
n_requests = 10
depot_location = (50, 50)
# Generate pickup and delivery locations
pickup_locations = np.random.rand(n_requests, 2) * 100
delivery_locations = np.random.rand(n_requests, 2) * 100
# All locations (depot + pickups + deliveries)
locations = [depot_location]
locations.extend(pickup_locations.tolist())
locations.extend(delivery_locations.tolist())
# Create requests
requests = []
for i in range(n_requests):
pickup_idx = i + 1
delivery_idx = i + 1 + n_requests
# Generate time windows
pickup_early = random.randint(0, 200)
pickup_late = pickup_early + random.randint(60, 120)
delivery_early = pickup_late + 20
delivery_late = delivery_early + random.randint(60, 120)
requests.append({
'pickup': pickup_idx,
'delivery': delivery_idx,
'quantity': random.randint(10, 30),
'pickup_tw': (pickup_early, pickup_late),
'delivery_tw': (delivery_early, delivery_late)
})
vehicle_capacity = 100
num_vehicles = 4
print(f"Problem: {n_requests} requests, {num_vehicles} vehicles")
print(f"Capacity: {vehicle_capacity}")
print("\nSolving PDP with OR-Tools...")
result = solve_pdp_ortools(locations, requests, vehicle_capacity,
num_vehicles, time_limit=60)
if result['status'] == 'Optimal':
print(f"\nStatus: {result['status']}")
print(f"Total Distance: {result['total_distance']:.2f}")
print(f"Vehicles Used: {result['num_vehicles']}")
print("\nRoutes:")
for route_info in result['routes']:
route = route_info['route']
times = route_info['times']
vehicle_id = route_info['vehicle_id']
print(f"\n Vehicle {vehicle_id + 1}:")
print(f" Route: {route}")
# Identify pickups and deliveries
for i, (node, time) in enumerate(zip(route, times)):
if node == 0:
print(f" Stop {i}: Depot at time {time:.0f}")
elif node <= n_requests:
req_idx = node - 1
print(f" Stop {i}: Pickup {req_idx} at time {time:.0f}")
else:
req_idx = node - n_requests - 1
print(f" Stop {i}: Delivery {req_idx} at time {time:.0f}")
else:
print(f"Status: {result['status']}")
---Tools & Libraries
工具与库
- OR-Tools (Google): Best for practical PDP (recommended)
- PuLP/Pyomo: MIP modeling
- jsprit: Java-based VRP solver with PDP support
- OR-Tools (Google):适用于实际PDP场景(推荐)
- PuLP/Pyomo:混合整数规划建模工具
- jsprit:基于Java的VRP求解器,支持PDP
Common Challenges & Solutions
常见挑战与解决方案
Challenge: Tight Time Windows
挑战:时间窗口紧张
Problem:
- Pickup and delivery time windows hard to satisfy
- Precedence + time windows creates difficulty
Solutions:
- Use time-oriented insertion criteria
- Allow some time window violations with penalties
- Increase fleet size
问题:
- 取货和送货的时间窗口难以满足
- 优先级+时间窗口增加了问题难度
解决方案:
- 使用面向时间的插入准则
- 允许一定的时间窗口违反并设置惩罚
- 增加车队规模
Challenge: Long Ride Times (Dial-a-Ride)
挑战:乘车时间过长(呼叫接送)
Problem:
- Passengers have maximum ride time limits
- Hard to serve many requests efficiently
Solutions:
- Add ride time constraints in formulation
- Use insertion heuristics with ride time checks
- Consider direct vs. shared rides
问题:
- 乘客有最长乘车时间限制
- 难以高效服务大量请求
解决方案:
- 在模型中添加乘车时间约束
- 使用包含乘车时间检查的插入启发式算法
- 考虑直达与拼车选项
Challenge: LIFO Constraints
挑战:后进先出(LIFO)约束
Problem:
- Last item picked up must be first delivered (truck loading)
- Restricts routing flexibility
Solutions:
- Track loading sequence explicitly
- Use specialized LIFO checking in feasibility
- May need to reject some request combinations
问题:
- 最后取的货物必须最先送达(卡车装载要求)
- 限制了路径规划的灵活性
解决方案:
- 明确跟踪装载顺序
- 在可行性检查中加入专门的LIFO验证
- 可能需要拒绝部分请求组合
Output Format
输出格式
PDP Solution Report
PDP解决方案报告
Problem:
- Requests: 25 pickup-delivery pairs
- Vehicles: 5 (capacity: 100 units)
Solution:
| Metric | Value |
|---|---|
| Total Distance | 892 km |
| Vehicles Used | 4 / 5 |
| Requests Served | 25 / 25 |
| On-time Pickups | 100% |
| On-time Deliveries | 100% |
Route Details:
Vehicle 1:
- Pickup 3 (8:15) → Pickup 7 (8:45) → Delivery 3 (9:20) → Delivery 7 (10:05) → Depot
问题:
- 请求:25组取送配对
- 车辆:5辆(容量:100单位)
解决方案:
| 指标 | 数值 |
|---|---|
| 总行驶距离 | 892 km |
| 使用车辆数 | 4 / 5 |
| 已服务请求数 | 25 / 25 |
| 准时取货率 | 100% |
| 准时送货率 | 100% |
路径详情:
车辆1:
- 取货3(8:15)→ 取货7(8:45)→ 送货3(9:20)→ 送货7(10:05)→ 仓库
Questions to Ask
需询问的问题
- Is each pickup paired with exactly one delivery?
- Must pickup occur before delivery on same vehicle?
- Are there time windows at pickups/deliveries?
- Is there max time between pickup and delivery?
- Are there LIFO (stacking) constraints?
- Is this dial-a-ride (passenger) or freight?
- 每个取货是否恰好对应一个送货?
- 取货必须在同一辆车的送货之前进行吗?
- 取货/送货地点是否有时间窗口?
- 取货和送货之间是否有最长时间限制?
- 是否存在后进先出(LIFO)约束?
- 这是呼叫接送(乘客)还是货运场景?
Related Skills
相关技能
- vehicle-routing-problem: For general VRP
- vrp-time-windows: For time window handling
- traveling-salesman-problem: For route sequencing
- vehicle-routing-problem:通用车辆路径问题
- vrp-time-windows:时间窗口处理
- traveling-salesman-problem:路径排序