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

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个城市跑的。

  1. /*
     * 使用模拟退火算法(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]; // 用于存放一个解
  2. // 中国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}};
  3. //函数声明
    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;
    }
  4. // 计算路径长度
    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]);
  5.         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; // 返回总的路径长度
    }
  6. // 初始化函数
    void init()
    {
        for(int i=0;i<N;i++)
            city_list[i] = i+1;  // 初始化一个解
    }
  7. // 产生一个新解
    // 此处采用随机交换两个位置的方式产生新的解
    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;   // 交换两个点
    }
  8. // 主函数
    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为二者差值
  9.     double r; // 0-1之间的随机数,用来决定是否接受新解
        while(> 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. Android 4 学习(16):Database and Content Providers

    参考<Professional Android 4 Development> Database and Content Providers Android Database简介 Andro ...

  2. 关于Bootstrap的整理和理解

    随着CSS3和HTML5的流行,我们的WEB页面不仅需要更人性化的设计理念,而且需要更酷的页面特效和用户体验.作为开发者,我们需要了解一些宝贵的CSS UI开源框架资源,它们可以帮助我们更快更好地实现 ...

  3. IDE 文件查找快捷键被占

    快捷键 Ctrl+Shift+F 被搜狗输入法抢占了. 打开搜狗输入法里的快捷键设置,去掉,my ide is ok!

  4. Hadoop IO 特性详解(2)

    (本文引用了microheart,ggjucheng的一些资料,在此感谢.charles觉得知识无价,开源共享无价) 这一次我们接着分析文件IO校验的相关代码,看看最底层是如何实现这种大数据集的文件校 ...

  5. mysql如何进行以=分割的字符串的拆分

    SUBSTRING_INDEX(str, delim, count) str: 要处理的字符串 delim: 分割符 count: 计数 如果为正数,则从左开始数,如果为负数,则从右开始数 使用示例: ...

  6. 初次用SqlServer查看本地的Excel文件时需要注意的地方

    日常用到通过SqlServer 读取Excel文件的案例 ,记录下来 文件路径 :C:\Users\Administrator\Desktop\icd10.xls 1.查询语句: SELECT  *F ...

  7. 并发之Striped64(l累加器)

    并发之Striped64(累加器)     对于该类的实现思想:    Striped64是在java8中添加用来支持累加器的并发组件,它可以在并发环境下使用来做某种计数,Striped64的设计思路 ...

  8. Luogu 1099 树网的核

    bzoj1999 数据加强版(n <= 5e5) 较早的noip题,值得研究 重要结论:直径的最长性,任何从直径中离开直径的点到它离开的点的距离,都不会比直径的另一端到它离开的点长(否则就有新的 ...

  9. laravel的mvc

  10. jquery遮罩层

    (function () { //遮罩层实现 zhe zhao ceng kexb 2016.2.24 $.extend($.fn, { mask: function (msg, maskDivCla ...