OR-Tools VRP 问题从入门到升天(一) TSP问题

OR-Tools VRP 问题从入门到升天(一) TSP问题

Ortools的VRP求解器简介

谷歌的Ortools整合了许多对运筹优化问题的求解器,其中最好用的部分就是VRP求解器。

在ortools中,VRP求解器是建立在constraint programming求解器之上的,因此除了一些经典的VRP问题约束,例如最大负载,时间窗以外,还可以通过约束规划求解器,灵活地添加一系列高度定制化的约束,例如要求某辆车经过某个节点,要求一个节点被多台车访问等。

在ortools中求解VRP问题时,最常用的有两类变量,我们可以从抽象层面了解一下其含义:

  1. 路径变量(Path variable):

    • next(i) - 代表节点 i 在路径中的后继节点的下标。用IndexToNode()可以从下表获取节点变量。

    • vehicle(i) - 表示节点 i 所属于的车辆路径编号,即第 i 个节点由哪辆车来服务。

    • active(i) - 布尔变量,如果节点 i 被访问,那么值为True,否则为False。

    • 下面的几组关系是等价的:active(i) == 0 <==> next(i) == i <==> vehicle(i) == -1next(i) == j ==> vehicle(j) == vehicle(i)

  2. 维度变量(Dimension variable) - 用于标记经过路径时累加的值,例如车辆的负载、消耗时间、行驶距离等。常用的有两类:

    • cumul(i, d) - 代表车辆到达节点 i 时维度 d 的值

    • transit(i, d) - 代表车辆经过节点 i 时,需要在维度 d 上累加的值

    • 这两者的关系为: next(i)==j ==> cumul(j,d)=cumul(i,d)+transit(i,d)

因此,从抽象层面上来讲,每台车从depot出发,不断进行next(i)操作,最终回到depot,即可求得该车辆的路径。每次next(i)操作的同时,我们也通过transit(i,d)不断累加我们关注的值,例如车辆的总行驶距离、消耗时间等。这就是ortools对VRP问题的抽象概括。

数据

这里我们使用的测试数据来自TSPlib中的gr120,其中包含120个节点。

数据的读入使用python中的tsplib95包来完成,节省自己编写数据读入部分的时间。

所有节点的可视化如下:

gr120.tsp_nodes.png

整体代码

我们先看一下整体的代码,了解调用ortools求解VRP的基本框架,然后再在后面去对各个重要的类进行详细分析:

## 包的导入
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import tsplib95
import matplotlib.pyplot as plt
%matplotlib inline
 
## 设定数据目录并读入数据
fname: str = "gr120.tsp"
input_fpath: str = r"data/" + fname
data = tsplib95.load(input_fpath).as_name_dict()

## 建立模型
# 创建routing index manager
n_vehicle = 1 # 车辆数,在TSP问题中为1
depot_index = 0 # 车库,设置为0号节点
manager = pywrapcp.RoutingIndexManager(data['dimension'], n_vehicle, depot_index)
# 创建一个Routing model
routing = pywrapcp.RoutingModel(manager)
# 创建一个distance call back,这里使用欧几里得距离
def distance_callback(from_index: int, to_index: int):
    from_node = manager.IndexToNode(from_index)
    to_node = manager.IndexToNode(to_index)
    # +1是因为读入数据时节点下标从1开始
    from_coor = data['display_data'][from_node+1]
    to_coor = data['display_data'][to_node+1]
    return (to_coor[0] - from_coor[0])**2 + (to_coor[1] - from_coor[1])**2
 # 因为我们需要在车辆经过节点i是为其在距离维度d上累加一定的值,所以这里我们使用transitCallBack; 之后我们会看到在设置时间窗口时,会用到cumul类的维度变量
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
# 给每条边设置cost
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
# 设置求解的启发式算法
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
# 求解
solution = routing.SolveWithParameters(search_parameters)

## 输出结果:
if solution:
    print("路径总长度: {} miles".format(solution.ObjectiveValue()))
    index = routing.Start(0) # routing.Start(0)代表获取0号车辆的起点
    path: str = "旅行商的路径:\n"
    visiting_sequence: list = []
    while not routing.IsEnd(index):
        visiting_sequence.append(manager.IndexToNode(index)+1)
        path += " {} ->".format(manager.IndexToNode(index)+1)
        previous_index = index
        index = solution.Value(routing.NextVar(index))
    visiting_sequence.append(manager.IndexToNode(index)+1)
    path += " {}\n".format(manager.IndexToNode(index)+1)
    print(path)
else:
    print("没有得到可行解")

得到结果:

