【建模算法】基于蚁群算法(ACA)求解TSP问题(Python实现)

TSP (traveling salesman problem,旅行商问题)是典型的NP完全问题,即其最坏情况下的时间复杂度随着问题规模的增大按指数方式增长,到目前为止还未找到一个多项式时间的有效算法。本文探讨了基于蚁群算法求解TSP问题的Python实现。

一、问题描述

本案例以31个城市为例,假定31个城市的位置坐标如表1所列。寻找出一条最短的遍历31个城市的路径。

城市编号

X坐标

Y坐标

城市编号

X坐标

Y坐标

1

1.304

2.312

17

3.918

2.179

2

3.639

1.315

18

4.061

2.37

3

4.177

2.244

19

3.78

2.212

4

3.712

1.399

20

3.676

2.578

5

3.488

1.535

21

4.029

2.838

6

3.326

1.556

22

4.263

2.931

7

3.238

1.229

23

3.429

1.908

8

4.196

1.044

24

3.507

2.376

9

4.312

0.79

25

3.394

2.643

10

4.386

0.57

26

3.439

3.201

11

3.007

1.97

27

2.935

3.24

12

2.562

1.756

28

3.14

3.55

13

2.788

1.491

29

2.545

2.357

14

2.381

1.676

30

2.778

2.826

15

1.332

0.695

31

2.37

2.975

16

3.715

1.678

二、蚁群算法简介

2.1 蚁群算法基本原理

1、蚂蚁在行走过程中会依据信息素来选择道路,选择信息素较浓的路走,并且在行走的路径中会释放信息素,对于所有蚂蚁都没经过的路,则随机选择一条路走;
2、蚂蚁释放的信息素浓度与长度相关,通常与路径长度成反比;
3、信息素浓的路径会受到蚂蚁更大概率的选择,形成正向反馈,最短路径上的信息素浓度会越来越大,最终蚁群就都按这条最短路径走。

信息素计算公式、转移概率、信息素重要程度因子、启发函数重要程度因子、信息素挥发因子等详细介绍可参考"蚁群算法详细讲解"和“TSP解决之道——蚁群算法

2.2 算法的两个关键步骤

1、选择:为蚂蚁选择下一个城市,信息素越多的路径被选中概率较大,可用轮盘赌算法实现;
2、信息素更新:一段时间后(蚂蚁走完一个城市或者走完整个路径后)重新计算信息素(计算方法:历史累计信息素-信息素挥发量+蚂蚁行走释放量),蚂蚁行走释放量的常见方法有三种:蚁周算法(ant-cycle,蚂蚁走完整个路径后,蚂蚁行走释放部分用Q/L计算,Q表示蚂蚁释放信息素的量,为常量,L表示路径总长度)、蚁密算法(ant-density,蚂蚁走完一个城市后,蚂蚁行走释放用Q表示)、蚁量算法(ant-quantity,蚂蚁走完一个城市后,蚂蚁行走释放用Q/dij表示,dij表示城市i和j之间的距离)。

三、蚁群算法设计

在本算法设计中,包含两层循环,外循环是迭代次数循环,内循环遍历每一只蚂蚁,其中信息素增量在每一只蚂蚁走完整体路径后对当前信息素更新,而信息素挥发只在每一代蚂蚁都走完后对当前信息素更新,流程如下:

python求解大型TSP问题 python tsp问题_python

四、求解结果

最优路线与最优值:

python求解大型TSP问题 python tsp问题_python_02


最优轨迹图:

python求解大型TSP问题 python tsp问题_python_03

五、Python源代码

#蚁群算法求解31座城市TSP问题完整代码:
import numpy as np
import pandas as pd
import math
import matplotlib.pyplot as plt
from time import perf_counter

