The Health Bureau of a city, in order to strengthen primary healthcare services, has equipped a mobile medical vehicle to regularly provide basic medical check-ups and medicines to various communities in the suburbs. To improve service efficiency, it is necessary to optimize the route of the mobile medical vehicle. The starting point of the medical vehicle is the Central City Hospital, located at coordinates (0, 0). The location information of the 10 communities to be served is as follows:

| COMMUNITY ID | (x, y)      |
|--------------|-------------|
| C1           | (3.5, 4.2)  |
| C2           | (-2.1, 5.6) |
| C3           | (4.8, -3.2) |
| C4           | (-3.6, -2.8)|
| C5           | (5.2, 2.4)  |
| C6           | (-4.1, 3.7) |
| C7           | (2.8, -4.5) |
| C8           | (6.3, -2.1) |
| C9           | (-5.2, -1.8)|
| C10          | (3.9, 5.8)  |

What is the shortest travel distance for the medical vehicle to start from the Central City Hospital, visit all communities, and return to the Central City Hospital? (Distance is calculated as the straight-line distance.)

Problem type: ILP (Medium: 110 variables, 22 constraints, 220 nonzeros)

Mathematical model:
Set:
1. Node
The set of nodes, \( N = \{0, 1, \dots, CommunityNum\} \)
# where node 0 is the starting point (Central City Hospital) and nodes 1 to n are the communities to be served.

Parameter:
1. CommunityNum
10
2. Coordinate
[(0, 0), (3.5, 4.2), (-2.1, 5.6), (4.8, -3.2), (-3.6, -2.8), (5.2, 2.4), (-4.1, 3.7), (2.8, -4.5), (6.3, -2.1), (-5.2, -1.8), (3.9, 5.8)]

Decision variable:
1. X
Binary variable, \( x_{ij} \forall i \in N, j \in N\), where \( x_{ij} = 1 \) if the vehicle departs from node \( i \) and arrives at node \( j \), otherwise \( x_{ij} = 0 \).

Objective:
1. Minimize the total distance traveled by the vehicle.
min: \sum_{i \in N}\sum_{j \in N} Distance[i][j] * x_{ij}
# Where \( Distance[i][j] \) is the Euclidean distance between node \( i \) and node \( j \).

Constraint:
1. Each community must be visited exactly once.
\sum_{i \in N} x_{ij} = 1 \forall j \in N
2. Each community must be exited exactly once.
\sum_{j \in N} x_{ij} = 1 \forall i \in N
3. Eliminate sub-tours.
\sum_{i \in N} \sum_{j \in N} x_{ij} \leq |S| - 1 \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_tsp(
    coordinates=[(0, 0), (3.5, 4.2), (-2.1, 5.6), (4.8, -3.2), (-3.6, -2.8),
                 (5.2, 2.4), (-4.1, 3.7), (2.8, -4.5), (6.3, -2.1),
                 (-5.2, -1.8), (3.9, 5.8)]):
    """
    Solves the Traveling Salesperson Problem (TSP) for community health services.
    """
    community_num = len(coordinates) - 1
    nodes = range(community_num + 1
                  )  # 0 is the central hospital, 1-10 are communities

    # Calculate distance matrix
    def calculate_distance(coord1, coord2):
        return math.sqrt((coord1[0] - coord2[0])**2 +
                         (coord1[1] - coord2[1])**2)

    distance = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                distance[i,
                         j] = calculate_distance(coordinates[i], coordinates[j])

    # Create a new model
    model = gp.Model("TSP")

    # Decision variables: x[i,j] = 1 if the path goes from i to j
    x = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                x[i, j] = model.addVar(vtype=GRB.BINARY, name=f'x_{i}_{j}')

    # Set objective: minimize total distance
    objective = gp.quicksum(distance[i, j] * x[i, j] for i in nodes
                            for j in nodes if i != j)
    model.setObjective(objective, GRB.MINIMIZE)

    # Constraint 1: Each node must be visited exactly once
    for j in nodes:
        model.addConstr(gp.quicksum(x[i, j] for i in nodes if i != j) == 1,
                        name=f'visit_to_{j}')

    # Constraint 2: Must leave each node exactly once
    for i in nodes:
        model.addConstr(gp.quicksum(x[i, j] for j in nodes if i != j) == 1,
                        name=f'leave_from_{i}')

    # Constraint 3: Subtour elimination using subset constraints
    def find_subtours(edges):
        # Create adjacency list
        adj = {i: [] for i in nodes}
        for i, j in edges:
            adj[i].append(j)

        # Find all subtours
        unvisited = set(nodes)
        subtours = []
        while unvisited:
            # Start from any unvisited node
            current = next(iter(unvisited))
            subtour = []
            while current in unvisited:
                unvisited.remove(current)
                subtour.append(current)
                # Move to next node in path
                next_nodes = adj.get(current, [])
                if not next_nodes:
                    break
                current = next_nodes[0]
            if len(subtour) < len(nodes):
                subtours.append(subtour)
        return subtours

    # Callback function for lazy constraints
    def subtour_cb(model, where):
        if where == GRB.Callback.MIPSOL:
            # Get values of decision variables
            x_vals = model.cbGetSolution(model._vars)
            edges = [(i, j) for i in nodes for j in nodes
                     if i != j and x_vals[i, j] > 0.5]

            # Find subtours
            subtours = find_subtours(edges)

            # Add subtour elimination constraints
            for S in subtours:
                if len(S) < len(nodes):
                    model.cbLazy(
                        gp.quicksum(x[i, j] for i in S for j in S
                                    if i != j) <= len(S) - 1)

    # Enable lazy constraints
    model._vars = x
    model.Params.lazyConstraints = 1

    # Optimize model with callback
    model.optimize(subtour_cb)

    # Return results
    if model.status == GRB.OPTIMAL:
        return {"status": "optimal", "obj": model.ObjVal}
    else:
        return {"status": f"{model.status}"}


# Run the solver function
if __name__ == "__main__":
    result = solve_tsp()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 39.12147411629252