更多精彩尽在微信公众号【程序猿声】

01 什么是旅行商问题(TSP)?

TSP问题(Traveling Salesman Problem,旅行商问题),由威廉哈密顿爵士和英国数学家克克曼T.P.Kirkman于19世纪初提出。问题描述如下:
有若干个城市,任何两个城市之间的距离都是确定的,现要求一旅行商从某城市出发必须经过每一个城市且只在一个城市逗留一次,最后回到出发的城市,问如何事先确定一条最短的线路已保证其旅行的费用最少?

如下图所示:

02 模拟退火算法(Simulate Annealing Arithmetic,SAA)

2.1 什么是模拟退火算法(简介)?

模拟退火算法(Simulate Annealing Arithmetic,SAA)是一种通用概率演算法,用来在一个大的搜寻空间内找寻命题的最优解。它是基于Monte-Carlo迭代求解策略的一种随机寻优算法。

模拟退火算法是S.Kirkpatrick, C.D.Gelatt和M.P.Vecchi等人在1983年发明的,1985年,V.Černý也独立发明了此演算法。模拟退火算法是解决TSP问题的有效方法之一。

2.2 模拟退火算法的来源

模拟退火算法来源于固体退火原理。

物理退火: 将材料加热后再经特定速率冷却,目的是增大晶粒的体积,并且减少晶格中的缺陷。材料中的原子原来会停留在使内能有局部最小值的位置,加热使能量变大,原子会离开原来位置,而随机在其他位置中移动。退火冷却时速度较慢,使得原子有较多可能可以找到内能比原先更低的位置。

模拟退火: 其原理也和固体退火的原理近似。模拟退火算法从某一较高初温出发,伴随温度参数的不断下降,结合概率突跳特性在解空间中随机寻找目标函数的全局最优解,即在局部最优解能概率性地跳出并最终趋于全局最优。

2.3 模拟退火算法思想

在介绍模拟退火算法之前,有必要给大家科普一下爬山算法 (Hill Climbing)。

2.3.1 爬山算法

爬山算法是一种简单的贪心搜索算法,该算法每次从当前解的临近解空间中选择一个最优解作为当前解,直到达到一个局部最优解。这种算法思想很单纯,但是也存在一个很大的缺陷。在搜索选择的过程中有可能会陷入局部最优解,而这个局部最优解不一定是全局最优解。比如下面这个问题:

假设A是当前解,爬山算法往前继续搜索,当搜索到B这个局部最优解时就会停止搜索了。因为此时在B点无论是往哪边走都不会得到更优的解了。但是,聪明的同学已经发现了,全局最优解在C点。

2.3.2 模拟退火算法

爬山法是完完全全的贪心法,这种贪心是很鼠目寸光的,只把眼光放在局部最优解上,因此只能搜索到局部的最优值。模拟退火其实也是一种贪心算法,只不过与爬山法不同的是,模拟退火算法在搜索过程引入了随机因素。模拟退火算法以一定的概率来接受一个比当前解要差的解,因此有可能会跳出这个局部的最优解,达到全局的最优解。从上图来说,模拟退火算法在搜索到局部最优解B后,会以一定的概率接受向右的移动。也许经过几次这样的不是局部最优的移动后会到达BC之间的峰点D,这样一来便跳出了局部最优解B,继续往右移动就有可能获得全局最优解C。如下图:

关于普通Greedy算法与模拟退火,这里也有一个有趣的比喻:

普通Greedy算法:兔子朝着比现在低的地方跳去。它找到了不远处的最低的山谷。但是这座山谷不一定最低的。

模拟退火:兔子喝醉了。它随机地跳了很长时间。这期间,它可能走向低处,也可能踏入平地。但是,它渐渐清醒了并朝最低的方向跳去。

如此一来,大家对模拟退火算法有了一定的认识,但是这还是不够的。对比上面两种算法,对于模拟退火算法我们提到了一个很important的概念--一定的概率,关于这个一定的概率是如何计算的。这里还是参考了固体的物理退火过程。

根据热力学的原理,在温度为T时,出现能量差为dE的降温的概率为P(dE),表示为:

P(dE) = exp( dE/(kT) )