路径总长度: 28422 miles
旅行商的路径:
 1 -> 76 -> 59 -> 15 -> 30 -> 29 -> 120 -> 32 -> 92 -> 28 -> 45 -> 78 -> 86 -> 94 -> 81 -> 22 -> 66 -> 31 -> 117 -> 85 -> 18 -> 19 -> 25 -> 108 -> 43 -> 79 -> 52 -> 33 -> 100 -> 58 -> 91 -> 68 -> 65 -> 69 -> 113 -> 107 -> 20 -> 46 -> 50 -> 98 -> 17 -> 118 -> 13 -> 49 -> 42 -> 41 -> 56 -> 44 -> 75 -> 14 -> 87 -> 74 -> 105 -> 40 -> 72 -> 7 -> 38 -> 4 -> 119 -> 103 -> 9 -> 23 -> 51 -> 11 -> 115 -> 21 -> 93 -> 2 -> 82 -> 3 -> 80 -> 27 -> 53 -> 64 -> 109 -> 88 -> 97 -> 12 -> 95 -> 77 -> 5 -> 63 -> 73 -> 57 -> 39 -> 83 -> 67 -> 37 -> 62 -> 99 -> 10 -> 35 -> 104 -> 106 -> 114 -> 101 -> 102 -> 48 -> 110 -> 112 -> 36 -> 84 -> 6 -> 89 -> 55 -> 47 -> 71 -> 26 -> 34 -> 116 -> 70 -> 8 -> 54 -> 90 -> 96 -> 111 -> 24 -> 60 -> 16 -> 61 -> 1

我们还可以对路径进行可视化:

plt.figure(figsize=(8, 6))
# 画出depot
depot_coor = data['display_data'][depot_index + 1]
plt.plot(depot_coor[0], depot_coor[1], 'r*', markersize=11)
# 路径可视化
for i, j in zip(visiting_sequence, visiting_sequence[1:]):
 start_coor = data['display_data'][i]
 end_coor = data['display_data'][j]
 plt.arrow(start_coor[0], start_coor[1], end_coor[0] - start_coor[0], end_coor[1] - start_coor[1])
plt.xlabel("X coordinate", fontsize = 14)
plt.ylabel("Y coordinate", fontsize = 14)
plt.title("TSP path for {}".format(fname), fontsize = 16)

结果如下图(红色星星代表depot):

Path_for_gr120.tsp.png

相关的类

RoutingIndexManager

RoutingIndexManager类是用来管理 NodeIndex <-> variable index之间的映射的。它之所以存在是因为在VRP问题中,节点的编号和变量编号并非是一一对应的,例如我们需要让一个节点被多个车辆访问时,这个节点会被分拆成多个变量,从而产生了单个节点编号和多个变量编号的对应。

它有两类初始化方法:

  • 当所有车辆具有相同起点和终点时,用RoutingIndexManager(num_nodes: int, num_vehicles: int, depot: NodeIndex) - 其中num_nodes表示问题中的节点数量,num_vehicles表示车辆数,depot表示所有车辆的起点和重点

  • 当每个车辆的起终点都需要单独设定时,用RoutingIndexManager(num_nodes: int, num_vehicles: int, starts: List[NodeIndex], ends: List[NodeIndex]) 或者 RoutingIndexManager(num_nodes: int, num_vehicles: int, starts_ends: List[Tuple[NodeIndex, NodeIndex]] - 其中starts数组给出每辆车的起点下标,ends数组给出每辆车的终点下标,starts_ends以成对的方式给出每辆车的起点和终点。

RoutingModel

在ortools中,这个类的作用相对混杂:

  • 在其中可以对模型的约束进行修改,例如使用SetPickUpAndDeliveryPolicyOfAllVehicles可以设置pick up & delivery过程中,车辆是FIFO还是FILO。

  • 可以对求解算法进行设置,如利用SolveWithParameters(search_parameters: RoutingSearchParameters),可以设置求解时的参数。甚至也可以通过一些方法设置求解时用的邻域搜索方法等。

  • 在求解后,其中也保存了解的信息,例如我们可以通过End(vehicle_id:int)获得某辆车的终点`。

它有两种初始化方法:

  • 采用默认的模型参数时:RoutingModel(manager: RoutingIndexManager),直接传入一个IndexManager类即可完成初始化;

  • 采用特定的模型参数时,可以显式的传入参数:RoutingModel(manager: RoutingIndexManager, paramters: RoutingModelParameters)

由于它包含的内容实在太多,具体的一些方法遇到的时候我们再行详解。

Assignment

Assignment类中包含了变量->域的映射,用来保存呈现给用户的结果。在上面的例子中,我们求解之后获得的solution就是一个Assignment类实例。

这个类中我们比较常用的方法有:

  • Value(var) - 返回变量var的值;

  • ObjectiveValue() - 返回当前解的目标函数值

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容