pickup-delivery-problem

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Pickup 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:
  1. 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?
  2. 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)
  3. Capacity Considerations
    • Is capacity consumed from pickup to delivery?
    • LIFO (last-in-first-out) constraint?
    • Vehicle capacity during entire route?
  4. Temporal Constraints
    • Time windows at pickup locations?
    • Time windows at delivery locations?
    • Maximum delivery lag after pickup?
    • Service times at each location?
  5. Problem Scale
    • Small (< 20 requests): Exact methods possible
    • Medium (20-100 requests): Advanced heuristics
    • Large (100+ requests): Metaheuristics required

在解决PDP实例之前,需要了解以下内容:
  1. 问题变体
    • 一对一(每个取货对应一个送货)?
    • 多对多(多个取货/送货)?
    • 呼叫接送(乘客运输)?
    • 当日快递服务?
    • 搬家/搬迁服务?
  2. 配对约束
    • 强制配对(取货i必须在送货i之前)?
    • 取货和送货之间的时间窗口?
    • 最长乘车时间(呼叫接送)?
    • 取货/送货能否拆分到多辆车?(通常不行)
  3. 容量考虑
    • 从取货到送货期间会占用容量吗?
    • 后进先出(LIFO)约束?
    • 整个路径中的车辆容量?
  4. 时间约束
    • 取货地点的时间窗口?
    • 送货地点的时间窗口?
    • 取货后最长送货延迟时间?
    • 每个地点的服务时间?
  5. 问题规模
    • 小型(<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:
MetricValue
Total Distance892 km
Vehicles Used4 / 5
Requests Served25 / 25
On-time Pickups100%
On-time Deliveries100%
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

需询问的问题

  1. Is each pickup paired with exactly one delivery?
  2. Must pickup occur before delivery on same vehicle?
  3. Are there time windows at pickups/deliveries?
  4. Is there max time between pickup and delivery?
  5. Are there LIFO (stacking) constraints?
  6. Is this dial-a-ride (passenger) or freight?

  1. 每个取货是否恰好对应一个送货?
  2. 取货必须在同一辆车的送货之前进行吗?
  3. 取货/送货地点是否有时间窗口?
  4. 取货和送货之间是否有最长时间限制?
  5. 是否存在后进先出(LIFO)约束?
  6. 这是呼叫接送(乘客)还是货运场景?

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:路径排序