其中k是一个常数,exp表示自然指数,且dE<0(温度总是降低的)。这条公式指明了:
1) 温度越高,出现一次能量差为dE的降温的概率就越大;
2) 温度越低,则出现降温的概率就越小。又由于dE总是小于0(不然怎么叫退火),因此dE/kT < 0 ,exp(dE/kT)取值是(0,1),那么P(dE)的函数取值范围是(0,1) 。

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

因此我们归结起来就是以下几点:
1) 若f( Y(i+1) ) <= f( Y(i) ) (即移动后得到更优解),则总是接受该移动;
2) 若f( Y(i+1) ) > f( Y(i) ) (即移动后的解比当前解要差),则以一定的概率接受移动,而且这个概率随着时间推移逐渐降低(逐渐降低才能趋向稳定)相当于上图中,从B移向BC之间的小波峰D时,每次右移(即接受一个更糟糕值)的概率在逐渐降低。如果这个坡特别长,那么很有可能最终我们并不会翻过这个坡。如果它不太长,这很有可能会翻过它,这取决于衰减 t 值的设定。

2.4 模拟退火算法伪代码

相信通过上面的讲解,大家已经对模拟退火算法认识得差不多了。下面我们来看看它的伪代码是怎么实现的。

03 使用模拟退火算法解决旅行商问题

旅行商问题属于所谓的NP完全问题。精确的解决TSP只能通过穷举所有的路径组合,其时间复杂度是O(N!) 。而使用模拟退火算法则可以较快速算法一条近似的最优路径。大体的思路如下:

  1. 产生一条新的遍历路径P(i+1),计算路径P(i+1)的长度L( P(i+1) )
  2. 若L(P(i+1)) < L(P(i)),则接受P(i+1)为新的路径,否则以模拟退火的那个概率接受P(i+1) ,然后降温
  3. 重复步骤1,2直到满足退出条件

好了多说无益,下面大家一起看代码吧。基于中国31个城市跑的。

/*
 * 使用模拟退火算法(SA)求解TSP问题(以中国TSP问题为例)
 * 参考自《Matlab 智能算法30个案例分析》
 */
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<time.h>
#include<math.h>
#define T0 50000.0  // 初始温度
#define T_end (1e-8)
#define q  0.98   // 退火系数
#define L 1000  // 每个温度时的迭代次数,即链长
#define N 31  // 城市数量
int city_list[N]; // 用于存放一个解 // 中国31个城市坐标
double city_pos[N][2] =
    {
    {1304,2312},{3639,1315},{4177,2244},{3712,1399},
    {3488,1535},{3326,1556},{3238,1229},{4196,1004},
    {4312,790},{4386,570},{3007,1970},{2562,1756},
    {2788,1491},{2381,1676},{1332,695},
    {3715,1678},{3918,2179},{4061,2370},
    {3780,2212},{3676,2578},{4029,2838},
    {4263,2931},{3429,1908},{3507,2367},
    {3394,2643},{3439,3201},{2935,3240},
    {3140,3550},{2545,2357},{2778,2826},
    {2370,2975}}; //函数声明
