A logistics company needs to deliver packages to multiple booked customer, the logistics center's coordinates are (0, 0), and the company has 5 trucks with a capacity of 15 units of packages available for delivery. It is known that there are 12 customer points that need to be delivered, with coordinates (4.36, -2.75), (-7.29, -3.22), (-8.74, -2.47), (-0.68, -5.15), (-0.39, -3.49), (-6.06, -5.52), (-2.67, 9.32), (9.91, 7.87), (2.03, -2.98), (2.33, -1.47), (5.1, -2.08), (-8.04, -9.78), and the demands are 6, 7, 1, 10, 8, 7, 3, 6, 5, 2, 4, 4 units of packages respectively. Trucks start from the logistics center, complete the delivery on the planned route, and then return to the logistics center. The number of trucks used should be less than or equal to the number of trucks owned by the company. Under the condition of meeting all customer demands, what is the shortest total driving distance for the trucks? (Assuming the distance between each point is calculated based on the straight-line distance.)

Problem type: ILP (Medium: 780 variables, 87 constraints, 3060 nonzeros)

Mathematical model:
Set:
1. Customer
The set of customers, \( C = \{1, 2, \dots, CustomerNum\} \)
2. Node
The set of nodes, \( N = C \cup \{0\} \)
# 0 is the logistics center
3. Truck
The set of trucks, \( K = \{1, 2, \dots, TruckNum\} \)

Parameter:
1. CustomerNum
12
2. NodeCoordinates
# Coordinates of the center(0) and customers
[(0, 0), (4.36, -2.75), (-7.29, -3.22), (-8.74, -2.47), (-0.68, -5.15), (-0.39, -3.49), (-6.06, -5.52), (-2.67, 9.32), (9.91, 7.87), (2.03, -2.98), (2.33, -1.47), (5.1, -2.08), (-8.04, -9.78)]
3. CustomerDemand
[6, 7, 1, 10, 8, 7, 3, 6, 5, 2, 4, 4]
4. TruckNum
5
5. TruckCapacity
15

Decision variable:
1. X
Binary variable, \( x_{ijk} \forall i, j \in N, i \neq j \forall k \in K\), where \( x_{ijk} = 1 \) if truck \( k \) travels from node \( i \) to node \( j \), otherwise \( x_{ijk} = 0 \).

Objective:
The minimum total distance of the truck routes.
min: \sum_{k \in K}\sum_{i \in N} \sum_{j \in N} Distance[i][j] * x_{ijk}
# Where \( Distance[i][j] \) is the Euclidean distance between node \( i \) and node \( j \).

Constraint:
1. Each customer must be visited exactly once.
\sum_{i \in N}\sum_{k \in K} x_{ijk} = 1 \forall j \in C
2. Truck leaves customer that it enters.
\sum_{j \in N} x_{ijk} - \sum_{j \in N} x_{jik} = 0 \forall i \in C \forall k \in K
3. Each truck leaves the logistics center less than or equal to once.
\sum_{j \in N} x_{0jk} <= 1 \forall k \in K
4. The truck that leaves the logistics center must return to the logistics center.
\sum_{i \in N} x_{i0k} = \sum_{j \in N} x_{0jk} \forall k \in K
4. Capacity constraint for each truck.
\sum_{i \in C}\sum_{j \in C} CustomerDemand[i] * x_{ijk} <= TruckCapacity \forall k \in K
5. Eliminate sub-tours.
\sum_{i \in S} \sum_{j \in S} x_{ijk} <= |S| - 1 \forall k \in K, \forall S \subseteq N, 1 < |S| < |N|

Type:
Binary, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB
import math

