模拟退火算法 求解旅行商问题

在算法里面有一类问题叫做最优化问题,这类问题通常的特点是问题的解空间很大,用传统的算法没有办法穷举。
比如时间复杂度是O(N!)的问题,这类问题解空间会非常非常的大,比如在N=15的时候,就有1307674368000种可能性。

在用自己的笔记本上面计算了一下,求解15个元素的全排列,跑了没多久电脑就卡死了,然后在实验室的服务器上面试了一下,计算的用时大概是300s左右,内存占了60G左右。这已经是一个很大的开销了。

#如果你感兴趣的话,可以在本地跑一下,改变‘abcd’的大小,超过10以上,程序运行时间开始明显的上升
import itertools
for i in itertools.permutations('abcd',4):
        print (''.join(i))

对于这种复杂的组合优化问题,解空间往往非常大,如果暴力计算的话,往往是需要计算几十甚至是几百个元素的全排列。刚刚在计算15的全排列的时候,已经开销已经非常大了。对于,几十几百的全排列,是一个不可思议的计算量。

so,对于这类问题,一个常见的解法是利用组合优化的算法,比如爬山法,模拟退火算法,蚁群算法等等。这类算法的核心思想是:放弃遍历所有解的尝试,而是通过遍历一部分解空间,来找到一个接近全局最优解的一个近似最优解。

模拟退火算法

模拟退火算法是一种启发式寻优算法,其出发点是基于物理中固体物质的退火过程与一般组合优化问题之间的相似性。模拟退火算法从某一较高初温出发,伴随温度参数的不断下降,结合概率突跳特性在解空间中随机寻找目标函数的全局最优解,即在局部最优解能概率性地跳出并最终趋于全局最优。

模拟退火算法是通过赋予搜索过程一种时变且最终趋于零的概率突跳性,从而可有效避免陷入局部极小并最终趋于全局最优的串行结构的优化算法

《模拟退火算法 求解旅行商问题》 image.png

如果我们现在有上面这样一个函数图,如图二所示,现在想求函数的全局最优解。如果采用贪心策略,那么从A点开始试探,如果函数值继续减少,那么试探过程就会继续。而当到达点B时,显然我们的探求过程就结束了。最终,只能找打一个局部最后解B。

模拟退火算法的搜索过程引入了随机因素。模拟退火算法以一定的概率来接受一个比当前解要差的解,因此有可能会跳出这个局部的最优解,达到全局的最优解。以上图为例,模拟退火算法在搜索到局部最优解B后,会以一定的概率接受向右继续移动。也许经过几次这样的不是局部最优的移动后会到达B 和C之间的峰点,于是就跳出了局部最小值B。

根据Metropolis准则,粒子在温度T时趋于平衡的概率为exp(-ΔE/(kT)),其中E为温度T时的内能,ΔE为其改变数,k为Boltzmann常数。Metropolis准则常表示为

《模拟退火算法 求解旅行商问题》 image.png

Metropolis准则表明,在温度为T时,出现能量差为dE的降温的概率为P(dE),表示为:P(dE) = exp( dE/(kT) )。其中k是一个常数,exp表示自然指数,且dE<0。所以P和T正相关。这条公式就表示:温度越高,出现一次能量差为dE的降温的概率就越大;温度越低,则出现降温的概率就越小。又由于dE总是小于0(因为退火的过程是温度逐渐下降的过程),因此dE/kT < 0 ,所以P(dE)的函数取值范围是(0,1) 。随着温度T的降低,P(dE)会逐渐降低。

我们将一次向较差解的移动看做一次温度跳变过程,我们以概率P(dE)来接受这样的移动。也就是说,在用固体退火模拟组合优化问题,将内能E模拟为目标函数值 f,温度T演化成控制参数 t,即得到解组合优化问题的模拟退火演算法:由初始解 i 和控制参数初值 t 开始,对当前解重复“产生新解→计算目标函数差→接受或丢弃”的迭代,并逐步衰减 t 值,算法终止时的当前解即为所得近似最优解,这是基于蒙特卡罗迭代求解法的一种启发式随机搜索过程。