double distance(double *,double *); // 计算两个城市距离
double path_len(int *);  // 计算路径长度
void  init();  //初始化函数
void create_new(); // 产生新解
// 距离函数
double distance(double * city1,double * city2)
{
    double x1 = *city1;
    double y1 = *(city1+1);
    double x2 = *(city2);
    double y2 = *(city2+1);
    double dis = sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
    return dis;
} // 计算路径长度
double path_len(int * arr)
{
    double path = 0; // 初始化路径长度
    int index = *arr; // 定位到第一个数字(城市序号)
    for(int i=0;i<N-1;i++)
    {
        int index1 = *(arr+i);
        int index2 = *(arr+i+1);
        double dis = distance(city_pos[index1-1],
                                city_pos[index2-1]);         path += dis;
    }
    int last_index = *(arr+N-1); // 最后一个城市序号
    int first_index = *arr; // 第一个城市序号
    double last_dis = distance(city_pos[last_index-1],
                                city_pos[first_index-1]);
    path = path + last_dis;
    return path; // 返回总的路径长度
} // 初始化函数
void init()
{
    for(int i=0;i<N;i++)
        city_list[i] = i+1;  // 初始化一个解
} // 产生一个新解
// 此处采用随机交换两个位置的方式产生新的解
void create_new()
{
    double r1 = ((double)rand())/(RAND_MAX+1.0);
    double r2 = ((double)rand())/(RAND_MAX+1.0);
    int pos1 = (int)(N*r1); //第一个交叉点的位置
    int pos2 = (int)(N*r2);
    int temp = city_list[pos1];
    city_list[pos1] = city_list[pos2];
    city_list[pos2] = temp;   // 交换两个点
} // 主函数
int main(void)
{
    srand((unsigned)time(NULL)); //初始化随机数种子
    time_t start,finish;
    start = clock(); // 程序运行开始计时
    double T;
    int count = 0; // 记录降温次数
    T = T0; //初始温度
    init(); //初始化一个解
    int city_list_copy[N]; // 用于保存原始解
    double f1,f2,df; //f1为初始解目标函数值,
                     //f2为新解目标函数值,df为二者差值     double r; // 0-1之间的随机数,用来决定是否接受新解
    while(T > T_end) // 当温度低于结束温度时,退火结束
    {
        for(int i=0;i<L;i++)
        {
            // 复制数组
            memcpy(city_list_copy,city_list,N*sizeof(int));
            create_new(); // 产生新解
            f1 = path_len(city_list_copy);
            f2 = path_len(city_list);
            df = f2 - f1;
            // 以下是Metropolis准则
            if(df >= 0)
            {
                r = ((double)rand())/(RAND_MAX);
                if(exp(-df/T) <= r) // 保留原来的解
                {
                    memcpy(city_list,city_list_copy,N*sizeof(int));
                }
            }
        }
        T *= q; // 降温
        count++;
    }
    finish = clock(); // 退火过程结束
    double duration = ((double)(finish-start))/CLOCKS_PER_SEC; // 计算时间
    printf("模拟退火算法,初始温度T0=%.2f,降温系数q=%.2f,每个温度迭代%d次,共降温%d次,得到的TSP最优路径为:\n",T0,q,L,count);
    for(int i=0;i<N-1;i++)  // 输出最优路径
    {
        printf("%d--->",city_list[i]);
    }
    printf("%d\n",city_list[N-1]);
    double len = path_len(city_list); // 最优路径长度
    printf("最优路径长度为:%lf\n",len);
    printf("程序运行耗时:%lf秒.\n",duration);
    return 0;
}

代码运行结果:

04 小结

从上面的过程我们可以看出,模拟退火算法是一种随机算法,它有一定的概率能求得全局最优解,但不一定。用模拟退火算法可以较快速地找出问题的最优近似解。它的关键之处还是在于允许一定的差解。不过,在小编不成熟的眼光看来,人生亦有相似之处。有时候可能放弃眼前短浅的利益,最终才可能获得更好的未来。现在失去的,在未来会以另一种方式归来。

