使用gurobi解决旅行商问题(TSP)

TSP问题

旅行商问题(Traveling Salesman Problem)是最著名的组合优化问题之一。这个问题很容易解释,但解决起来非常复杂——即使是在少数城市的情况下也是如此。

问题描述

TSP 可以定义如下:对于给定的城市列表和每对城市之间的距离,我们希望找到到达每个城市一次并返回原点城市的最短路线。
有一类旅行商问题是这样的:假设从城市$i$到城市$j$的距离和城市$j$到城市$i$的距离是相等的,这种类型的旅行商问题也称为对称旅行商问题。在接下来的例子中我们使用欧几里得距离,模型是有效的,与确定距离的方式无关。

解决方法

数学编程是一种声明性方法,建模者制定数学优化模型,该模型捕获复杂决策问题的关键方面。
数学优化模型有五个组成部分,即:

  • 集合和索引。
  • 参数。
  • 决策变量。
  • 目标函数。
  • 约束。

TSP模型制定

集合和索引

$i,j\in Capitals$:索引以及美国首府城市的集合。
Pairings=$\{(i,j)\in Capitals\times Capitals\}$:可采取的组合集合。
$S\subset Capticals$:一个美国首府城市集合的子集。
$G=(Capitals,Pairings)$:一幅由集合Capticals定义的点、集合Pairings定义的边构成的图。

参数

$d_{i,j}\in \mathbb{R}^+$: 从城市$i$到城市$j$的距离,其中$(i,j)\in Pairings$。
注意:从城市$i$到城市$j$的距离和城市$j$到城市$i$的距离是相等的,也就是$d_{i,j}=d_{j,i}$,因此,这个TSP也被称为对称旅行推销员问题。

决策变量

$x_{i,j}\in\{0,1\}$:如果我们决定连接城市$i$与城市$j$,则此变量等于1,否则,决策变量等于零。

目标函数

  • 最短路线,最小化路线的总距离。路线是一系列首都城市,销售人员只访问每个城市一次,然后返回起始的首都城市。

    $$ \begin{align} \mathrm{Min}\quad Z=\sum\limits_{(i,j)\in\mathrm{Pairings}}d_{i,j}\cdot x_{i,j} \end{align} $$

约束

  • 对称约束。对于每条边$(i,j)$,确保首府$i$和$j$是相连接的,如果前者在访问后者之前或之后,需立即被访问(ps:这句话翻译的不好)。

$$ \begin{align} x_{i,j}=x_{j,i}\forall(i,j)\in Pairings \end{align} $$

  • 进出首都。对于每个首都$i$,确保该城市与其他两个城市相连.

    $$ \begin{align} \sum\limits_{(i,j)\in\mathrm{Pairings}}x_{i,j}=2\forall i\in Capitals \end{align} $$

  • 子回路的消除。这些约束确保了对于任何属于集合$Cpitals$的城市子集$S$,是没有循环的。也就是说,不存在访问子集中所有城市并返回原始城市的路线。

$$ \begin{align} \sum\limits_{(i\neq j)\in S}\boldsymbol{x}_{i,j}\leq|S|-1\forall\quad S\subset Capitals \end{align} $$

Python 实现

