着实不错的自适应大邻域搜索算法ALNS

news2024/11/18 3:30:07

文章目录

  • 引言
  • 演进路线
    • 邻域搜索,NS
    • 变邻域搜素,VDNS
    • 大邻域搜索,LNS
    • 自适应大邻域搜索,ALNS
  • 代码实现
    • 34个国内城市的TSP
    • 测试集XQF131
  • 相关阅读

引言

之前介绍的差分进化算法和蚁群算法分别适用于求解连续优化问题和组合优化问题,它们都属于基于种群进化的智能优化算法。

除此之外,智能优化算法中还有一大类,即基于单点出发的智能优化算法,比如模拟退火算法、禁忌搜索算法和邻域搜索算法等。本文将着重介绍其中一个邻域搜索算法:自适应大邻域搜索算法(Adaptive large neighborhood search,ALNS)。

选择ALNS的主要原因包含三个:ALNS中很多参数设计的逻辑是自适应的,比较符合长期发展的需要;从行业实践来看,菜鸟的车辆路径规划引擎入围过2021年Franz Edelman杰出成就奖(被称为运筹学界“奥斯卡”),其核心算法就是ALNS,因此ALNS看起来比较有前景;之前我自己做过变邻域搜索算法的项目实践,效果还不错,而ALNS听名字就比变邻域搜索更加高级,因此也有动力去学习ALNS。

正文见下。

演进路线

在网上搜索邻域搜索算法,能看到很多相关的名词,比如邻域、变邻域和大邻域等,对于我这种一知半解的人来说,着实有些让人费解。本着追根溯源的思路,我详细学习了大佬Pisinger的综述性文章(其实是书的其中一章):Large Neighborhood Search。概念众多,个人认为,了解清楚以下4个就足够了:邻域搜索(NS)、变邻域搜素(VDNS)、大邻域搜索(LNS)和自适应大邻域搜索(ALNS)。

邻域搜索,NS

邻域这个词应该不难理解,本质就是在当前解 x 0 \pmb x_0 x0 附近的一些区域。至于如何定义区域,则需要看实际问题的特征。

如果 x \pmb x x 是连续的,可以定义邻域是距离 x 0 \pmb x_0 x0 不超过1的空间范围,当 x 0 \pmb x_0 x0 是一维时,例如 x 0 = 2 x_0=2 x0=2,此时邻域就是 [ 1 , 3 ] [1,3] [1,3];当 x 0 \pmb x_0 x0 是二维时,例如 x 0 = [ 2 , 2 ] \pmb x_0=[2,2] x0=[2,2],此时邻域就是圆心为 [ 2 , 2 ] [2,2] [2,2]、半径为1的圆。

如果 x \pmb x x 是离散的,例如 x 0 = [ 2 , 2 ] \pmb x_0=[2,2] x0=[2,2],可以定义邻域是某一个分量向左或向右移动1个单位,此时邻域便包括: [ 1 , 2 ] 、 [ 3 , 2 ] 、 [ 2 , 1 ] [1,2]、[3,2]、[2,1] [1,2][3,2][2,1] [ 2 , 3 ] [2,3] [2,3]

有了邻域后,就可以做邻域搜索了,其含义就是在邻域内找到最好的解 x ⋆ \pmb x ^{\star} x,从而更新 x 0 \pmb x_0 x0

邻域搜索的核心是设计出合理的邻域,下图显示了不同邻域下最终得到不同解的示意图。基本结论为:邻域越大,越有机会找到全局最优解,但是为此也需要花费更多的计算时间。

再举个TSP中的邻域实例:2-opt。假设当前解为
A − B − C − D − E − F − G − H − A A-B-C-D-E-F-G-H-A ABCDEFGHA
随机选择选择两个点,假设是4和7,对应D和G,此时可以将当前解拆成如下3段
A − B − C , D − E − F − G , H − A A-B-C,\quad D-E-F-G,\quad H-A ABC,DEFG,HA
2-opt的定义是,前后两段保持不变,中间一段进行翻转,然后重新拼接起来,即
A − B − C − G − F − E − D − H − A A-B-C-G-F-E-D-H-A ABCGFEDHA

变邻域搜素,VDNS

如果邻域空间随着问题规模的增长成指数型增长,或者邻域空间本身就比较大,那么通常会被定义为VLSN(very large-scale neighbourhood search)。针对VLSN,要实现邻域的完全遍历,几乎不太现实,因此就需要一些新的解决方案。

变邻域搜索(Variable-Depth Neighborhood Search, VDNS)便是其中一种。其基本思路为:首先在 x 0 \pmb x_0 x0的小邻域内搜索,如果有更优解,则更新 x 0 \pmb x_0 x0 x 1 \pmb x_1 x1,然后在 x 1 \pmb x_1 x1的小邻域内重新搜索;如果没有更优解,则扩大 x 0 \pmb x_0 x0的邻域,继续搜索,找到更优解后,更新 x 0 \pmb x_0 x0并回到小邻域搜索。

下图是一个VDNS的实例。通过在2处的3次邻域扩充,找到了更优解3。

相比NS,VDNS可以在更多邻域内寻找更优解,计算时间也较为可控。

大邻域搜索,LNS

大邻域搜索(Large Neighborhood Search, LNS)可以理解为求解VLSN的另一种解决方案,而且主要用来求解组合优化问题。这里直接使用原文中的VRP实例来描述LNS的实现过程。