【智能算法】用模拟退火(SA, Simulated Annealing)算法解决旅行商问题 (TSP, Traveling Salesman Problem)的更多相关文章

  1. 多线程动态规划算法求解TSP(Traveling Salesman Problem) 并附C语言实现例程

    TSP问题描述: 旅行商问题,即TSP问题(Travelling Salesman Problem)又译为旅行推销员问题.货郎担问题,是数学领域中著名问题之一.假设有一个旅行商人要拜访n个城市,他必须 ...

  2. 模拟退火 Simulated annealing

    模拟退火 Simulated annealing 看看有空把图片完善一下好了 模拟退火算法的一些背景 既然要说模拟退火算法,就应该说一下模拟退火算法的背景,模拟退火算法是局部搜索算法的一种扩展,该算法 ...

  3. 数据挖掘算法学习(三)NaiveBayes算法

    算法简单介绍 NBC是应用最广的分类算法之中的一个.朴素贝叶斯模型发源于古典数学理论,有着坚实的数学基础,以及稳定的分类效率.同一时候,NBC模型所需预计的參数非常少,对缺失数据不太敏感,算法也比較简 ...

  4. Monte Carlo simulated annealing

    蒙特·卡罗分子模拟计算 使用蒙特·卡罗方法进行分子模拟计算是按照以下步骤进行的: 1. 使用随机数发生器产生一个随机的分子构型. 2. 对此分子构型的其中粒子坐标做无规则的改变,产生一个新的分子构型. ...

  5. 浅析负载均衡的6种算法,Ngnix的5种算法。

    浅析负载均衡的6种算法,Ngnix的5种算法.浮生偷闲百家号03-21 10:06关注内容导读其实际效果越来越接近于平均分配调用量到后端的每一台服务器,也就是轮询的结果.源地址哈希的思想是根据获取客 ...

  6. Machine Learning读书会,面试&算法讲座,算法公开课,创业活动,算法班集锦

    Machine Learning读书会,面试&算法讲座,算法公开课,创业活动,算法班集锦 近期活动: 2014年9月3日,第8次西安面试&算法讲座视频 + PPT 的下载地址:http ...

  7. 数据结构与算法JavaScript (五) 串(经典KMP算法)

    KMP算法和BM算法 KMP是前缀匹配和BM后缀匹配的经典算法,看得出来前缀匹配和后缀匹配的区别就仅仅在于比较的顺序不同 前缀匹配是指:模式串和母串的比较从左到右,模式串的移动也是从 左到右 后缀匹配 ...

  8. 【原创】机器学习之PageRank算法应用与C#实现(1)算法介绍

    考虑到知识的复杂性,连续性,将本算法及应用分为3篇文章,请关注,将在本月逐步发表. 1.机器学习之PageRank算法应用与C#实现(1)算法介绍 2.机器学习之PageRank算法应用与C#实现(2 ...

  9. EM算法(4):EM算法证明

    目录 EM算法(1):K-means 算法 EM算法(2):GMM训练算法 EM算法(3):EM算法运用 EM算法(4):EM算法证明 EM算法(4):EM算法证明 1. 概述 上一篇博客我们已经讲过 ...

随机推荐

  1. Keepalived使用小结

    编译安装 1.安装环境 CentOS release 6.4 Based on Linux 2.6.32,安装1.2.9,没问题 在Red Hat Enterprise Linux Server re ...

  2. Asp.NetCore远程自启动、重启、关闭实现

    一.背景 NetCore作为微服务可以注册到服务中心,服务中心可以远程启动.重启.关闭该微服务 二.实现 1.创建一个NetCore 2.0 WebApi项目 2.创建一个进程去管理NetCore程序 ...

  3. Vulkan Tutorial 03 理解Instance

    操作系统:Windows8.1 显卡:Nivida GTX965M 开发工具:Visual Studio 2017 Creating an instance 与Vulkan打交道,通常的步骤是创建一个 ...

  4. s=1+2*3+4*5*6+7*8*9*10+.... C语言求解

    #include <stdio.h> /*类似斐波那契数列的计算方式 项 1 2 3 4 1 2*3 4*5*6 7*8*9*10 生成项的起始数字 1 2 4 7 和后一项的差值 1 2 ...

  5. dyld_decache&MesaSQLite

    [dyld_decache] Starting from iPhone OS 3.1, the individual libraries files supplied by the system ar ...

  6. 【HDU1573】X问题

    [题目描述] 求在小于等于N的正整数中有多少个X满足:X mod a[0] = b[0], X mod a[1] = b[1], X mod a[2] = b[2], …, X mod a[i] = ...

  7. libevent源码深度剖析十

    libevent源码深度剖析十 ——支持I/O多路复用技术 张亮 Libevent的核心是事件驱动.同步非阻塞,为了达到这一目标,必须采用系统提供的I/O多路复用技术,而这些在Windows.Linu ...

  8. java实现微信H5支付

    前面做了app微信支付的回调处理,现在需要做微信公众号的支付,花了一天多时间,终于折腾出来了!鉴于坑爹的微信官方没有提供Java版的demo,所以全靠自己按照同样坑爹的文档敲敲敲,所以记录下来,以供自 ...

  9. Python中sort与sorted函数

    python中列表的内置函数sort()可以对列表中的元素进行排序,而全局性的sorted()函数则对所有可迭代的序列都是适用的: 并且sort()函数是内置函数,会改变当前对象,而sorted()函 ...

  10. CF570D Tree Requests

    离线 + 树状数组 如果子树中的一个深度的所有点中有两个以上的字母出现了奇数次,那么这个询问的答案就是$No$,其他的情况吧都是$Yes$. 由于只有$26$个字母,我们可以考虑暴力检验,把树映射到$ ...