class AntList(object):
    def __init__(self,distfunc,getEtatable,numant=5,numcity=10,alpha=1,rho=0.1,Q=1):
        """ 构造函数 """
        self.numant = numant        # 蚂蚁个数
        self.numcity = numcity        # 城市个数
        self.alpha = alpha            # 信息素重要程度因子
        self.rho = rho                # 信息素的挥发速度
        self.Q = Q                    # 品质因子
        self.distfunc=distfunc
        self.getEtatable=getEtatable

        self.bestantunit=None
        self.population=[]
        self.pathtable = np.zeros((self.numant,self.numcity)).astype(int)    # 路径记录表
        self.generation=0

    def Init_eta_phe(self):
        """
            函数名:Init_eta_phe(self)
            函数功能:    对启发函数和信息素进行初始化
        """
        self.etatable = self.getEtatable()            # 启发函数矩阵,表示蚂蚁从城市i转移到矩阵j的期望程度
        self.pheromonetable  = np.ones((self.numcity,self.numcity))            # 信息素矩阵

    def InitStartPosition(self):
        """
            函数名:InitStartPosition(self)
            函数功能:    初始化蚂蚁的起始位置
        """
        #  随机产生各个蚂蚁的起点城市
        if self.numant <= self.numcity:       # 城市数比蚂蚁数多
            self.pathtable[:,0] = np.random.permutation(range(0,self.numcity))[:self.numant]
        else:                               # 蚂蚁数比城市数多,需要补足
            self.pathtable[:self.numcity,0] = np.random.permutation(range(0,self.numcity))[:]
            self.pathtable[self.numcity:,0] = np.random.permutation(range(0,self.numcity))[:self.numant-self.numcity]

    def upDateInf(self):
        """
            函数名:upDateInf(self)
            函数功能:    对信息素进行更新
        """
        changepheromonetable = np.zeros((self.numcity,self.numcity))
        
        if self.population:
            for antunit in self.population:
                for i in range(self.numcity-1):
                    changepheromonetable[antunit.path[i]][antunit.path[i+1]] += self.Q/antunit.length
                changepheromonetable[antunit.path[self.numcity-1]][antunit.path[0]] += self.Q/antunit.length
            self.pheromonetable = (1-self.rho)*self.pheromonetable + changepheromonetable
        else:
            self.Init_eta_phe()

    def getNextCity(self,unvisited,visiting):
        """
            函数名:getNextCity(self,unvisited,visiting)
            函数功能:    根据信息素和启发函数矩阵,通过轮盘赌法随机选下一个城市
                输入    1     self:类自身
                输入 2    unvisited:未走过的城市列表
                输入 2    visited:已经走过的城市列表
                输出    1    k:下一个城市的编号
            其他说明:无
        """
        listunvisited = list(unvisited)
        probtrans = np.zeros(len(listunvisited))

        for k in range(len(listunvisited)):
            probtrans[k] = np.power(self.pheromonetable[visiting][listunvisited[k]],self.alpha)\
                *np.power(self.etatable[visiting][listunvisited[k]],self.alpha)

        cumsumprobtrans = (probtrans/sum(probtrans)).cumsum()
        cumsumprobtrans -= np.random.rand()

        k = listunvisited[np.where(cumsumprobtrans>0)[0][0]] # 下一个要访问的城市
        return k

    def GoOnePath(self,i):
        """
            函数名:distance(self, path)
            函数功能:    第i只蚂蚁从随机点出发找到一条路径
                输入    1     self:类自身
                输入 2    i:当代的第i只蚂蚁
                输出    1    antunit:一个蚂蚁单元类
            其他说明:无
        """
        visiting = self.pathtable[i,0]        # 当前所在的城市

        unvisited = set(range(self.numcity))# 未访问的城市
        unvisited.remove(visiting)            # 删除元素
        
        for j in range(1,self.numcity):        # 循环numcity-1次,访问剩余的numcity-1个城市
            # 每次用轮盘法选择下一个要访问的城市
            k=self.getNextCity(unvisited,visiting)
            
            self.pathtable[i,j] = k
            unvisited.remove(k)
            visiting = k
        
        antunit=AntUnit(self.pathtable[i],self.distfunc(self.pathtable[i]))
        if self.bestantunit:
            if self.bestantunit.length>antunit.length:
                self.bestantunit=antunit
        else:
            self.bestantunit=antunit
        
        return antunit

    def nextGeneration(self):
        """
            函数名:nextGeneration(self)
            函数功能:    产生下一代
        """
        self.upDateInf()
        newPopulation = []                        # 新种群

        for i in range(self.numant):
            newPopulation.append(self.GoOnePath(i))

        self.population = newPopulation
        self.generation += 1