VRP,vehicle routing problem,车辆路径问题,是非常经典的一类组合优化问题,可以简单描述为:一个中心点和多个客户,主要目标是使用尽量少的车和行驶尽量短的距离,或花费尽量少的成本,基本约束是要拜访完所有客户并且车辆最终返回中心点,如果再加入其他约束,如每辆车有装载限制,则被称为CVRP问题,如果继续加入约束,还可以扩展至VRP-TW等。

下图是一个VRP解的实例。图中的方块为中心点,一共21个客户,使用了4辆车,分别拜访了6(蓝绿)、3(红)、5(蓝)和7(紫)个客户。从图上看,很多车辆间的路径是交叉的,并不美观,大致判断总行驶距离是有优化空间的。那具体该如何优化呢?

LNS提供的是一种“destroy”+“repair”组合的优化方案。首先,对上述的当前解进行destroy操作,将当前解进行一定程度的破坏。下图显示了一种destroy的方案,即针对每辆车,分别删除其中2、1、1和2个客户,然后跳过被破坏的客户点,重新连接成闭合曲线。

接着,对破坏后的解,进行repair操作,将被删除的客户重新插入到路径中,保证约束的满足,得到一个测试解。下图的方案是贪心策略:将被删除的客户插入到总路径最短的位置中。

然后,比较当前解和测试解的指标大小,如果指标更优,则将当前解更新为测试解,否则便不更新。

此后,重新进行destroy+repair,直至满足终止条件,退出优化。

下图是LNS的伪代码。目标函数为最小化 c ( x ) c(x) c(x);第4行的 d ( x ) d(x) d(x)指的是destroy, r ( d ( x ) ) r(d(x)) r(d(x))指的是repair;尤其需要注意的是这里有三个 x x x x x x指的是当前解, x t x^t xt指的是基于当前解 x x x + destroy + repair产生的测试解, x b x^b xb指的是截至当前的历史最优解;另外,在第5行还有个 accept ( x t , x ) \text{accept}(x^t,x) accept(xt,x)函数,表明是否更新当前解 x x x,还需要一个判断,我们刚刚说的判断指标是否更优是最初的版本,后续版本更倾向于借鉴模拟退火算法中的设计思路:产生一个随机数,如果其值低于 e − ( c ( x t ) − c ( x ) ) / T e^{-(c(x^t)-c(x))/T} e(c(xt)c(x))/T( T T T为外部参数),即使指标没有更优,也会将当前解更新为测试解,这样当前解便有机会跳出局部最优解,找到更优解。

自适应大邻域搜索,ALNS

从LNS的设计逻辑来看,算法的核心是设计出匹配具体问题的destroy和repair算子。针对repair,常用的算子不多,主要就随机和贪心两类策略;但针对destroy,算子的数量就有些大了:首先需要确定的是destroy的比例,还是以上一节的VRP问题为例,一共21个客户,如果destroy比例为10%,则需要删除2个客户;然后还要确定删除哪两个,这就有420种情况,如果destroy比例为20%,那就有143640种情况。所以,如何在每次迭代时找出恰当的destroy和repair算子,就显得尤为重要。

ALNS的目标是根据具体问题的特征,自适应地找到最恰当的算子。其设计思路可以理解为:开始阶段先罗列可以选择的所有算子,并且不同算子的选择概率是完全相同的;此后如果某个算子能带来更大的指标提升,该算子被选择的概率就会提升,从而有更大的概率在后续的迭代中被使用。

下图是ALNS的伪代码。相比LNS,主要变化为:第2、4和12行。第2行中的 ρ − \rho^- ρ指的是选择不同destroy算子的概率, ρ + \rho^+ ρ+指的是选择不同repair算子的概率。迭代开始时,他们均被设置为1,即被选择的概率保持相同;每一轮迭代后,都会对它们进行更新,即第12行,更新逻辑稍微复杂,稍后再细说。第4行中的 Ω − \Omega^- Ω指的是destroy算子的集合, Ω + \Omega^+ Ω+指的是repair算子的集合,当前轮次选择哪一个destroy和repair,采用轮盘赌的方式确定,即 ρ − \rho^- ρ ρ + \rho^+ ρ+的概率越大,被选中的概率越大。

