禁忌搜索实践--29座城市旅行商问题

问题概述

给定29座城市, 求解旅行商从第一座城市开始, 访问每一座城市一次后, 回到起始城市的最短路径。

29座城市坐标依次如下:(360, 1980), (1150, 1760), (630, 1660), (40, 2090), (750, 1100), (750, 2030), (1030, 2070), (1650, 650), (1490, 1630), (790, 2260), (710, 1310), (840, 550), (1170, 2300), (970, 1340), (510, 700), (750, 900), (1280, 1200) , (230, 590), (460, 860), (1040, 950), (590, 1390), (830, 1770), (490, 500), (1840, 1240), (1260, 1500), (1280, 790), (490, 2130), (1460, 1420), (1260, 1910)

方法概述

我们不可能将所有情况全部列举出来, 寻求最小答案, 这样计算量实在太大。本文将分别采用局部搜索算法和禁忌搜索算法尝试解决TSP问题,并分析两种方法的利弊。

局部搜索算法

给定初始路径, 进行路径变换, 生成若干路径, 并从中选择比当前路径更短的路径, 重复迭代若干次, 直至找到的最短路径不变。

配图1.png

Python代码实现如下:

city = [(0, (360, 1980)), (1, (1150, 1760)), (2, (630, 1660)), (3, (40, 2090)), (4, (750, 1100)), 
    (5, (750, 2030)),(6, (1030, 2070)), (7, (1650, 650)), (8, (1490, 1630)), (9, (790, 2260)),
    (10, (710, 1310)),(11, (840, 550)), (12, (1170, 2300)), (13, (970, 1340)), (14, (510, 700)), 
    (15, (750, 900)),(16, (1280, 1200)), (17, (230, 590)), (18, (460, 860)), (19, (1040, 950)),
    (20, (590, 1390)),(21, (830, 1770)), (22, (490, 500)), (23, (1840, 1240)), (24, (1260, 1500)),
    (25, (1280, 790)),(26, (490, 2130)), (27, (1460, 1420)), (28, (1260, 1910))]

# 初始路径
road = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]
def cost_road(road):
    """
    通过两点间距离公式计算总路径距离,从城市0出发回到城市0
    :param road: 当前路径
    :return distance: 路径距离
    """
    
    distance = ((city[0][1][0]-city[road[0]][1][0])**2+(city[0][1][1]-city[road[0]][1][1])**2)**0.5
    for i in range(27):
        distance += ((city[road[i]][1][0]-city[road[i+1]][1][0])**2 +
                     (city[road[i]][1][1]-city[road[i+1]][1][1])**2)**0.5
    distance += ((city[road[27]][1][0]-city[0][1][0])**2+(city[road[27]][1][1]-city[0][1][1])**2)**0.5
    return distance

def neighbours(road):
    """
    通过交换当前路径中的两座城市生成领域路径,并从中挑选比当前路径更短的路径
    由于初始城市固定,所以共生成378种不同的路径
    :param road: 当前路径
    :return best_cost:最短距离, best_road:最短路径
    """
    
    length = len(road)
    roads = []
    best_cost = cost_road(road)
    best_road = road
    # 生成领域路径存放于roads
    for i in range(length):
        for j in range(i+1, length):
            temp = road[:]
            temp[i], temp[j] = temp[j], temp[i]
            roads.append(temp)
    # 搜寻最短距离,最短路径
    for each_road in roads:
        each_cost = cost_road(each_road)
        if each_cost < best_cost:
            best_cost = each_cost
            best_road = each_road
    return best_cost, best_road
# 循环搜索最短路径,直到最短路径,最短距离不发生变化
costs = []
while 1:
    cost, road = neighbours(road)
    costs.append(cost)
    if len(costs) > 3 and costs[-1] == costs[-2] == costs[-3]:
        break

通过以上代码很容易找到局部最优解[3, 2, 5, 26, 9, 12, 6, 28, 1, 8, 23, 7, 25, 11, 14, 22, 17, 18, 15, 4, 20, 10, 13, 19, 16, 27, 24, 21], 路径距离为10273.335954787268。显然这不是全局最优解, 例如选取[3, 26, 5, 9, 12, 6, 28, 1, 24, 16, 27, 8, 23, 7, 25, 19, 11, 22, 17, 14, 18, 15, 4, 13, 10, 20, 2, 21],计算路径距离为9111.60741390125。

禁忌搜索算法

相对比于局部搜索算法, 禁忌搜索算法通过接受第二短路径甚至第三、第四短路径而不是一直采纳最短路径,从而避免一直停留在局部最短路径。

本例中采用随机交换当前路径中两个城市40次,从而生成40种不同的领域路径,采取的禁忌表长度为3, 终止准则为迭代次数, 迭代次数为4000次。

配图2.png

Python代码实现如下:

import random