用模拟算法求解旅行商问题

有 N ( <=20 ) 台 PC 放在机房内,现在要求由你选定一台 PC,用共 N-1 条网线从这台机器开始一台接一台地依次连接他们,最后接到哪个以及连接的顺序也是由你选定的,为了节省材料,网线都拉直。求最少需要一次性购买多长的网线。

"""
模拟退火算法
"""
import numpy as np
import math
import matplotlib.pyplot as plt
import random
import copy

#calu_city_length 计算的是城市0到城市1,城市1到城市2,... ,城市18到城市19,城市19到城市0,组成一个环的距离
#为什么要这么连接?因为这里做了一个coding上面的调整,假设城市之间的连接关系是不能调整的,只能按照city0到city1...city18到city19,city19到city0的关系连接。但是,在后面调参的时候,需要调整的是城市之间的连接顺序
def calu_city_length(city):
    sum_distance=0
    for i in range(len(city)-1):
        x_diff=city[i][0][0]-city[i+1][0][0]
        y_diff=city[i][0][1]-city[i+1][0][1]
        distance= math.sqrt(x_diff*x_diff+y_diff*y_diff)
        sum_distance += distance
    #计算city[n-1]-city[0]的距离
    distance=math.sqrt( (city[len(city)-1][0][0]-city[0][0][0])**2 + (city[len(city)-1][0][1]-city[0][0][1])**2 )
    sum_distance+=distance
    return sum_distance

def show(city):
    x_list=[]
    y_list=[]
    for i in range(len(city)):
        x=city[i][0][0]
        y=city[i][0][1]
        x_list.append(x)
        y_list.append(y)
    plt.plot(x_list,y_list)
    plt.show()


def perturb(city):
    a=range(len(city))
    c1,c2=random.sample(a,2)
    temp_x=city[c1][0][0]
    temp_y=city[c1][0][1]
    city[c1][0][0]=city[c2][0][0]
    city[c1][0][1]=city[c2][0][1]   
    city[c2][0][0]=temp_x
    city[c2][0][1]=temp_y
    return city


def sa(city,temperature):
    cost=[]
    while temperature >0.001:
        for i in range(100):
            len1=calu_city_length(city)
            #扰动
            orignal_city=copy.deepcopy(city)
            #perturb 函数里面把city 已经修改了,结果是city与perturb_city是相等的
            perturb_city=perturb(city)
            len2=calu_city_length(perturb_city)

            delta=len2-len1
            if (delta <0):   #新路线好与旧路线
                city=perturb_city
            else:
                #以一定的概率接收较差的解
                r=random.random()
                if math.exp(((-delta)/temperature)) > r:
                    city=perturb_city
                else:
                    city=orignal_city
        temperature=float(temperature) * 0.99
        cost.append(calu_city_length(city))
    return city,cost            


if __name__=="__main__":
    #城市的个数
    n=20
    temperature=2000

    #随机初始化城市的坐标
    #所有的城市都是连通的,城市之间的距离就是坐标的距离
    city=np.random.randint(1,100,size=(n,1,2))

    #访问第20城市的坐标
    #坐标x:city[19][0][0]
    #坐标y:city[19][0][1]

    city,cost=sa(city,temperature)
    plt.plot(cost)
    plt.show()
    print (calu_city_length(city))  
运行结果:

如果把每一次的代价输出的话,那么结果如下,横坐标的迭代的次数,纵坐标是将所有城市连通的总代价。cost很明显的随着程序的运行的次数在下降,然后收敛在某个值。

《模拟退火算法 求解旅行商问题》 2017-09-24 20-53-21屏幕截图.png

PS:
在今年的数学建模里面,我们team就用的是这个算法,但是用之前一直不是很确定模拟退化的算法会不会在模型的算法里面生效。直到程序运行的结果出来,算法成功收敛!!!开心到爆炸!!!

参考资料:
https://zhuanlan.zhihu.com/p/23968011
http://blog.csdn.net/kai940325/article/details/43267917

    原文作者:zhaozhengcoder
    原文地址: https://www.jianshu.com/p/d365323a45d4
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