def solve_vehicle_routing(
    customer_num=12,
    truck_num=5,
    truck_capacity=15,
    node_coordinates=[
        (0, 0), (4.36, -2.75), (-7.29, -3.22), (-8.74, -2.47),
        (-0.68, -5.15), (-0.39, -3.49), (-6.06, -5.52),
        (-2.67, 9.32), (9.91, 7.87), (2.03, -2.98), (2.33, -1.47),
        (5.1, -2.08), (-8.04, -9.78)
    ],
    customer_demand=[6, 7, 1, 10, 8, 7, 3, 6, 5, 2, 4, 4]
):
    """
    Solves the vehicle routing problem.
    """
    model = gp.Model("VehicleRoutingProblem")

    center = 0
    customers = range(1, customer_num + 1)
    nodes = [center] + list(customers)
    trucks = range(1, truck_num + 1)

    distance = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                x1, y1 = node_coordinates[i]
                x2, y2 = node_coordinates[j]
                distance[i, j] = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

    x = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                for k in trucks:
                    x[i, j, k] = model.addVar(vtype=GRB.BINARY, name=f"x_{i}_{j}_{k}")

    objective = gp.quicksum(distance[i, j] * x[i, j, k] for i in nodes for j in nodes for k in trucks if i != j)
    model.setObjective(objective, GRB.MINIMIZE)

    for j in customers:
        model.addConstr(gp.quicksum(x[i, j, k] for i in nodes if i != j for k in trucks) == 1, name=f"customer_visited_{j}")

    for i in customers:
        for k in trucks:
            model.addConstr(gp.quicksum(x[i, j, k] for j in nodes if i != j) - gp.quicksum(x[j, i, k] for j in nodes if i != j) == 0, name=f"flow_conservation_{i}_{k}")

    for k in trucks:
        model.addConstr(gp.quicksum(x[center, j, k] for j in customers) <= 1, name=f"truck_{k}_leaves_center")
        model.addConstr(gp.quicksum(x[center, j, k] for j in customers) == gp.quicksum(x[j, center, k] for j in customers), name=f"truck_{k}_returns_center")

    for k in trucks:
        model.addConstr(gp.quicksum(customer_demand[j - 1] * gp.quicksum(x[i, j, k] for i in nodes if i != j) for j in customers) <= truck_capacity, name=f"truck_{k}_capacity")

    def find_subtours(x_vals, k):
        edges = [(i, j) for i in customers for j in customers if i != j and x_vals.get((i, j, k), 0) > 0.5]
        if not edges:
            return []
        
        neighbors = {i: [] for i in customers}
        for i, j in edges:
            neighbors[i].append(j)
        
        unvisited = set(customers)
        subtours = []
        while unvisited:
            curr = next(iter(unvisited))
            tour = [curr]
            unvisited.remove(curr)
            
            while True:
                neighbors_of_curr = [j for j in neighbors.get(curr, []) if j in unvisited]
                if not neighbors_of_curr:
                    break
                curr = neighbors_of_curr[0]
                tour.append(curr)
                unvisited.remove(curr)
            
            is_valid_tour = any(x_vals.get((center, i, k), 0) > 0.5 for i in tour) and any(x_vals.get((i, center, k), 0) > 0.5 for i in tour)
            if not is_valid_tour and tour:
                subtours.append(tour)
        return subtours

    def subtour_elimination_callback(model, where):
        if where == GRB.Callback.MIPSOL:
            x_vals = model.cbGetSolution(x)
            for k in trucks:
                subtours = find_subtours(x_vals, k)
                for subtour in subtours:
                    if len(subtour) < customer_num:
                        model.cbLazy(gp.quicksum(x[i, j, k] for i in subtour for j in subtour if i != j) <= len(subtour) - 1)

    model.Params.lazyConstraints = 1
    model.optimize(subtour_elimination_callback)

    if model.status == GRB.OPTIMAL:
        return {"status": "optimal", "obj": model.objVal}
    else:
        return {"status": f"{model.status}"}

if __name__ == "__main__":
    result = solve_vehicle_routing()
    print(result)



Solution status: OPTIMAL
Optimal objective value: 104.33525648686994