此建模示例需要导入以下库:

  • math 访问数学函数。
  • itertools 实现了许多迭代器构建块。
  • folium 创建地图。
  • gurobipy 调用Gurobi算法来求解MIP模型。

    读取和输入数据

    从json文件中读取大写名称和坐标。

    import json
    
    # Read capital names and coordinates from json file
    capitals_json = json.load(open('capitals.json'))
    capitals = []
    coordinates = {}
    for state in capitals_json:
        if state not in ['AK', 'HI']:
          capital = capitals_json[state]['capital']
          capitals.append(capital)
          coordinates[capital] = (float(capitals_json[state]['lat']), float(capitals_json[state]['long']))

    预处理

    以下函数计算每对州府组合的距离。

    import math
    from itertools import combinations,product
    
    # Compute pairwise distance matrix
    
    def distance(city1, city2):
        c1 = coordinates[city1]
        c2 = coordinates[city2]
        diff = (c1[0]-c2[0], c1[1]-c2[1])
        return math.sqrt(diff[0]*diff[0]+diff[1]*diff[1])
    
    dist = {(c1, c2): distance(c1, c2) for c1, c2 in product(capitals, capitals) if c1 != c2}

    回调定义

    以下函数确定延迟约束以消除子回路。

    # Callback - use lazy constraints to eliminate sub-tours
    
    def subtourelim(model, where):
        if where == GRB.Callback.MIPSOL:
            # make a list of edges selected in the solution
            vals = model.cbGetSolution(model._vars)
            selected = gp.tuplelist((i, j) for i, j in model._vars.keys()
                                 if vals[i, j] > 0.5)
            # find the shortest cycle in the selected edge list
            tour = subtour(selected)
            if len(tour) < len(capitals):
                # add subtour elimination constr. for every pair of cities in subtour
                model.cbLazy(gp.quicksum(model._vars[i, j] for i, j in combinations(tour, 2))
                             <= len(tour)-1)

    查找最短路线

    以下函数确定从给定的一组边开始的最短路径。

    # Given a tuplelist of edges, find the shortest subtour
    
    def subtour(edges):
        unvisited = capitals[:]
        cycle = capitals[:] # Dummy - guaranteed to be replaced
        while unvisited:  # true if list is non-empty
            thiscycle = []
            neighbors = unvisited
            while neighbors:
                current = neighbors[0]
                thiscycle.append(current)
                unvisited.remove(current)
                neighbors = [j for i, j in edges.select(current, '*')
                             if j in unvisited]
            if len(thiscycle) <= len(cycle):
                cycle = thiscycle # New shortest subtour
        return cycle

    模型部署

    我们现在通过定义决策变量、约束和目标函数来确定TSP的模型。接下来,我们开始优化,Gurobi找到TSP的最佳路径。

    import gurobipy as gp
    from gurobipy import GRB
    
    # tested with Python 3.7 & Gurobi 9.0.0
    
    m = gp.Model()
    
    # Variables: is city 'i' adjacent to city 'j' on the tour?
    
    vars = m.addVars(dist.keys(), obj=dist, vtype=GRB.BINARY, name='e')
    for i, j in vars.keys():
        m.addConstr(vars[j, i] == vars[i, j])  # edge in opposite direction
    
    # Constraints: two edges incident to each city
    
    m.addConstrs(vars.sum(c, '*') == 2 for c in capitals)
    
    # Optimize the model
    
    m._vars = vars
    m.Params.lazyConstraints = 1
    m.optimize(subtourelim)

    分析

    我们检索TSP的最优解,并验证最优路线(或旅游)到达所有城市并返回到原始城市。

    # Retrieve solution
    
    vals = m.getAttr('x', vars)
    selected = gp.tuplelist((i, j) for i, j in vals.keys() if vals[i, j] > 0.5)
    
    tour = subtour(selected)
    assert len(tour) == len(capitals)

    最佳路线显示在下图中。

    # Map the solution
    
    import folium
    
    map = folium.Map(location=[40,-95], zoom_start = 4)
    
    points = []
    for city in tour:
      points.append(coordinates[city])
    points.append(points[0])
    
    folium.PolyLine(points).add_to(map)
    
    map

    总结

    旅行商问题(TSP)是最流行的组合优化问题。这个问题很容易解释,尽管解决起来很复杂。解决的最大TSP问题有85900个城市。TSP是解决复杂组合优化问题的新方法的发现来源,并导致了许多应用。
    在这个建模示例中,我们展示了如何将对称旅行推销员问题公式化为MIP问题。我们还展示了如何通过使用惰性约束来动态消除子任务。

    参考文献

[1] D. L. Applegate, R. E. Bixby, V. Chvatal and W. J. Cook , The Traveling Salesman Problem: A Computational Study, Princeton University Press, Princeton, 2006.

[2] http://www.math.uwaterloo.ca/tsp/index.html

[3] https://www.youtube.com/watch?v=q8nQTNvCrjE&t=35s

[4]http://www.math.uwaterloo.ca/tsp/concorde.html

最后修改:2023 年 03 月 11 日
如果觉得我的文章对你有用,请随意赞赏