本专栏将会重点围绕车辆路径规划问题进行讲解,会涵盖车辆路径规划中的很多常见问题,如TSP问题,CVRP问题,VRPTW问题,VRPCTW-MultiDepot问题,EVRPTW问题,PDVRP问题,Mul-ObjVRP问题,M2-2E-VRP双层网络模型,将会逐步为大家进行梳理与整理。本将小节内容将会讲解第一个问题:TSP问题。
前言:在【01路径规划问题的相关理论】这一小节的内容中,有讲述到TSP问题是车辆路径规划问题的基础模型,我们在系统性的学习VRP问题时,首先来讲解TSP问题,文章将会包含TSP问题的各种算法,会从不同的维度、利用不同的算法来讲解,TSP问题后续也会整理成一个系列文章,分别会从遗传算法、粒子群优化算法、模拟退火算法、蚁群算法、分支定界法以及动态规划等不同算法维度来讲解,目的在于:
①为大家铺垫不同的算法模型,为后续更多更复杂的规划问题打基础;
②后续将不会重点介绍算法的理论框架,会直接根据问题给出解决的代码模型。
根据百度百科的解释:
旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。

我们系列文章将会轻理论,重实践,重代码,不会和你长篇大论关于算法的原理内容,若想比较详细的了解关于原理内容,请自行百度学习。
遗传算法(Genetic Algorithm,GA)起源于达尔文进化论,是一种随机全局搜索优化方法,模拟了自然选择和遗传中发生的复制、交叉(crossover)和变异(mutation)等现象,从任一初始种群出发,通过随机选择、交叉和变异操作,产生一群更适合环境的个体,通过一代一代的进化,最后收敛到一群最适应环境的个体,从而求得问题的优质解的一种方法。
在执行遗传算法前,有两个东西必须提前实现设定好;
- 可行解的表示方法,即编码方式,目前路径规划领域中常用的编码方式为实数编码,除此以外,还有二进制编码、浮点编码等,这些不做为我们的重点。
- 适应度函数,也就是计算子代适应度的函数,这个函数一般是目标函数,如要最大化某个函数,则将这个函数作为目标函数即可,反之最小化的时候加个负号即可。
遗传算法是一个迭代算法,在进行初始化后按照 “选择,交叉,变异” 的顺序进行迭代,当满足设定的结束条件(例如达到一定的代数),便将得到的解作为问题的解。其流程如下所示 :
说明:案例中的TSP问题,我们不需要过多的纠结说,车辆有无限制,编码后如何解码的问题,这可能是初学者对这个算法模型最大的疑问,我们后续在讲解VRP问题时,会提及到车辆限制以及遗传算法中如何解码的问题,其中我相信绝大数同学对于整个算法理解不会有问题,但就一直没有弄明白整数编码的情况下,求出的线路,算法是如何解码的。
我们得明白这样一个问题:
旅行商问题TSP与装箱问题Packing problem的混合问题=车辆路径规划问题VRP
- def random_init(self, num_total, num_city):
- tmp = [x for x in range(num_city)]
- result = []
- for i in range(num_total):
- random.shuffle(tmp)
- result.append(tmp.copy())
- return result
随即初始化一个个体的方法。其中通过rang函数生成一个所有城市节点数量的序列,然后使用shuffle函数将这个序列打乱,就是我们模拟的一种随即路线。
- def greedy_init(self, dis_mat, num_total, num_city):
- start_index = 0
- result = []
- for i in range(num_total):
- rest = [x for x in range(0, num_city)]
- # 所有起始点都已经生成了
- if start_index >= num_city:
- start_index = np.random.randint(0, num_city)
- result.append(result[start_index].copy())
- continue
- current = start_index
- rest.remove(current)
- # 找到一条最近邻路径
- result_one = [current]
- while len(rest) != 0:
- tmp_min = math.inf
- tmp_choose = -1
- for x in rest:
- if dis_mat[current][x] < tmp_min:
- tmp_min = dis_mat[current][x]
- tmp_choose = x
-
- current = tmp_choose
- result_one.append(tmp_choose)
- rest.remove(tmp_choose)
- result.append(result_one)
- start_index += 1
- return result
- # 计算路径长度
- def compute_pathlen(self, path, dis_mat):
- try:
- a = path[0]
- b = path[-1]
- except:
- import pdb
- pdb.set_trace()
- result = dis_mat[a][b]
- for i in range(len(path) - 1):
- a = path[i]
- b = path[i + 1]
- result += dis_mat[a][b]
- return result
- # 计算种群适应度
- def compute_adp(self, fruits):
- adp = []
- for fruit in fruits:
- if isinstance(fruit, int):
- import pdb
- pdb.set_trace()
- length = self.compute_pathlen(fruit, self.dis_mat)
- adp.append(1.0 / length)
- return np.array(adp)
- def swap_part(self, list1, list2):
- index = len(list1)
- list = list1 + list2
- list = list[::-1]
- return list[:index], list[index:]
- def ga_cross(self, x, y):
- len_ = len(x)
- assert len(x) == len(y)
- path_list = [t for t in range(len_)]
- order = list(random.sample(path_list, 2))
- order.sort()
- start, end = order
-
- # 找到冲突点并存下他们的下标,x中存储的是y中的下标,y中存储x与它冲突的下标
- tmp = x[start:end]
- x_conflict_index = []
- for sub in tmp:
- index = y.index(sub)
- if not (index >= start and index < end):
- x_conflict_index.append(index)
-
- y_confict_index = []
- tmp = y[start:end]
- for sub in tmp:
- index = x.index(sub)
- if not (index >= start and index < end):
- y_confict_index.append(index)
-
- assert len(x_conflict_index) == len(y_confict_index)
-
- # 交叉
- tmp = x[start:end].copy()
- x[start:end] = y[start:end]
- y[start:end] = tmp
-
- # 解决冲突
- for index in range(len(x_conflict_index)):
- i = x_conflict_index[index]
- j = y_confict_index[index]
- y[i], x[j] = x[j], y[i]
-
- assert len(set(x)) == len_ and len(set(y)) == len_
- return list(x), list(y)
- def ga_mutate(self, gene):
- path_list = [t for t in range(len(gene))]
- order = list(random.sample(path_list, 2))
- start, end = min(order), max(order)
- tmp = gene[start:end]
- # np.random.shuffle(tmp)
- tmp = tmp[::-1]
- gene[start:end] = tmp
- return list(gene)
- def run(self):
- BEST_LIST = None
- best_score = -math.inf
- self.best_record = []
- for i in range(1, self.iteration + 1):
- tmp_best_one, tmp_best_score = self.ga()
- self.iter_x.append(i)
- self.iter_y.append(1. / tmp_best_score)
- if tmp_best_score > best_score:
- best_score = tmp_best_score
- BEST_LIST = tmp_best_one
- self.best_record.append(1./best_score)
- print(i,1./best_score)
- print(1./best_score)
- return self.location[BEST_LIST], 1. / best_score
- fig, axs = plt.subplots(2, 1, sharex=False, sharey=False)
- axs[0].scatter(Best_path[:, 0], Best_path[:,1])
- Best_path = np.vstack([Best_path, Best_path[0]])
- axs[0].plot(Best_path[:, 0], Best_path[:, 1])
- axs[0].set_title('规划结果')
- iterations = range(model.iteration)
- best_record = model.best_record
- axs[1].plot(iterations, best_record)
- axs[1].set_title('收敛曲线')
- plt.show()
最终运行的结果如下:

模型收敛图如下:

若读者朋友需要全部的案例数据以及完整的代码,可以自行点击案例链接下载,但其实如果您能看懂上述的代码结构,我相信不用作者提供完整的代码也是可以自行进行测试完成的,也希望朋友们在实在没办法的情况下,才去下载完整资源,这样的学习效果会最好。