city = [(0, (360, 1980)), (1, (1150, 1760)), (2, (630, 1660)), (3, (40, 2090)), (4, (750, 1100)),
        (5, (750, 2030)), (6, (1030, 2070)), (7, (1650, 650)), (8, (1490, 1630)), (9, (790, 2260)),
        (10, (710, 1310)), (11, (840, 550)), (12, (1170, 2300)), (13, (970, 1340)), (14, (510, 700)),
        (15, (750, 900)), (16, (1280, 1200)), (17, (230, 590)), (18, (460, 860)), (19, (1040, 950)),
        (20, (590, 1390)), (21, (830, 1770)), (22, (490, 500)), (23, (1840, 1240)), (24, (1260, 1500)),
        (25, (1280, 790)), (26, (490, 2130)), (27, (1460, 1420)), (28, (1260, 1910))]

# 当前路径road
road = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28]

def cost_road(road):
    """
    通过两点间距离公式计算总路径距离,从城市0出发回到城市0
    :param road: 当前路径
    :return distance: 路径距离
    """
    
    distance = ((city[0][1][0]-city[road[0]][1][0])**2+(city[0][1][1]-city[road[0]][1][1])**2)**0.5
    for i in range(27):
        distance += ((city[road[i]][1][0]-city[road[i+1]][1][0])**2+
                     (city[road[i]][1][1]-city[road[i+1]][1][1])**2)**0.5
    distance += ((city[road[27]][1][0]-city[0][1][0])**2+(city[road[27]][1][1]-city[0][1][1])**2)**0.5
    return distance

def neighbours(road, tabu_list):
    """
    通过随机交换当前路径中两座城市的方式生成领域路径,
    本例共生成40种不同的路径,并依据路径距离从小到大排序,
    若领域中最短路径距离小于历史最优距离或,则更新当前路径和历史最优,
    若领域中最短路径大于历史最优但不位于禁忌表中,则更新当前路径,
    若最短路径位于禁忌表中,则考虑第二短路径......
    :param road: 当前路径
    :param tabu_list: 禁忌表
    :return road: 更新后的当前路径, best_road: 历史最优路径
    """

    roads = []
    path = road[:]
    move_list = []
    # best_cost: 历史最短距离, best_road: 历史最短路径
    global best_cost
    global best_road
    # 生成40种领域路径存放于roads中,并按距离排序
    for ix in range(40):
        move = random.sample(path, 2)
        move.sort()
        temp = road[:]
        if move in move_list:
            continue
        move_list.append(move)
        first = temp.index(move[0])
        second = temp.index(move[1])
        temp[first], temp[second] = temp[second], temp[first]
        cost = cost_road(temp)
        roads.append((move, cost, temp))
    roads.sort(key=lambda x: x[1])
    # 从领域中最短路径开始进行判断,是否更新历史最优和当前路径
    if roads[0][0] not in tabu_list or roads[0][1] < best_cost:
        if roads[0][1] < best_cost:
            best_cost = roads[0][1]
            best_road = roads[0][2]
        road = roads[0][2]
        tabu_list.append(roads[0][0])
    elif roads[1][0] not in tabu_list:
        road = roads[1][2]
        tabu_list.append(roads[1][0])
    elif roads[2][0] not in tabu_list:
        road = roads[2][2]
        tabu_list.append(roads[2][0])
    else:
        road = roads[3][2]          # 极少情况下,领域中前三种路径均位于禁忌表中,选取第四种路径
    # 维持禁忌表长度为3
    if len(tabu_list) > 3:
        tabu_list.pop(0)
    return road, best_road
# tabu_list: 禁忌表, best_road: 历史最短路径, best_cost: 历史最短距离
tabu_list = []
best_road = road
best_cost = cost_road(road)
# 循环迭代4000次,既搜寻终止条件为迭代次数耗尽
for ix in range(4000):
    road, best_road = neighbours(road, tabu_list)

由于生成领域路径的随机性, 每次运行算法都将得到不同的最短路径,我所运行后最短路径为[26, 9, 12, 6, 28, 1, 24, 16, 27, 8, 23, 7, 25, 19, 11, 17, 22, 14, 18, 15, 4, 13, 10, 20, 2, 21, 5, 3],最短距离为9300.9636202749。随着领域范围的改变,禁忌表长度的改变,迭代次数的改变,也将得到不同的最短路径。所以很难确定是否找到的路径一定是全局最短路径。

问题分析

通过以上两种方法的比较, 不难看出局部搜索算法的局限性很大,很容易陷入局部最优解;而禁忌搜索算法通过接受非最优解,从而达到更加宽广的搜索范围,避免停留在局部最优解。

但此种禁忌搜索算法更像是在碰运气找最优解,有时难免重复做出丢西瓜捡芝麻的决策,从而大幅度降低寻找最优解的效率,以至于迭代了成百上千次依然没有找到全局最优解。

你可能感兴趣的:(禁忌搜索实践--29座城市旅行商问题)