or-tools路线问题优化方案和实践(蚁群算法、遗传算法、模拟退火算法)
路径问题介绍
有时我们会遇到寻找最佳路线的问题,这里的“最佳”是指权重最小,可以是总距离或者费用等。比如:
- 一家包裹配送服务公司想要为司机指定送货路线。
- 一家有线电视公司希望为技术人员分配用于拨打住宅服务电话的路由。
- 一家拼车公司想要为司机分配上车和下车路线。
其中最有名的要数旅行商问题TSP(traveling salesman problem)。从仓库出发,经过各个要去的城市,最终返回仓库。例如下图:
通过计算我们可以确定ACDBA是一条最短的路线。 那么总共有多少种路线呢?这是一个排列问题,可以把这问题看成把起点&终点A排除之后,从BCD中选出不同组合,可以表达成 A33A_{3}^{3}A33,也就是3的阶乘。我们都知道阶乘的增长是非常快的,当可选择数量到达10,那么路线为3628800条,当达到20,那么路线数量会跳到2432902008176640000,很快将变的不可计算。
TSP的要求相对简单,起点终点一致,没有太多限制条件。车辆路线问题VRP(vehicle routing problem)更加通用,是tsp的一种泛化,贴近实际场景。VRP可能涉及多辆汽车,起点终点不同,初始路径等等限制条件。
求解算法
由于相关问题需要大量的计算,所以我们通常使用一些策略,去尽量贴近最优解。这里我简单介绍几种比较有名的。
蚁群算法( Ant Colony Optimization)
蚂蚁在巢穴(Nest)和食物(Food)之间搬运,路中间有障碍,有多条路线可以选择。当蚂蚁经过路线之后会留下信息素,信息素的浓度会影响蚂蚁的决策概率。由于更短的路径,相同时间内会有更多蚂蚁通过,慢慢的更短路径的信息素浓度会越来越高,最终会收敛为一条比较好的路线。就像现实生活中一样,蚁群算法路径通常不是最短路径,而是相对较短。
缺点:收敛速度慢,容易陷入局部最优解。
遗传算法(Genetic Algorithm,GA)
遗传算法故名思义是模仿生物进化的一种算法。好的路径片段相当于好的基因,通过不停杂交、变异产生路径变化,然后经过筛选,将好的基因继续流传下去。
基本概念:
种群(Population):
个体(Individual):
染色体(Chromosome):由基因拼接组成;作为个体的蓝本,在算法中一般代表个体自身。
基因(Gene):个体携带的基因。
三个基本操作:交叉(Crossover)、变异(Mutation)和 选择(Selection)。
交叉:
类似于两性繁殖,让两个个体产生两个基因有一定交叉的孩子,这个孩子将会进入群落,进行生存竞争。
变异:
变异则是自身产生一定局部改变,这将产生新的局部路径组合。
选择:选择的目的是为了从当前群体中选出优良的个体,使它们有机会作为父代为下一代繁衍子孙。根据各个个体的适应度值,按照一定的规则或方法从上一代群体中选择出一些优良的个体遗传到下一代种群中。选择的依据是适应性强的个体为下一代贡献一个或多个后代的概率大。选择的基准就是个体的适应度。
拟退火算法(SA simulated annealing)
退火的概念: 我们可能听过淬火,退火的名词,这属于金属冶炼的一个概念,一块铁的硬度和韧性与碳含量有关。碳越高铁越硬但是也越脆,碳越低铁越软、延展性越好。淬火是在金属加热之后快速冷却,让碳保留在铁块内。而退火则是慢慢降温,让碳在铁块中沉淀消失,同时,让铁原子排列更加规则,从而消除应力,不容易断裂。
退火的过程中,铁原子经历了这样一个过程:首先是不是很规则的状态,升温后,铁原子会变的非常活泼,随着温度慢慢降低,铁原子运动的幅度降低,慢慢的会接近按秩序排列。
那么这和路径优化有什么关系呢?我们可把退火的过程看作路径优化的过程。初始状态,我们可以使用贪心算法等方式,选一个近似解。然后模拟升温之后的状态,允许选取非局部最优解来尝试是否能获得更优质的全局解。在降温的过程中,我们模拟铁原子运动幅度降低,允许的路径选取尺度越来越小,最准冷却的时候,会得到一个相对优质的解。
拟退火算法,适合数据量很大,数据没有明显结构性的场景使用。
OR-Tools
介绍
or-tools 是google 推出的解决,运筹学(operation research)工具。可以为我们提供VRP的快速解决工具,功能十分丰富强大,由C++编写保证其性能,可以使用python,java等主语语言调用。
小试牛刀
按照第一张图的case,我们使用or-tools求路径。
from ortools.constraint_solver import routing_enums_pb2,pywrapcp
#根据第一部分给的动图,我们可以把距离整理成这样一个int的二维矩阵。
matrix = [
# A->A,A->B,A->C,A->D
[0, 10, 35, 30],
# B->A,B->B,B->C,B->D
[10, 0, 30, 15],
[35, 30, 0, 30],
[30, 15, 30, 0]
]
# 注意实际可能比这更复杂,A->B和B->A实际可能并不相同。
# 创建一个索引管理器,作用主要是将matrix和index对应起来。
manager = pywrapcp.RoutingIndexManager(
len(matrix),
1,# 车辆数量
0 #从A点出发,最终回到A点。
# 如果起点终点不同,这里再添加一个终点index。
)
# 创建一个路由模型,路由模型最终将负责求解。
routing = pywrapcp.RoutingModel(manager)
# 定义一个距离回调函数,用来计算两个节点之间的距离。
def distance_callback(from_index, to_index):
"""Returns the distance between the two nodes."""
# manager.IndexToNode 会将求解器的内部索引(可以放心地忽略)转换为位置的数字。
from_node = manager.IndexToNode(from_index)
to_node = manager.IndexToNode(to_index)
return matrix[from_node][to_node]
# 注册距离回调函数。
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
# 设置成本评估函数,注意这里仅仅设置了距离作为成本,实际上有可能更为复杂,可能是根据时间、或者油费等等。
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
# 计算路径
solution = routing.SolveWithParameters(search_parameters)
if solution:
"""Prints solution on console."""
print(f"Objective: {solution.ObjectiveValue()} miles")
index = routing.Start(0)
plan_output = "Route for vehicle 0:\n"
route_distance = 0
while not routing.IsEnd(index):
plan_output += f" {manager.IndexToNode(index)} ->"
previous_index = index
index = solution.Value(routing.NextVar(index))
plan_output += f" {manager.IndexToNode(index)}\n"
print(plan_output)
Objective: 90 miles
Route for vehicle 0:
0 -> 1 -> 3 -> 2 -> 0
可以看到可以很快的得到了最佳路线。
策略调整
我们上面给的例子非常简单,即使暴力回溯,也可以很快解决,实际过程中数据量可能会比这个大很多。针对不同场景,我们可以使用不同的求解策略。
现在我们用numpy制造100个点位,来对比不同策略。
import time
import numpy as np
# 生成随机坐标点
num_points = 100 # 指定点的数量
min_value = 0 # 坐标范围的最小值
max_value = 300 # 坐标范围的最大值
# 生成包含随机坐标点的 NumPy 数组
random_points = np.random.randint(low=min_value, high=max_value, size=(num_points, 2))
print("随机坐标点数组:")
print(random_points.tolist())
使用随机点位测试:自动模式、GLS(基于遗传算法的优化算法)、拟退火算法,并使用matplotlib可视化输出。
import time
from ortools.constraint_solver import routing_enums_pb2,pywrapcp
manager = pywrapcp.RoutingIndexManager(len(random_points), 1,0)
routing = pywrapcp.RoutingModel(manager)
from matplotlib import pyplot as plt
import math
def distance_callback(from_i, to_i):
from_index = manager.IndexToNode(from_i)
to_index = manager.IndexToNode(to_i)
""" 计算两点之间距离 """
from_x,from_y=random_points[from_index][0],random_points[from_index][1]
to_x,to_y=random_points[to_index][0],random_points[to_index][1]
distance = math.sqrt(pow(to_x - from_x, 2) + pow(to_y - from_y, 2))
return int(distance)
def get_path(solution)->list:
"""Prints solution on console."""
print(f"Objective: {solution.ObjectiveValue()}")
index = routing.Start(0)
plan_output = "Route for vehicle 0:\n"
route_distance = 0
path=[]
while not routing.IsEnd(index):
plan_output += f" {manager.IndexToNode(index)} ->"
previous_index = index
path.append(random_points[index])
index = solution.Value(routing.NextVar(index))
route_distance += routing.GetArcCostForVehicle(previous_index, index, 0)
plan_output += f" {manager.IndexToNode(index)}\n"
path.append(random_points[manager.IndexToNode(index)])
print(plan_output)
return path
def plot_path(path,distance,time):
x, y = zip(*path)
plt.plot(x, y, marker='o', linestyle='-')
plt.xlabel(f'distance:{distance},scenods:{round(time,2)}')
transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
# 在这里可以切换搜索策略
#search_parameters.local_search_metaheuristic = (
# routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
# 注意GUIDED_LOCAL_SEARCH和SIMULATED_ANNEALING 都需要设置时间限制,否则可能不会停止
#search_parameters.time_limit.seconds = 2
start =time.time()
solution = routing.SolveWithParameters(search_parameters)
end = time.time()
if solution:
path = get_path(solution)
plot_path(path,solution.ObjectiveValue(),end-start)
plt.show()
可以看到,不管是哪种模式,都有比较好的效果。经过多次尝试,通常来说gls更接近最优解,但是会消耗更多时间。automatic 会分析数据,然后选取策略,达到速度和效果的平和。
如果希望有更近一步详细配置,可以查阅ortools的官方文档developers.google.com/optimizatio…
转载自:https://juejin.cn/post/7323055938066546739