class AntUnit(object):
    """
        类名:GAUnit
        类说明:    遗传算法个体类
    """
    def __init__(self, aPath = None,aLength = -1):
        """ 构造函数 """
        self.path = list(aPath)            # 个体的基因序列
        self.length = aLength              # 初始化适配值

class Node:
    """
        类名:Node
        类说明:    城市节点类
    """
    def __init__(self,CityNum):
        """
        函数名:GetData()
        函数功能:    从外界读取城市数据并处理
            输入    无
            输出    1 Position:各个城市的位置矩阵
                2 CityNum:城市数量
                3 Dist:城市间距离矩阵
        其他说明:无
        """
        self.visited=[False]*CityNum    #记录城市是否走过
        self.start=0                    #起点城市
        self.end=0                      #目标城市
        self.current=0                  #当前所处城市
        self.num=0                      #走过的城市数量
        self.pathsum=0                  #走过的总路程
        self.lb=0                       #当前结点的下界
        self.listc=[]                   #记录依次走过的城市

def GetData(datapath):
    """
    函数名:GetData()
    函数功能:    从外界读取城市数据并处理
        输入    无
        输出    1 Position:各个城市的位置矩阵
            2 CityNum:城市数量
            3 Dist:城市间距离矩阵
    其他说明:无
    """
    dataframe = pd.read_csv(datapath,sep=" ",header=None)
    Cities = dataframe.iloc[:,1:3]
    Position= np.array(Cities)                #从城市A到B的距离矩阵
    CityNum=Position.shape[0]                #CityNum:代表城市数量
    Dist = np.zeros((CityNum,CityNum))        #Dist(i,j):城市i与城市j间的距离

    #计算距离矩阵
    for i in range(CityNum):
        for j in range(CityNum):
            if i==j:
                Dist[i,j] = math.inf
            else:
                Dist[i,j] = math.sqrt(np.sum((Position[i,:]-Position[j,:])**2))
    return Position,CityNum,Dist

def ResultShow(Min_Path,BestPath,CityNum,string):
    """
        函数名:GetData()
        函数功能:    从外界读取城市数据并处理
            输入    无
            输出    1 Position:各个城市的位置矩阵
                2 CityNum:城市数量
                3 Dist:城市间距离矩阵
        其他说明:无
    """    
    print("基于"+string+"求得的最优路线:")
    for m in range(CityNum):
        print(str(BestPath[m])+"—>",end="")
    print(BestPath[CityNum])
    print("最优值:"+str(Min_Path))
    print()

def draw(BestPath,Position,title):
    """
        函数名:draw(BestPath,Position,title)
        函数功能:    通过最优路径将旅行商依次经过的城市在图表上绘制出来
            输入    1     BestPath:最优路径
                2    Position:各个城市的位置矩阵
                3    title:图表的标题
            输出    无
        其他说明:无
    """
    plt.rcParams['font.sans-serif'] = 'SimHei'  # 设置中文显示
    plt.rcParams['axes.unicode_minus'] = False
    plt.title(title)
    plt.plot(Position[BestPath, 0],Position[BestPath, 1], marker='>', mec='r', mfc='w',label=u'路线')
    plt.legend()  # 让图例生效
    for i,city in enumerate(Position): 
        plt.text(city[0], city[1], str(i))
    plt.xlabel('横坐标')
    plt.ylabel('纵坐标')
    plt.show()

