@
Traveling Salesman Problem(TSP),中文名叫旅行商问题,货郎担问题(前者更常见)。TSP的描述如下: 给定一系列的结点集合
如下图
图片来自:http://algorist.com/problems/Traveling_Salesman_Problem.html
上述例子中的节点可以广义化成一系列的区域,如下图。但是本质是一样的问题。
图片来自http://www.math.uwaterloo.ca/tsp/methods/opt/subtour.htm
该问题是计算机领域和应用数学领域一个非常经典和重要的问题。下面我们来从运筹学的角度,来详细了解一下TSP。
直观上来讲,我们可以将问题建模为:
上面的模型:
也就是说,上面的两个约束联合起来,可以保证,获得的解一定满足:每一个点都被访问一次,并且,经过一个点就会离开一个点。看上去貌似是没错的吧,直觉上是可以得到一条上图中展示的路径的,依次不重复经过所有节点的封闭的完美路径。但是不然。
也就是说,上述模型并不正确,会导致一个叫做子环路subtour的东东出现。如下面的简单解释 子环路(subtour):没有包含所有节点的一条闭环。子环路首先是一个封闭的环;其次,这个环中被访问的节点集合(假设为
图片来自http://www.math.uwaterloo.ca/tsp/methods/opt/subtour.htm
可以看到,子环路也完美的满足(i) 每个点只被访问一次,并且(ii) 经过一个点就离开那个点。但是这样的解会导致解中含有多个相离的环,也就是subtour。而我们需要的解是一个单个的经过所有点的大环。为了得到一个大环,我们就要添加消除子环路的约束,来完善TSP的模型。
这里比较常见的消除子环路的办法有两种:
当然,之前我还钻研过TSP的另外一种建模思路,就是用1-tree的定义,结合纯图论的理论来支持建模的,日后我再专门写一个1-tree建模+column generation求解的文章。
之前,我们的问题描述中提到,图中点的个数为
换句话说,对于点
也就是,只有上面这个条件成立,才会导致subtour的出现。 那么这个子环路不存在的条件就是(即破圈的方法)加入下面的约束
也就是说,我们只允许所有点都被包含进来的环存在,即包含点的个数为N的环,删除其余所有的环。那怎么做呢,一个简单的想法就是枚举,也就是我们在TSP中经常看到的约束:
可以看到
【小坑1】 上面的模型中,是假设了网络是全连接的情况,也就是任何两个点都是可以直接到达的,这也是为什么前两条约束加了
这个subtour-elimination的约束,是一个枚举的约束,我们不能在建模的时候就直接全枚举,这样的话有
那怎么办呢?业内一般采用Gurobi或者CPLEX求解器中提供的callback(回调函数)的方法来动态的添加subtour-elimination约束。总的来讲,就是在branch and bound tree迭代的过程中,根据当前结点的松弛后的线性规划模型(relaxed LP)的解,来检查该解是否有存在子环路 subtour,如果有,我们就把执行subtour-elimination时候产生的破圈约束加到正在求解的模型中去; 如果没有,我们就直接接着迭代算法。
当然这个check的过程和branch and bound tree的过程是并行的。具体实现在下面展示。这里由于篇幅原因和为了保证可读性。我们先把这小节结束了。
另外一种消除子环路的方法是加入Miller-Tucker-Zemlin(MTZ)约束。(本人认为这个方法的思想真的非常巧妙,做这个的时候就非常佩服前辈们的奇思妙想)。具体方法是:
这样就可以完美的解决子环路的问题。
也就是引入决策变量
【小思考1】
其中
理论上来讲,根据课本中的说法,
我们还是整理成逻辑约束的形式吧,上面的看着费劲
其中
这样,TSP问题的第二种最终版模型可以表示为
MTZ约束的加入,使得原问题增加了
并且,就我看过的论文来讲,大家还是用MTZ约束多一些。像TRB,TRC,TRE, TS, EJOR上的文章,很多都是用MTZ约束,当然他们也不会在论文中指出这些约束是MTZ约束,他们只是说这是消除子环路的,毕竟MTZ也是常识了。具体论文我不在这里举例了,之后再找时间贴过来。
接下来还是列几个小坑在这里,把前面说过的坑也一并再强调一下。
【小坑2】 注意,实现这个的时候需要注意,由于
我们将模型修正一下,变成点集为
请仔细琢磨上面约束1,约束2和约束3,不等式后面的comment部分的细微变化,这些都是为之后写代码的时候做基础,为了避免栽跟头。
接下来,我们解释一下为什么MTZ约束会work吧。
MTZ这个约束为什么能够消除子环路呢?
我们将MTZ约束、做一个变换,得到:
在上式中,
这个约束保证了,当
我们任取
也就是说,根据MTZ约束,如果上述情况成立,则必有:
将以上相加,我们得到
上面不等式显然不成立,这说明,这个子环路不可能出现,这也就用反证法证明了,任一满足MTZ的点集,都不存在环路。而注意,我们的约束后的comment是
其他情况,任意取
下面我们来讲一下,Python调用Gurobi,如何用callback实现subtour-elimination,以及如何实现MTZ版的TSP求解吧。
首先,我们还是以VRP上古大神solomon [^1]的benchmark为算例,来进行今天代码的数值实验部分。
算例下载地址: https://www.sintef.no/projectweb/top/vrptw/solomon-benchmark/100-customers/
在这之前,我想先说一下Gurobi和CPLEX里面的callback是怎么个逻辑:
下面我夹杂王者荣耀的角度来轻松解释callback是怎么起作用的,打农药的小伙伴应该秒懂。(有些地方不严谨,但是大概是这么个意思,这段本来就是辅助理解,不严谨的地方可以私信我,我再修改)
1.之前说过,subtour-elimination的想法,是想把所有的子集列举出来,为每一个子集添加破圈约束,但是这么做太慢。 2.于是我们想,先不加subtour-elimination的约束,我们先把只含有前两组约束的IP输入给Gurobi求解,Gurobi当然会先把IP的整数约束松弛掉,把模型变成LP,然后调用branch and bound算法,并将IP松弛后的relaxed LP作为根节点,进行branch and bound tree的迭代。 下面高能解释来了 我们把这个算法的迭代比作一场王者双排排位赛。假设我们准备开始玩游戏,我方打野选了野王Gurobi,OK,我见势立马一手奶妈蔡文姬,死跟打野,只干两件事1. 探视野(识别subtour)和2. 给助攻(根据subtour构建破圈约束)。Gurobi大佬构建模型,并且加入了前两组约束。(铭文带的不够呀,我有点慌,大佬却说,躺好看我carry, 帮我看蓝探视野),而我们也不示弱, 选了subtour-elimination的辅助装出装策略。(也就是subtour-elimination的callback函数,用于添加通过callback的方式添加subtour-elimination约束的) 3.游戏开始,Gurobi打着前两组约束,并且设置model.Params.lazyConstraints = 1也就是给我(软辅蔡文姬)发出跟着我的信号。然后拉着我一起双排开始了游戏,代码中就是model.optimize(subtourelim)。 OK,算法开始迭代。野王Gurobi还是基本操作,熟练的branch and bound在野区以最适合的刷野路径刷野。在刷野(迭代)的过程中,在每个branch and bound tree的结点处,Gurobi会去调用各种变式的simplex算法得到该节点的解。如果得到了一个整数解(也可以是得到小数解就操作),我们可以在这个地方,人为的插一脚,相当于Gurobi老哥正在屁颠屁颠刷野(跑算法)呢,我们在旁边探视野(做监工),一直就这么直勾的看着。我们看到老哥得到了一个整数解(或者一个小数可行解),一机灵,激动地过去拍拍Gurobi老哥的肩膀说,大佬,我拿一下这个节点的LP解哈,您继续。 4.OK, 拿到了LP的解以后,我们自己来看看这个解中存不存在subtour,如果我们检测完后发现不存在。尴尬,我们假装啥也没发生。继续静静地看着Gurobi老哥刷野(算法迭代)。下一次,Gurobi老哥又得到了一个整数解(或者一个小数可行解),我们再厚脸皮去拿过来检测,结果发现有2-5-8-2这个子环路混子混在里面,这次可被我逮个正着哈,小伙儿,你子环路了幺。我们激动地大声告诉Gurobi老哥:"老哥,老哥,子环路2-5-8-2在这儿挑衅你,你去GUNK它,把2-5-8-2这个混子送回家",顺便我们还根据这个子环路2-5-8-2的特点,快速给Gurobi老哥想出了一套连招:老哥,你1433223就可以秒他!!! 哈哈,在实际中,把这个子环路踢出去的方法(也就是刚刚的连招)就是加下面这个约束:
上面的描述并不完美,但是我想,应该能给你一些辅助理解callback工作逻辑的帮助。
其实总结一下,使用callback的方法分为下面几步(只针对本问题)
model._vars = X
model.Params.lazyConstraints = 1
model.optimize(subtourelim)
【Remark】这里,subtourelim(model, where)中,添加子环路消除约束是以lazyConstraints的形式添加的,lazyConstraints就是不在建模一开始就加入,而是在算法迭代的过程中,动态的在branch and bound的分支节点处才加入的约束。可以通过callback函数,控制在节点的解满足什么样的条件下,我们去构建特定形式的约束,这个约束以lazyConstraints的形式构建并添加到求解的函数model.optimize()中,然后Gurobi就可以自动的识别,且调用callback函数,按照你的要求在求解过程中把约束加进去。这一招branch and cut, benders decomposition, row generation的时候用的非常多。想要进阶的小伙伴这招儿还是必须要攻克的。
这部分代码有点长,待我明天再放出来把。算了,还是直接放上来把。
首先定义一些读取数据的函数:
# _*_coding:utf-8 _*_
'''
@author: Hsinglu Liu
@version: 1.0
@Date: 2019.5.5
'''
from __future__ import print_function
from __future__ import division, print_function
from gurobipy import *
import re;
import math;
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import copy
from matplotlib.lines import lineStyles
import time
starttime = time.time()
# function to read data from .txt files
def readData(path, nodeNum):
nodeNum = nodeNum;
cor_X = []
cor_Y = []
f = open(path, 'r');
lines = f.readlines();
count = 0;
# read the info
for line in lines:
count = count + 1;
if(count >= 10 and count <= 10 + nodeNum):
line = line[:-1]
str = re.split(r" +", line)
cor_X.append(float(str[2]))
cor_Y.append(float(str[3]))
# compute the distance matrix
disMatrix = [([0] * nodeNum) for p in range(nodeNum)]; # 初始化距离矩阵的维度,防止浅拷贝
# data.disMatrix = [[0] * nodeNum] * nodeNum]; 这个是浅拷贝,容易重复
for i in range(0, nodeNum):
for j in range(0, nodeNum):
temp = (cor_X[i] - cor_X[j])**2 + (cor_Y[i] - cor_Y[j])**2;
disMatrix[i][j] = (int)(math.sqrt(temp));
# disMatrix[i][j] = 0.1 * (int)(10 * math.sqrt(temp));
# if(i == j):
# data.disMatrix[i][j] = 0;
# print("%6.0f" % (math.sqrt(temp)), end = " ");
temp = 0;
return disMatrix;
def printData(disMatrix):
print("-------cost matrix-------n");
for i in range(len(disMatrix)):
for j in range(len(disMatrix)):
#print("%d %d" % (i, j));
print("%6.1f" % (disMatrix[i][j]), end = " ");
# print(disMatrix[i][j], end = " ");
print();
def reportMIP(model, Routes):
if model.status == GRB.OPTIMAL:
print("Best MIP Solution: ", model.objVal, "n")
var = model.getVars()
for i in range(model.numVars):
if(var[i].x > 0):
print(var[i].varName, " = ", var[i].x)
print("Optimal route:", Routes[i])
def getValue(var_dict, nodeNum):
x_value = np.zeros([nodeNum + 1, nodeNum + 1])
for key in var_dict.keys():
a = key[0]
b = key[1]
x_value[a][b] = var_dict[key].x
return x_value
def getRoute(x_value):
# 假如是5个点的算例,我们的路径会是1-4-2-3-5-6这样的,因为我们加入了一个虚拟点
# 也就是当路径长度为6的时候,我们就停止,这个长度和x_value的长度相同
x = copy.deepcopy(x_value)
# route_temp.append(0)
previousPoint = 0
arcs = []
route_temp = [previousPoint]
count = 0
while(len(route_temp) < len(x) and count < len(x)):
print('previousPoint: ', previousPoint, 'count: ', count)
if(x[previousPoint][count] > 0):
previousPoint = count
route_temp.append(previousPoint)
count = 0
continue
else:
count += 1
return route_temp
# cost = [[0, 7, 2, 1, 5],
# [7, 0, 3, 6, 8],
# [2, 3, 0, 4, 2],
# [1, 6, 4, 0, 9],
# [5, 8, 2, 9, 0]]
然后定义几个非常关键的用于添加subtour-elimination约束的函数:
其中,函数subtourelim(model, where)中,调用了函数computeDegree(graph)、findEdges(graph)和subtour(graph)。
# Callback - use lazy constraints to eliminate sub-tours
# 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
print('model._vars', model._vars)
# vals = model.cbGetSolution(model._vars)
x_value = np.zeros([nodeNum + 1, nodeNum + 1])
for m in model.getVars():
if(m.varName.startswith('x')):
# print(var[i].varName)
# print(var[i].varName.split('_'))
a = (int)(m.varName.split('_')[1])
b = (int)(m.varName.split('_')[2])
x_value[a][b] = model.cbGetSolution(m)
print("solution = ", x_value)
# print('key = ', model._vars.keys())
# selected = []
# for i in range(nodeNum):
# for j in range(nodeNum):
# if(i != j and x_value[i][j] > 0.5):
# selected.append((i, j))
# selected = tuplelist(selected)
# # selected = tuplelist((i,j) for i in range(nodeNum), for if x_value[i][j] > 0.5)
# print('selected = ', selected)
# find the shortest cycle in the selected edge list
tour = subtour(x_value)
print('tour = ', tour)
if(len(tour) < nodeNum + 1):
# add subtour elimination constraint for every pair of cities in tour
print("---add sub tour elimination constraint--")
# model.cbLazy(quicksum(model._vars[i][j]
# for i in tour
# for j in tour
# if i != j)
# <= len(tour)-1)
# LinExpr = quicksum(model._vars[i][j]
# for i in tour
# for j in tour
# if i != j)
for i,j in itertools.combinations(tour, 2):
print(i,j)
model.cbLazy(quicksum(model._vars[i, j]
for i,j in itertools.combinations(tour, 2))
<= len(tour)-1)
LinExpr = quicksum(model._vars[i, j]
for i,j in itertools.combinations(tour, 2))
print('LinExpr = ', LinExpr)
print('RHS = ', len(tour)-1)
# compute the degree of each node in given graph
def computeDegree(graph):
degree = np.zeros(len(graph))
for i in range(len(graph)):
for j in range(len(graph)):
if(graph[i][j] > 0.5):
degree[i] = degree[i] + 1
degree[j] = degree[j] + 1
print('degree', degree)
return degree
# given a graph, get the edges of this graph
def findEdges(graph):
edges = []
for i in range(1, len(graph)):
for j in range(1, len(graph)):
if(graph[i][j] > 0.5):
edges.append((i, j))
return edges
# Given a tuplelist of edges, find the shortest subtour
def subtour(graph):
# compute degree of each node
degree = computeDegree(graph)
unvisited = []
for i in range(1, len(degree)):
if(degree[i] >= 2):
unvisited.append(i)
cycle = range(0, nodeNum + 1) # initial length has 1 more city
edges = findEdges(graph)
edges = tuplelist(edges)
print(edges)
while unvisited: # true if list is non-empty
thiscycle = []
neighbors = unvisited
while neighbors: # true if neighbors is non-empty
current = neighbors[0]
thiscycle.append(current)
unvisited.remove(current)
neighbors = [j for i,j in edges.select(current,'*') if j in unvisited]
neighbors2 = [i for i,j in edges.select('*',current) if i in unvisited]
if(neighbors2):
neighbors.extend(neighbors2)
# print('current:', current, 'n neighbors', neighbors)
isLink = ((thiscycle[0], thiscycle[-1]) in edges) or ((thiscycle[-1], thiscycle[0]) in edges)
if(len(cycle) > len(thiscycle) and len(thiscycle) >= 3 and isLink):
# print('in = ', ((thiscycle[0], thiscycle[-1]) in edges) or ((thiscycle[-1], thiscycle[0]) in edges))
cycle = thiscycle
return cycle
return cycle
然后是建模部分的代码,建模部分相比学运筹的人比较熟悉,这里比较特殊的就是求解时候的几行代码:
# nodeNum = 5
nodeNum = 10
# # path = 'C:UsershsingluLiueclipse-workspacePythonCallGurobi_ApplicationsVRPTWR101.txt';
path = 'solomon-100/in/c201.txt';
cost = readData(path, nodeNum)
printData(cost)
model = Model('TSP')
# creat decision variables
X = {}
mu = {}
for i in range(nodeNum + 1):
mu[i] = model.addVar(lb = 0.0
, ub = 100 #GRB.INFINITY
# , obj = distance_initial
, vtype = GRB.CONTINUOUS
, name = "mu_" + str(i)
)
for j in range(nodeNum + 1):
if(i != j):
X[i, j] = model.addVar(vtype = GRB.BINARY
, name = 'x_' + str(i) + '_' + str(j)
)
# set objective function
obj = LinExpr(0)
for key in X.keys():
i = key[0]
j = key[1]
if(i < nodeNum and j < nodeNum):
obj.addTerms(cost[key[0]][key[1]], X[key])
elif(i == nodeNum):
obj.addTerms(cost[0][key[1]], X[key])
elif(j == nodeNum):
obj.addTerms(cost[key[0]][0], X[key])
model.setObjective(obj, GRB.MINIMIZE)
# add constraints 1
for j in range(1, nodeNum + 1):
lhs = LinExpr(0)
for i in range(0, nodeNum):
if(i != j):
lhs.addTerms(1, X[i, j])
model.addConstr(lhs == 1, name = 'visit_' + str(j))
# add constraints 2
for i in range(0, nodeNum):
lhs = LinExpr(0)
for j in range(1, nodeNum + 1):
if(i != j):
lhs.addTerms(1, X[i, j])
model.addConstr(lhs == 1, name = 'visit_' + str(j))
# model.addConstr(X[0, nodeNum] == 0, name = 'visit_' + str(0) + ',' + str(nodeNum))
# set lazy constraints
model._vars = X
model.Params.lazyConstraints = 1
model.optimize(subtourelim)
# subProblem.optimize()
x_value = getValue(X, nodeNum)
# route = getRoute(x_value)
# print('optimal route:', route)
搞定。再重复一下,关键的地方就是subtourelim()这个函数和subtour(graph)这两个关键函数。还有就是求解的时候,别忘了model.optimize(subtourelim).就可以了。
Ok,我们将solomon100个点的VRP算例中的c201.txt拿出来,取钱10个点运行一下,结果为:
obj : 127
optimal route: [0, 4, 3, 7, 1, 2, 5, 8, 9, 6, 0]
完美!是不是觉得世界都又好了。哈哈,若干年前,我就是这种感觉。
首先定义一些读取数据的函数什么的,同上。
# _*_coding:utf-8 _*_
'''
@author: Hsinglu Liu
@version: 1.0
@Date: 2019.5.5
'''
# _*_coding:utf-8 _*_
'''
@author: Hsinglu Liu
@version: 1.0
@Date: 2019.5.5
'''
from __future__ import print_function
from __future__ import division, print_function
from gurobipy import *
import re;
import math;
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import copy
from matplotlib.lines import lineStyles
import time
starttime = time.time()
# function to read data from .txt files
def readData(path, nodeNum):
nodeNum = nodeNum;
cor_X = []
cor_Y = []
f = open(path, 'r');
lines = f.readlines();
count = 0;
# read the info
for line in lines:
count = count + 1;
if(count >= 10 and count <= 10 + nodeNum):
line = line[:-1]
str = re.split(r" +", line)
cor_X.append(float(str[2]))
cor_Y.append(float(str[3]))
# compute the distance matrix
disMatrix = [([0] * nodeNum) for p in range(nodeNum)]; # 初始化距离矩阵的维度,防止浅拷贝
# data.disMatrix = [[0] * nodeNum] * nodeNum]; 这个是浅拷贝,容易重复
for i in range(0, nodeNum):
for j in range(0, nodeNum):
temp = (cor_X[i] - cor_X[j])**2 + (cor_Y[i] - cor_Y[j])**2;
disMatrix[i][j] = (int)(math.sqrt(temp));
# disMatrix[i][j] = 0.1 * (int)(10 * math.sqrt(temp));
# if(i == j):
# data.disMatrix[i][j] = 0;
# print("%6.0f" % (math.sqrt(temp)), end = " ");
temp = 0;
return disMatrix;
def printData(disMatrix):
print("-------cost matrix-------n");
for i in range(len(disMatrix)):
for j in range(len(disMatrix)):
#print("%d %d" % (i, j));
print("%6.1f" % (disMatrix[i][j]), end = " ");
# print(disMatrix[i][j], end = " ");
print();
def reportMIP(model, Routes):
if model.status == GRB.OPTIMAL:
print("Best MIP Solution: ", model.objVal, "n")
var = model.getVars()
for i in range(model.numVars):
if(var[i].x > 0):
print(var[i].varName, " = ", var[i].x)
print("Optimal route:", Routes[i])
def getValue(var_dict, nodeNum):
x_value = np.zeros([nodeNum + 1, nodeNum + 1])
for key in var_dict.keys():
a = key[0]
b = key[1]
x_value[a][b] = var_dict[key].x
return x_value
def getRoute(x_value):
'''
input: x_value的矩阵
output:一条路径,[0, 4, 3, 7, 1, 2, 5, 8, 9, 6, 0],像这样
'''
# 假如是5个点的算例,我们的路径会是1-4-2-3-5-6这样的,因为我们加入了一个虚拟点
# 也就是当路径长度为6的时候,我们就停止,这个长度和x_value的长度相同
x = copy.deepcopy(x_value)
# route_temp.append(0)
previousPoint = 0
route_temp = [previousPoint]
count = 0
while(len(route_temp) < len(x)):
#print('previousPoint: ', previousPoint )
if(x[previousPoint][count] > 0):
previousPoint = count
route_temp.append(previousPoint)
count = 0
continue
else:
count += 1
return route_temp
'''
# toy example
cost = [[0, 7, 2, 1, 5],
[7, 0, 3, 6, 8],
[2, 3, 0, 4, 2],
[1, 6, 4, 0, 9],
[5, 8, 2, 9, 0]]
'''
然后就是Python调用Gurobi求解TSP的代码了(MTZ约束消除子环路)。MTZ的实现还是比较简单的。
# nodeNum = 5
nodeNum = 10
# # path = 'C:UsershsingluLiueclipse-workspacePythonCallGurobi_ApplicationsVRPTWR101.txt';
path = 'solomon-100/in/c201.txt';
cost = readData(path, nodeNum)
printData(cost)
model = Model('TSP')
# creat decision variables
X = {}
mu = {}
for i in range(nodeNum + 1):
mu[i] = model.addVar(lb = 0.0
, ub = 100 #GRB.INFINITY
# , obj = distance_initial
, vtype = GRB.CONTINUOUS
, name = "mu_" + str(i)
)
for j in range(nodeNum + 1):
if(i != j):
X[i, j] = model.addVar(vtype = GRB.BINARY
, name = 'x_' + str(i) + '_' + str(j)
)
# set objective function
obj = LinExpr(0)
for key in X.keys():
i = key[0]
j = key[1]
if(i < nodeNum and j < nodeNum):
obj.addTerms(cost[key[0]][key[1]], X[key])
elif(i == nodeNum):
obj.addTerms(cost[0][key[1]], X[key])
elif(j == nodeNum):
obj.addTerms(cost[key[0]][0], X[key])
model.setObjective(obj, GRB.MINIMIZE)
# add constraints 1
for j in range(1, nodeNum + 1):
lhs = LinExpr(0)
for i in range(0, nodeNum):
if(i != j):
lhs.addTerms(1, X[i, j])
model.addConstr(lhs == 1, name = 'visit_' + str(j))
# add constraints 2
for i in range(0, nodeNum):
lhs = LinExpr(0)
for j in range(1, nodeNum + 1):
if(i != j):
lhs.addTerms(1, X[i, j])
model.addConstr(lhs == 1, name = 'visit_' + str(j))
# add MTZ constraints
# for key in X.keys():
# org = key[0]
# des = key[1]
# if(org != 0 or des != 0):
# # pass
# model.addConstr(mu[org] - mu[des] + 100 * X[key] <= 100 - 1)
for i in range(0, nodeNum):
for j in range(1, nodeNum + 1):
if(i != j):
model.addConstr(mu[i] - mu[j] + 100 * X[i, j] <= 100 - 1)
model.write('model.lp')
model.optimize()
x_value = getValue(X, nodeNum)
route = getRoute(x_value)
print('optimal route:', route)
设置10个点跑一个toy example试试,结果为
Explored 683 nodes (4011 simplex iterations) in 0.19 seconds
Thread count was 8 (of 8 available processors)
Solution count 5: 127 137 150 ... 230
Optimal solution found (tolerance 1.00e-04)
Best objective 1.270000000000e+02, best bound 1.270000000000e+02, gap 0.0000%
optimal route: [0, 4, 3, 7, 1, 2, 5, 8, 9, 6, 10]
由于我们把起始点copy了一下,因此最优解为
obj : 127
optimal route: [0, 4, 3, 7, 1, 2, 5, 8, 9, 6, 0]
运筹修炼真是个非常磨人的事情,需要理论与实战结合才能理解更深入。理论已经门槛够高了,再加上编程实现,可真要了命了。另外有非常多的小细节,大佬们在论文中并不会讲,但是又非常关键,只有自己实际一个一个去踩坑,或者多请教前辈,毕竟行万里路不如高人指路。
这里我把我的笔记和心得放在这里,供大家参考,互相交流学习,进步。也是为我自己整理、复习一下之前的知识。以后我自己复习的时候回来看也非常方便。
国内运筹学科普好文还是不太多见,很多都是从1到100的文章。分析讲述一些论文的idea什么的,都是为基础非常好的优秀者们看的。详细的讲述从0到1,如何把基础的东西吃透的文章比较少,让我们这些还不够强的孩子着实举步维艰,听讲座听得懂idea,但是做起来却啥啥也不行。希望以后有干货的,实用的文章越来越多,帮助众多学子解决基本的,底层的疑惑文章。哎,感觉国内OR界博士生们对branch and cut, branch and price, branch and cut and price, benders, DW decomposition等都理解比较深,貌似国内OR人均水平为随手实现上述一系列精确算法如探囊取物。我还是继续好好迎头赶上,抓紧修炼自己把。希望以后能够真正能够慢慢提高理论和实战水准,荷枪实弹学到真技术,为做有质量的学术打好基础。同时,也希望国内运筹发展越来越好吧。
[1]: Desrochers, M., Desrosiers, J., & Solomon, M. (1992). A new optimization algorithm for the vehicle routing problem with time windows. Operations research, 40(2), 342-354. https://doi.org/10.1287/opre.40.2.342 [2]:Gurobi documents https://www.gurobi.com/documentation/ [3]:Winston, W. L., & Goldberg, J. B. (2004). Operations research: applications and algorithms (Vol. 3). Belmont^ eCalif Calif: Thomson/Brooks/Cole. [4]:Desaulniers, G., Desrosiers, J., & Solomon, M. M. (Eds.). (2006). Column generation (Vol. 5). Springer Science & Business Media.