回到 ρ − \rho^- ρ ρ + \rho^+ ρ+的更新逻辑。为了让它们的更新和指标提升程度挂上勾,需要先引入一个额外参数
Ψ = { w 1 , 测试解为历史最优解 w 2 , 测试解不是历史最优解,但是优于当前解 w 3 , 当前解更新为测试解 w 4 , 测试解较差,当前解不更新 \Psi=\left\{ \begin{aligned} w_1, & 测试解为历史最优解 \\ w_2, & 测试解不是历史最优解,但是优于当前解 \\ w_3, & 当前解更新为测试解 \\ w_4, & 测试解较差,当前解不更新 \end{aligned} \right. Ψ= w1,w2,w3,w4,测试解为历史最优解测试解不是历史最优解,但是优于当前解当前解更新为测试解测试解较差,当前解不更新

然后便可以按如下公式进行更新 ρ − \rho^- ρ ρ + \rho^+ ρ+
ρ − = λ ρ − + ( 1 − λ ) Ψ \rho^- = \lambda \rho^- + (1-\lambda )\Psi ρ=λρ+(1λ)Ψ
ρ + = λ ρ + + ( 1 − λ ) Ψ \rho^+ = \lambda \rho^+ + (1-\lambda )\Psi ρ+=λρ++(1λ)Ψ
此处, λ \lambda λ的含义可以类比蚁群算法中信息素的挥发系数。

需要说明的是,为了保证更大的 Ψ \Psi Ψ能让 ρ − \rho^- ρ ρ + \rho^+ ρ+也更大,一般要求 w 1 > w 2 > w 3 > w 4 w_1>w_2>w_3>w_4 w1>w2>w3>w4

代码实现

34个国内城市的TSP

上一篇介绍蚁群算法的文章中,计算实例是国内34个主要城市的TSP问题。其中,蚁群算法得到的最优解是15944.43,使用ortools得到的全局最优解是15614.84。

本节也使用该实例,以此初步评估ALNS的能力。以下是python实现的代码。其中,destroy算子有3个:随机筛选N个城市、删除距离最大的N个城市和随机删除连续的N个城市;repair算子有2个:随机插入和贪心插入,不过考虑到随机插入的效果大概率比较差,所以代码中实际只使用了贪心插入,设置方式参见代码第138行。代码的可扩展性比较好,如果想调整destroy和repair算子,只需要调整第66行的destroy函数和109行的repair函数即可。

import copy
import math
import time
import random
import numpy as np
import pandas as pd


# 计算TSP总距离
def dis_cal(path, dist_mat):
    distance = 0
    for i in range(len(path) - 1):
        distance += dist_mat[path[i]][path[i + 1]]
    distance += dist_mat[path[-1]][path[0]]
    return distance


# 随机删除N个城市
def random_destroy(x, destroy_city_cnt):
    new_x = copy.deepcopy(x)
    removed_cities = []

    # 随机选择N个城市,并删除
    removed_index = random.sample(range(0, len(x)), destroy_city_cnt)
    for i in removed_index:
        removed_cities.append(new_x[i])
        x.remove(new_x[i])
    return removed_cities


# 删除距离最大的N个城市
def max_n_destroy(x, destroy_city_cnt):
    new_x = copy.deepcopy(x)
    removed_cities = []

    # 计算顺序距离并排序
    dis = []
    for i in range(len(new_x) - 1):
        dis.append(dist_mat[new_x[i]][new_x[i + 1]])
    dis.append(dist_mat[new_x[-1]][new_x[0]])
    sorted_index = np.argsort(np.array(dis))

    # 删除最大的N个城市
    for i in range(destroy_city_cnt):
        removed_cities.append(new_x[sorted_index[-1 - i]])
        x.remove(new_x[sorted_index[-1 - i]])

    return removed_cities


# 随机删除连续的N个城市
def continue_n_destroy(x, destroy_city_cnt):

    new_x = copy.deepcopy(x)
    removed_cities = []

    # 随机选择N个城市,并删除
    removed_index = random.sample(range(0, len(x)-destroy_city_cnt), 1)[0]
    for i in range(removed_index + destroy_city_cnt, removed_index, -1):
        removed_cities.append(new_x[i])
        x.remove(new_x[i])
    return removed_cities


# destroy操作
def destroy(flag, x, destroy_city_cnt):
    # 三个destroy算子,第一个是随机删除N个城市,第二个是删除距离最大的N个城市,第三个是随机删除连续的N个城市
    removed_cities = []
    if flag == 0:
        # 随机删除N个城市
        removed_cities = random_destroy(x, destroy_city_cnt)
    elif flag == 1:
        # 删除距离最大的N个城市
        removed_cities = max_n_destroy(x, destroy_city_cnt)
    elif flag == 2:
        # 随机删除连续的N个城市
        removed_cities = continue_n_destroy(x, destroy_city_cnt)

    return removed_cities


# 随机插入
def random_insert(x, removed_cities):
    insert_index = random.sample(range(0, len(x)), len(removed_cities))
    for i in range(len(insert_index)):
        x.insert(insert_index[i], removed_cities[i])


# 贪心插入
def greedy_insert(x, removed_cities):
    dis = float('inf')
    insert_index = -1

    for i in range(len(removed_cities)):
        # 寻找插入后的最小总距离
        for j in range(len(x) + 1):
            new_x = copy.deepcopy(x)
            new_x.insert(j, removed_cities[i])
            if dis_cal(new_x, dist_mat) < dis:
                dis = dis_cal(new_x, dist_mat)
                insert_index = j

        # 最小位置处插入
        x.insert(insert_index, removed_cities[i])
        dis = float('inf')


# repair操作
def repair(flag, x, removed_cities):
    # 两个repair算子,第一个是随机插入,第二个贪心插入
    if flag == 0:
        random_insert(x, removed_cities)
    elif flag == 1:
        greedy_insert(x, removed_cities)


# 选择destroy算子
def select_and_destroy(destroy_w, x, destroy_city_cnt):
    # 轮盘赌逻辑选择算子
    prob = destroy_w / np.array(destroy_w).sum()
    seq = [i for i in range(len(destroy_w))]
    destroy_operator = np.random.choice(seq, size=1, p=prob)[0]

    # destroy操作
    removed_cities = destroy(destroy_operator, x, destroy_city_cnt)

    return x, removed_cities, destroy_operator


# 选择repair算子
def select_and_repair(repair_w, x, removed_cities):
    # # 轮盘赌逻辑选择算子
    prob = repair_w / np.array(repair_w).sum()
    seq = [i for i in range(len(repair_w))]
    repair_operator = np.random.choice(seq, size=1, p=prob)[0]

    # repair操作:此处设定repair_operator=1,即只使用贪心策略
    repair(1, x, removed_cities)

    return x, repair_operator


# ALNS主程序
def calc_by_alns(dist_mat):
    # 模拟退火温度
    T = 100
    # 降温速度
    a = 0.97

    # destroy的城市数量
    destroy_city_cnt = int(len(dist_mat) * 0.1)
    # destroy算子的初始权重
    destroy_w = [1, 1, 1]
    # repair算子的初始权重
    repair_w = [1, 1]
    # destroy算子的使用次数
    destroy_cnt = [0, 0, 0]
    # repair算子的使用次数
    repair_cnt = [0, 0]
    # destroy算子的初始得分
    destroy_score = [1, 1, 1]
    # repair算子的初始得分
    repair_score = [1, 1]
    # destroy和repair的挥发系数
    lambda_rate = 0.5

    # 当前解,第一代,贪心策略生成
    removed_cities = [i for i in range(dist_mat.shape[0])]
    x = []
    repair(1, x, removed_cities)

    # 历史最优解解,第一代和当前解相同,注意是深拷贝,此后有变化不影响x,也不会因x的变化而被影响
    history_best_x = copy.deepcopy(x)

    # 迭代
    cur_iter = 0
    max_iter = 1000
    print(
        'cur_iter: {}, best_f: {}, best_x: {}'.format(cur_iter, dis_cal(history_best_x, dist_mat), history_best_x))
    while cur_iter < max_iter:
        # destroy算子
        destroyed_x, remove, destroy_operator_index = select_and_destroy(destroy_w, x, destroy_city_cnt)
        destroy_cnt[destroy_operator_index] += 1

        # repair算子
        new_x, repair_operator_index = select_and_repair(repair_w, destroyed_x, remove)
        repair_cnt[repair_operator_index] += 1

        if dis_cal(new_x, dist_mat) <= dis_cal(x, dist_mat):
            # 测试解更优,更新当前解
            x = new_x
            if dis_cal(new_x, dist_mat) <= dis_cal(history_best_x, dist_mat):
                # 测试解为历史最优解,更新历史最优解,并设置最高的算子得分
                history_best_x = copy.deepcopy(new_x)
                destroy_score[destroy_operator_index] = 1.5
                repair_score[repair_operator_index] = 1.5
            else:
                # 测试解不是历史最优解,但优于当前解,设置第二高的算子得分
                destroy_score[destroy_operator_index] = 1.2
                repair_score[repair_operator_index] = 1.2
        else:
            if np.random.random() < np.exp((dis_cal(x, dist_mat) - dis_cal(new_x, dist_mat))) / T:
                # 当前解优于测试解,但满足模拟退火逻辑,依然更新当前解,设置第三高的算子得分
                x = new_x
                destroy_score[destroy_operator_index] = 0.8
                repair_score[repair_operator_index] = 0.8
            else:
                # 当前解优于测试解,也不满足模拟退火逻辑,不更新当前解,设置最低的算子得分
                destroy_score[destroy_operator_index] = 0.5
                repair_score[repair_operator_index] = 0.5

        # 更新destroy算子的权重
        destroy_w[destroy_operator_index] = \
            destroy_w[destroy_operator_index] * lambda_rate + \
            (1 - lambda_rate) * destroy_score[destroy_operator_index] / destroy_cnt[destroy_operator_index]
        # 更新repair算子的权重
        repair_w[repair_operator_index] = \
            repair_w[repair_operator_index] * lambda_rate + \
            (1 - lambda_rate) * repair_score[repair_operator_index] / repair_cnt[repair_operator_index]
        # 降低温度
        T = a * T

        # 结束一轮迭代,重置模拟退火初始温度
        cur_iter += 1
        print(
            'cur_iter: {}, best_f: {}, best_x: {}'.format(cur_iter, dis_cal(history_best_x, dist_mat), history_best_x))

    # 打印ALNS得到的最优解
    print(history_best_x)
    print(dis_cal(history_best_x, dist_mat))


if __name__ == '__main__':
    original_cities = [['西宁', 101.74, 36.56],
                       ['兰州', 103.73, 36.03],
                       ['银川', 106.27, 38.47],
                       ['西安', 108.95, 34.27],
                       ['郑州', 113.65, 34.76],
                       ['济南', 117, 36.65],
                       ['石家庄', 114.48, 38.03],
                       ['太原', 112.53, 37.87],
                       ['呼和浩特', 111.65, 40.82],
                       ['北京', 116.407526, 39.90403],
                       ['天津', 117.200983, 39.084158],
                       ['沈阳', 123.38, 41.8],
                       ['长春', 125.35, 43.88],
                       ['哈尔滨', 126.63, 45.75],
                       ['上海', 121.473701, 31.230416],
                       ['杭州', 120.19, 30.26],
                       ['南京', 118.78, 32.04],
                       ['合肥', 117.27, 31.86],
                       ['武汉', 114.31, 30.52],
                       ['长沙', 113, 28.21],
                       ['南昌', 115.89, 28.68],
                       ['福州', 119.3, 26.08],
                       ['台北', 121.3, 25.03],
                       ['香港', 114.173355, 22.320048],
                       ['澳门', 113.54909, 22.198951],
                       ['广州', 113.23, 23.16],
                       ['海口', 110.35, 20.02],
                       ['南宁', 108.33, 22.84],
                       ['贵阳', 106.71, 26.57],
                       ['重庆', 106.551556, 29.563009],
                       ['成都', 104.06, 30.67],
                       ['昆明', 102.73, 25.04],
                       ['拉萨', 91.11, 29.97],
                       ['乌鲁木齐', 87.68, 43.77]]
    original_cities = pd.DataFrame(original_cities, columns=['城市', '经度', '纬度'])
    D = original_cities[['经度', '纬度']].values * math.pi / 180
    city_cnt = len(original_cities)
    dist_mat = np.zeros((city_cnt, city_cnt))
    for i in range(city_cnt):
        for j in range(city_cnt):
            if i == j:
                # 相同城市不允许访问
                dist_mat[i][j] = 1000000
            else:
                # 单位:km
                dist_mat[i][j] = 6378.14 * math.acos(
                    math.cos(D[i][1]) * math.cos(D[j][1]) * math.cos(D[i][0] - D[j][0]) +
                    math.sin(D[i][1]) * math.sin(D[j][1]))

    # ALNS求解TSP
    time0 = time.time()
    calc_by_alns(dist_mat)
    print('使用ALNS求解TSP,耗时: {} s'.format(time.time() - time0))

运行代码后发现,经过不到4s的计算时间,ALNS即可得到全局最优解15614.84。

想了一下,会不会是上述代码中的初始解质量本身比较高,才导致最终的计算效率比较高?目前代码中初始解的计算方案是贪心策略,代码见168到170行。所以把这几行换成

x = [i for i in range(dist_mat.shape[0])]

即初始解只是简单的从小到大排列。然后重新运行代码,发现依然在4秒内就可以得到全局最优解15614.84。

看起来,ALNS的效果还是挺好的。

测试集XQF131

为了进一步评估ALNS的效果,再增加点问题规模。本节使用通用的TSP测试集之一:XQF131。该TSP中包含131个城市点,全局最优解为564。

先尝试调用ortools求解,发现计算了6个小时都没返回优化结果,就放弃继续死磕了。

然后是尝试ALNS,以下是python实现的代码。ALNS的逻辑和上一节保持一致,只是替换了输入数据。

import copy
import math
import time
import random
import numpy as np
import pandas as pd


# 计算TSP总距离
def dis_cal(path, dist_mat):
    distance = 0
    for i in range(len(path) - 1):
        distance += dist_mat[path[i]][path[i + 1]]
    distance += dist_mat[path[-1]][path[0]]
    return distance


# 随机删除N个城市
def random_destroy(x, destroy_city_cnt):
    new_x = copy.deepcopy(x)
    removed_cities = []

    # 随机选择N个城市,并删除
    removed_index = random.sample(range(0, len(x)), destroy_city_cnt)
    for i in removed_index:
        removed_cities.append(new_x[i])
        x.remove(new_x[i])
    return removed_cities


# 删除距离最大的N个城市
def max_n_destroy(x, destroy_city_cnt):
    new_x = copy.deepcopy(x)
    removed_cities = []

    # 计算顺序距离并排序
    dis = []
    for i in range(len(new_x) - 1):
        dis.append(dist_mat[new_x[i]][new_x[i + 1]])
    dis.append(dist_mat[new_x[-1]][new_x[0]])
    sorted_index = np.argsort(np.array(dis))

    # 删除最大的N个城市
    for i in range(destroy_city_cnt):
        removed_cities.append(new_x[sorted_index[-1 - i]])
        x.remove(new_x[sorted_index[-1 - i]])

    return removed_cities


# 随机删除连续的N个城市
def continue_n_destroy(x, destroy_city_cnt):

    new_x = copy.deepcopy(x)
    removed_cities = []

    # 随机选择N个城市,并删除
    removed_index = random.sample(range(0, len(x)-destroy_city_cnt), 1)[0]
    for i in range(removed_index + destroy_city_cnt, removed_index, -1):
        removed_cities.append(new_x[i])
        x.remove(new_x[i])
    return removed_cities


# destroy操作
def destroy(flag, x, destroy_city_cnt):
    # 三个destroy算子,第一个是随机删除N个城市,第二个是删除距离最大的N个城市,第三个是随机删除连续的N个城市
    removed_cities = []
    if flag == 0:
        # 随机删除N个城市
        removed_cities = random_destroy(x, destroy_city_cnt)
    elif flag == 1:
        # 删除距离最大的N个城市
        removed_cities = max_n_destroy(x, destroy_city_cnt)
    elif flag == 2:
        # 随机删除连续的N个城市
        removed_cities = continue_n_destroy(x, destroy_city_cnt)

    return removed_cities


# 随机插入
def random_insert(x, removed_cities):
    insert_index = random.sample(range(0, len(x)), len(removed_cities))
    for i in range(len(insert_index)):
        x.insert(insert_index[i], removed_cities[i])


# 贪心插入
def greedy_insert(x, removed_cities):
    dis = float('inf')
    insert_index = -1

    for i in range(len(removed_cities)):
        # 寻找插入后的最小总距离
        for j in range(len(x) + 1):
            new_x = copy.deepcopy(x)
            new_x.insert(j, removed_cities[i])
            if dis_cal(new_x, dist_mat) < dis:
                dis = dis_cal(new_x, dist_mat)
                insert_index = j

        # 最小位置处插入
        x.insert(insert_index, removed_cities[i])
        dis = float('inf')


# repair操作
def repair(flag, x, removed_cities):
    # 两个repair算子,第一个是随机插入,第二个贪心插入
    if flag == 0:
        random_insert(x, removed_cities)
    elif flag == 1:
        greedy_insert(x, removed_cities)


# 选择destroy算子
def select_and_destroy(destroy_w, x, destroy_city_cnt):
    # 轮盘赌逻辑选择算子
    prob = destroy_w / np.array(destroy_w).sum()
    seq = [i for i in range(len(destroy_w))]
    destroy_operator = np.random.choice(seq, size=1, p=prob)[0]

    # destroy操作
    removed_cities = destroy(destroy_operator, x, destroy_city_cnt)

    return x, removed_cities, destroy_operator


# 选择repair算子
def select_and_repair(repair_w, x, removed_cities):
    # # 轮盘赌逻辑选择算子
    prob = repair_w / np.array(repair_w).sum()
    seq = [i for i in range(len(repair_w))]
    repair_operator = np.random.choice(seq, size=1, p=prob)[0]

    # repair操作:此处设定repair_operator=1,即只使用贪心策略
    repair(1, x, removed_cities)

    return x, repair_operator


# ALNS主程序
def calc_by_alns(dist_mat):
    # 模拟退火温度
    T = 100
    # 降温速度
    a = 0.97

    # destroy的城市数量
    destroy_city_cnt = int(len(dist_mat) * 0.1)
    # destroy算子的初始权重
    destroy_w = [1, 1, 1]
    # repair算子的初始权重
    repair_w = [1, 1]
    # destroy算子的使用次数
    destroy_cnt = [0, 0, 0]
    # repair算子的使用次数
    repair_cnt = [0, 0]
    # destroy算子的初始得分
    destroy_score = [1, 1, 1]
    # repair算子的初始得分
    repair_score = [1, 1]
    # destroy和repair的挥发系数
    lambda_rate = 0.5

    # 当前解,第一代,贪心策略生成
    removed_cities = [i for i in range(dist_mat.shape[0])]
    x = []
    repair(1, x, removed_cities)

    # 历史最优解解,第一代和当前解相同,注意是深拷贝,此后有变化不影响x,也不会因x的变化而被影响
    history_best_x = copy.deepcopy(x)

    # 迭代
    cur_iter = 0
    max_iter = 1000
    print(
        'cur_iter: {}, best_f: {}, best_x: {}'.format(cur_iter, dis_cal(history_best_x, dist_mat), history_best_x))
    while cur_iter < max_iter:
        # destroy算子
        destroyed_x, remove, destroy_operator_index = select_and_destroy(destroy_w, x, destroy_city_cnt)
        destroy_cnt[destroy_operator_index] += 1

        # repair算子
        new_x, repair_operator_index = select_and_repair(repair_w, destroyed_x, remove)
        repair_cnt[repair_operator_index] += 1

        if dis_cal(new_x, dist_mat) <= dis_cal(x, dist_mat):
            # 测试解更优,更新当前解
            x = new_x
            if dis_cal(new_x, dist_mat) <= dis_cal(history_best_x, dist_mat):
                # 测试解为历史最优解,更新历史最优解,并设置最高的算子得分
                history_best_x = copy.deepcopy(new_x)
                destroy_score[destroy_operator_index] = 1.5
                repair_score[repair_operator_index] = 1.5
            else:
                # 测试解不是历史最优解,但优于当前解,设置第二高的算子得分
                destroy_score[destroy_operator_index] = 1.2
                repair_score[repair_operator_index] = 1.2
        else:
            if np.random.random() < np.exp((dis_cal(x, dist_mat) - dis_cal(new_x, dist_mat))) / T:
                # 当前解优于测试解,但满足模拟退火逻辑,依然更新当前解,设置第三高的算子得分
                x = new_x
                destroy_score[destroy_operator_index] = 0.8
                repair_score[repair_operator_index] = 0.8
            else:
                # 当前解优于测试解,也不满足模拟退火逻辑,不更新当前解,设置最低的算子得分
                destroy_score[destroy_operator_index] = 0.5
                repair_score[repair_operator_index] = 0.5

        # 更新destroy算子的权重
        destroy_w[destroy_operator_index] = \
            destroy_w[destroy_operator_index] * lambda_rate + \
            (1 - lambda_rate) * destroy_score[destroy_operator_index] / destroy_cnt[destroy_operator_index]
        # 更新repair算子的权重
        repair_w[repair_operator_index] = \
            repair_w[repair_operator_index] * lambda_rate + \
            (1 - lambda_rate) * repair_score[repair_operator_index] / repair_cnt[repair_operator_index]
        # 降低温度
        T = a * T

        # 结束一轮迭代,重置模拟退火初始温度
        cur_iter += 1
        print(
            'cur_iter: {}, best_f: {}, best_x: {}'.format(cur_iter, dis_cal(history_best_x, dist_mat), history_best_x))

    # 打印ALNS得到的最优解
    print(history_best_x)
    print(dis_cal(history_best_x, dist_mat))


if __name__ == '__main__':
    original_cities = [[0, 13],
              [0, 26],
              [0, 27],
              [0, 39],
              [2, 0],
              [5, 13],
              [5, 19],
              [5, 25],
              [5, 31],
              [5, 37],
              [5, 43],
              [5, 8],
              [8, 0],
              [9, 10],
              [10, 10],
              [11, 10],
              [12, 10],
              [12, 5],
              [15, 13],
              [15, 19],
              [15, 25],
              [15, 31],
              [15, 37],
              [15, 43],
              [15, 8],
              [18, 11],
              [18, 13],
              [18, 15],
              [18, 17],
              [18, 19],
              [18, 21],
              [18, 23],
              [18, 25],
              [18, 27],
              [18, 29],
              [18, 31],
              [18, 33],
              [18, 35],
              [18, 37],
              [18, 39],
              [18, 41],
              [18, 42],
              [18, 44],
              [18, 45],
              [25, 11],
              [25, 15],
              [25, 22],
              [25, 23],
              [25, 24],
              [25, 26],
              [25, 28],
              [25, 29],
              [25, 9],
              [28, 16],
              [28, 20],
              [28, 28],
              [28, 30],
              [28, 34],
              [28, 40],
              [28, 43],
              [28, 47],
              [32, 26],
              [32, 31],
              [33, 15],
              [33, 26],
              [33, 29],
              [33, 31],
              [34, 15],
              [34, 26],
              [34, 29],
              [34, 31],
              [34, 38],
              [34, 41],
              [34, 5],
              [35, 17],
              [35, 31],
              [38, 16],
              [38, 20],
              [38, 30],
              [38, 34],
              [40, 22],
              [41, 23],
              [41, 32],
              [41, 34],
              [41, 35],
              [41, 36],
              [48, 22],
              [48, 27],
              [48, 6],
              [51, 45],
              [51, 47],
              [56, 25],
              [57, 12],
              [57, 25],
              [57, 44],
              [61, 45],
              [61, 47],
              [63, 6],
              [64, 22],
              [71, 11],
              [71, 13],
              [71, 16],
              [71, 45],
              [71, 47],
              [74, 12],
              [74, 16],
              [74, 20],
              [74, 24],
              [74, 29],
              [74, 35],
              [74, 39],
              [74, 6],
              [77, 21],
              [78, 10],
              [78, 32],
              [78, 35],
              [78, 39],
              [79, 10],
              [79, 33],
              [79, 37],
              [80, 10],
              [80, 41],
              [80, 5],
              [81, 17],
              [84, 20],
              [84, 24],
              [84, 29],
              [84, 34],
              [84, 38],
              [84, 6],
              [107, 27]]
    original_cities = np.array(original_cities)
    dist_mat = np.zeros((len(original_cities), len(original_cities)))
    for i in range(len(original_cities)):
        for j in range(len(original_cities)):
            if i == j:
                dist_mat[i][j] = 100000
            else:
                dist_mat[i][j] = math.sqrt((original_cities[i][0] - original_cities[j][0]) ** 2 +
                                           (original_cities[i][1] - original_cities[j][1]) ** 2)

    # ALNS求解TSP
    time0 = time.time()
    calc_by_alns(dist_mat)
    print('使用ALNS求解TSP,耗时: {} s'.format(time.time() - time0))

运行代码后,可以在3分钟得到589的解,和最优解564之间的gap为4%。对比计算时间和最优解,整体上还是令人满意的。

另外,github上有个大佬也写了ALNS,据称可以在1分钟内得到574的解,和最优解之间的gap仅为2%。他代码的理解成本有些高,我大概学习了一下。部分我觉得不错的思路,已经用在了以上的代码中,建议刚入门ALNS的童鞋可以先看明白我的这篇文章,再去学习他的代码。

相关阅读

差分进化算法:https://mp.weixin.qq.com/s?__biz=MzIyMzc3MjIyMw==&mid=2247484871&idx=1&sn=defa15d216059b478bcd8b5cb2d97880&chksm=e8186e97df6fe781ebf62d1637826c22d675729f794c6675b886d2596b8acc2c4906381b5eba&token=1630762518&lang=zh_CN#rd

蚁群算法:https://mp.weixin.qq.com/s?__biz=MzIyMzc3MjIyMw==&mid=2247484883&idx=1&sn=2a25919d1a20b4783c1d79fdc91ee676&chksm=e8186e83df6fe795f0dc8dcc447f47082720b9e435e6bda0ff262f6575805ee6da2127a4a1e7&token=1630762518&lang=zh_CN#rd

LNS和ALNS:https://backend.orbit.dtu.dk/ws/portalfiles/portal/5293785/Pisinger.pdf

ALNS python版本_1: https://blog.csdn.net/qq_40894102/article/details/106794504

ALNS python版本_2: https://blog.csdn.net/weixin_46651999/article/details/113065064

大佬ALNS实践:https://github.com/N-Wouda/ALNS/blob/master/examples/travelling_salesman_problem.ipynb

菜鸟的车辆路径规划引擎:https://zhuanlan.zhihu.com/p/344773150

TSP测试集:https://www.math.uwaterloo.ca/tsp/vlsi/index.html#XQF131

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1200395.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git基本概念和使用方式

Git 是一种版本控制系统&#xff0c;用于管理文件版本的变化。以下是其基本概念和使用方式&#xff1a; 仓库&#xff08;repository&#xff09;&#xff1a;Git 存储代码的地方&#xff0c;可以理解为一个项目的文件夹。提交&#xff08;commit&#xff09;&#xff1a;Git …

【OpenCV实现图像:用OpenCV图像处理技巧之白平衡算法2】

文章目录 概要Gray-world AlgotithmGround Truth Algorithm结论&#xff1a; 概要 随着数字图像处理技术的不断发展&#xff0c;白平衡算法成为了图像处理中一个关键的环节。白平衡的目标是校正图像中的颜色偏差&#xff0c;使得白色在图像中呈现真实的白色&#xff0c;从而提…

Linux之基础开发工具gdb调试器的使用(三)

文章目录 一、Linux调试器-gdb使用1、安装gdb2、背景3、Debug和release4、区分Debug和release 二、Linux调试器-gdb命令演示1、显示指定行之后的代码&#xff08;自动记录最后一条指令&#xff09;2、断点1、打印断点2、查看断点3、删除断点4、使能&#xff08;禁用/开启&#…

统计分钟级别的视频在线用户数+列炸裂+repeat函数

统计分钟级别的视频在线用户数 1、原始数据如下&#xff1a; uid vid starttime endtime select aa as uid,v00l as vid,2023-10-25 12:00 as starttime,2023-10-2512:15 as endtime union select bb as uid,v002 as vid,2023-10-25 12:05 as starttime,2023-10-25 12:19 …

笔记:AI量化策略开发流程-基于BigQuant平台(二)

五、模型训练股票预测 完成了数据处理&#xff0c;接下来就可利用平台集成的各算法进行模型训练和模型预测啦。本文将详细介绍“模型训练”、“模型预测”两大模块操作、原理。 模型训练和模型预测是AI策略区别于传统量化策略的核心&#xff0c;我们通过模型训练模块利用训练…

为什么Android 手机这么慢?如何提高 Android 手机的运行速度

速印机&#xff08;理想、荣大等&#xff09;、复印机&#xff08;夏普、东芝、理光、佳能、震旦等全系列&#xff09;、打印机、扫描仪、传真机、多媒体教学一体机、交互式电子白板、报警器材、监控、竞业达监考设备及其它监考设备、听力考试设备、特种安防设备维护及维修。吴…

Linux必备:这十个流程图让你变的更强!

图是我们与信息联系并处理其重要性的绝佳方法&#xff1b;它们有助于传达关系和抽取信息&#xff0c;并使我们能够可视化概念。 从基本工作流程图到复杂的网络图&#xff0c;组织图&#xff0c;BPMN&#xff08;业务过程模型和符号&#xff09;&#xff0c;UML图等等&#xff0…

面试10000次依然会问的【ThreadLocal】,你还不会?

ThreadLocal简介与基本概念 ThreadLocal&#xff0c;即线程局部变量&#xff0c;是Java语言中用于实现线程数据隔离的一个重要类。这种机制允许在多线程环境中&#xff0c;每个线程都有自己的变量副本&#xff0c;从而使得每个线程都可以独立地改变自己的副本&#xff0c;而不…

JDK1.8 新特性(一)【默认方法、静态方法和Lambda表达式】

前言 今天学习Java8 新特性&#xff0c;主要是之前在学习 Scala、JavaFX 中遇到一些 Lambda 表达式&#xff0c;感觉 lambda 表达式确实很简洁&#xff0c;很有必要学一学。 目录 前言 1、接口的默认方法与静态方法 编写接口 编写接口的实现类 测试 2、Lambda表达式&am…

YOLO目标检测——交通标志分类数据集下载分享【含对应voc、coco和yolo三种格式标签】

实际项目应用&#xff1a;交通标志识别数据集在自动驾驶、交通安全监控、智能交通系统、驾驶员辅助系统和城市规划等领域都有广泛应用的潜力数据集说明&#xff1a;交通标志分类数据集&#xff0c;真实场景的高质量图片数据&#xff0c;数据场景丰富&#xff0c;含多场景白天黑…

Linux之gdb

gdb就是一个Linux的调试工具&#xff0c;类似与vs里面的调试 可执行程序也有格式&#xff0c;不是简单的二进制堆砌

PyCharm因安装了illuminated Cloud插件导致加载项目失败

打开Pycharm时会有弹窗提示&#xff1a; The license for Illuminated Cloud is invalid or has expired. All Illuminated Cloud features will be disabled. 这个弹窗会导致你加载项目一直失败&#xff0c;close project 也关不掉&#xff0c;我都是用任务管理器杀死进程的…

DevChat全能型AI编程助手,助你“以一敌三卷翻好友”

DevChat全能型AI编程助手&#xff0c;助你“以一敌三卷翻好友” 什么是DevChat&#xff0c;它能帮助我们做什么&#xff1f; DevChat是OpenAI的一个产品&#xff0c;它是一个可以进行编程相关对话的AI。这意味着你可以使用它来解决一些编程上的问题或者获取关于编程的建议。 …

Doris:读取Doris数据的N种方法

目录 1.MySQL Client 2.JDBC 3. 查询计划 4.Spark Doris Connector 5.Flink Doris Connector 1.MySQL Client Doris 采用 MySQL 协议&#xff0c;高度兼容 MySQL 语法&#xff0c;支持标准 SQL&#xff0c;用户可以通过各类客户端工具来访问 Doris。登录到doris服务器后&a…

基于SSM框架的高校试题管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;Vue 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#xff1a;是 目录…

交换机工作原理

交换机工作原理 交换机功能&#xff1a;端口扩展&#xff08;默认同一网络&#xff09;&#xff0c;如果只是两台设备进行通信&#xff0c;可以直接连接这两台设备而不用交换机&#xff0c;但如果设备较多&#xff0c;设备没有那么多接口&#xff0c;那么这个时候就需要交换机…

智慧水利整体解决方案:PPT全文43页,附下载

关键词&#xff1a;智慧水利发展前景&#xff0c;智慧水利解决方案&#xff0c;智慧水利建设方案&#xff0c;智慧水利平台系统 一、智慧水利建设背景 传统水利系统存在一些问题&#xff1a; 现有基础感知不能满足更高标准的水利管理需求&#xff1b;决策调度支撑能力亟需加强…

ztree结合hmap使用经验分享

项目背景 在建德封控拦截系统&#xff08;Vue3antd2.x&#xff09;为追求更快的地图初始化体验&#xff0c;在尝试了hmap2.5.0版本以及2.6.3版本后&#xff0c;由于这两个版本在现场电脑的初始化速度不够流畅&#xff0c;最终使用的是hmap2.1.3版本。同时由于布控选设备&#…

c语言练习第11周(1~5)

数列 1 1 2 3 5 8 13 21 ... 被称为斐波纳数列。 输入若干个正整数N&#xff0c;输出这个序列的前 N 项的和。 题干数列 1 1 2 3 5 8 13 21 ... 被称为斐波纳数列。 输入若干个正整数N&#xff0c;输出这个序列的前 N 项的和。输入样例3 5 4 1输出样例…

ftp服务器(filezilla服务端软件)下载、安装、使用

下载 通过360软件管家下载 输入filezilla&#xff0c;点击搜索&#xff0c;点击安装 修改安装路径 等待安装完成 配置服务端 启动配置 双击打开&#xff0c;点击软件中间按钮 不用输入密码&#xff0c;因为安装的时候没有设置密码 如果在安装的时候设置了密码&#xff0c;…