def ant():
    """
        函数名:ant()
        函数功能:蚁群算法核心
    """
    numant = 25          # 蚂蚁个数
    numcity = CityNum   # 城市个数
    alpha = 1           # 信息素重要程度因子
    rho = 0.1           # 信息素的挥发速度
    Q = 1
    
    iters = 0
    itermax = 500
    
    etatable = 1.0/(Dist+np.diag([1e10]*numcity))       # 启发函数矩阵,表示蚂蚁从城市i转移到矩阵j的期望程度
    pheromonetable  = np.ones((numcity,numcity))        # 信息素矩阵
    pathtable = np.zeros((numant,numcity)).astype(int)  # 路径记录表
    
    lengthaver = np.zeros(itermax)          # 各代路径的平均长度
    lengthbest = np.zeros(itermax)          # 各代及其之前遇到的最佳路径长度
    pathbest = np.zeros((itermax,numcity))  # 各代及其之前遇到的最佳路径

    while iters < itermax:
        #  随机产生各个蚂蚁的起点城市
        if numant <= numcity:   # 城市数比蚂蚁数多
            pathtable[:,0] = np.random.permutation(range(0,numcity))[:numant]
        else:                   # 蚂蚁数比城市数多,需要补足
            pathtable[:numcity,0] = np.random.permutation(range(0,numcity))[:]
            pathtable[numcity:,0] = np.random.permutation(range(0,numcity))[:numant-numcity]
        
        length = np.zeros(numant)       # 计算各个蚂蚁的路径距离
        
        for i in range(numant): 
            visiting = pathtable[i,0]         # 当前所在的城市
            unvisited = set(range(numcity))   # 未访问的城市
            unvisited.remove(visiting)        # 删除元素
            
            for j in range(1,numcity):        # 循环numcity-1次,访问剩余的numcity-1个城市
                # 每次用轮盘法选择下一个要访问的城市
                listunvisited = list(unvisited)
                probtrans = np.zeros(len(listunvisited))
                
                for k in range(len(listunvisited)):
                    probtrans[k] = np.power(pheromonetable[visiting][listunvisited[k]],alpha)\
                        *np.power(etatable[visiting][listunvisited[k]],alpha)
                cumsumprobtrans = (probtrans/sum(probtrans)).cumsum()
                cumsumprobtrans -= np.random.rand()
                
                k = listunvisited[np.where(cumsumprobtrans>0)[0][0]] # 下一个要访问的城市
                pathtable[i,j] = k
                unvisited.remove(k)
                length[i] += Dist[visiting][k]
                visiting = k
            
            length[i] += Dist[visiting][pathtable[i,0]] # 蚂蚁的路径距离包括最后一个城市和第一个城市的距离
        
        
        # 包含所有蚂蚁的一个迭代结束后,统计本次迭代的若干统计参数
        lengthaver[iters] = length.mean()
        if iters == 0:
            lengthbest[iters] = length.min()
            pathbest[iters] = pathtable[length.argmin()].copy()      
        else:
            if length.min() > lengthbest[iters-1]:
                lengthbest[iters] = lengthbest[iters-1]
                pathbest[iters] = pathbest[iters-1].copy()
            else:
                lengthbest[iters] = length.min()
                pathbest[iters] = pathtable[length.argmin()].copy()    
        
        # 更新信息素
        changepheromonetable = np.zeros((numcity,numcity))
        for i in range(numant):
            for j in range(numcity-1):
                changepheromonetable[pathtable[i,j]][pathtable[i,j+1]] += Q/length[i]
            changepheromonetable[pathtable[i,j+1]][pathtable[i,0]] += Q/length[i]
        pheromonetable = (1-rho)*pheromonetable + changepheromonetable
        
        # 迭代次数指示器+1
        iters += 1 

    path_tmp=pathbest[-1]
    BestPath=[]
    for i in path_tmp:
        BestPath.append(int(i))
    BestPath.append(BestPath[0])
    
    return BestPath,lengthbest[-1]

#########程序入口#########################################
if __name__ == "__main__":
    Position,CityNum,Dist = GetData("data.txt")
    
    start=perf_counter()       #计时开始
    BestPath,Min_Path = ant()
    print("运行时间是: {:.5f}s".format(perf_counter()-start))            # 程序计时结束
    
    print()
    ResultShow(Min_Path,BestPath,CityNum,"蚁群算法")
    draw(BestPath,Position,"轨迹图")