喜欢的话可以扫码关注我们的公众号哦,更多精彩尽在微信公众号【程序猿声】

00 目录
- 局部搜索再次科普
- 变邻域搜索
- 造轮子写代码
01 局部搜索科普三连
虽然之前做的很多篇启发式的算法都有跟大家提过局部搜索这个概念,为了加深大家的印象,在变邻域主角登场之前还是给大家科普一下相关概念。热热身嘛~
1.1 局部搜索是什么玩意儿?
官方一点:局部搜索是解决最优化问题的一种启发式算法。对于某些计算起来非常复杂的最优化问题,比如各种NP完全问题,要找到最优解需要的时间随问题规模呈指数增长,因此诞生了各种启发式算法来退而求其次寻找次优解,是一种近似算法(Approximate algorithms),以时间换精度的思想。局部搜索就是其中的一种方法。
通俗一点:局部搜索算法是对一类算法的统称,符合其框架的算法很多,比如之前公众号推文中介绍的爬山法、模拟退火法和禁忌搜索算法都属于局部搜索算法。尽管各个算法在优化过程中的细节存在差异,但在优化流程上呈现出很大的共性。
它的基本原理是在临近解中迭代,使目标函数逐步优化,直至不能再优化为止。
1.2 局部搜索的过程
我们可以将局部搜索算法的统一框架描述为:
- 算法从一个或若干个初始解出发。
- 在算法参数控制下由当前状态的邻域中产生若干个候选解。
- 以某种策略在候选解中确定新的当前解。
- 伴随控制参数的调节,重复执行上述搜索过程,直至满足算法终止准则。
- 结束搜索过程并输出优化结果。
1.3 局部搜索的几大要素
局部搜索算法主要包含五大要素:
- 目标函数:用来判断解的优劣。
- 邻域的定义:根据不同问题,有着不同的邻域定义。
- 初始解的产生规则
- 新解的产生和接受规则
- 算法终止准则
其中前两个要素的定义和算法要解决的特定问题有关,而且不同的人对同一问题可能有完全不同的定义。后三个要素定义的不同则会产生各种不同的局部搜索算法,而它们的效率和最终解的质量也会有很大的差异。
02 变邻域搜索算法
2.1 什么是VNS?
对上面的局部搜索有一定的印象以后,理解变邻域搜索也不难了。其实说白了,变邻域搜索算法(VNS)就是一种改进型的局部搜索算法。它利用不同的动作构成的邻域结构进行交替搜索,在集中性和疏散性之间达到很好的平衡。其思想可以概括为“变则通”。
变邻域搜索依赖于以下事实:
- 一个邻域结构的局部最优解不一定是另一个邻域结构的局部最优解。
- 全局最优解是所有可能邻域的局部最优解。
它由主要由以下两个部分组成:
- variable neighborhood descent (VND)
- shaking procedure
大家别急,下面我们将会对这两个部分进行分析。然后,before that……
2.2 你们一定想知道邻域是什么?
官方一点:所谓邻域,简单的说即是给定点附近其他点的集合。在距离空间中,邻域一般被定义为以给定点为圆心的一个圆;而在组合优化问题中,邻域一般定义为由给定转化规则对给定的问题域上每结点进行转化所得到的问题域上结点的集合。
通俗一点:邻域就是指对当前解进行一个操作(这个操作可以称之为邻域动作)可以得到的所有解的集合。那么邻域的本质区别就在于邻域动作的不同了。
2.3 还是要说说邻域动作
邻域动作是一个函数,通过这个函数,对当前解s,产生其相应的邻居解集合。例如:对于一个bool型问题,其当前解为:s = 1001,当将邻域动作定义为翻转其中一个bit时,得到的邻居解的集合N(s)={0001,1101,1011,1000},其中N(s) ∈ S。同理,当将邻域动作定义为互换相邻bit时,得到的邻居解的集合N(s)={0101,1001,1010}。
2.4 variable neighborhood descent (VND)
VND其实就是一个算法框架,它的过程描述如下:
- 初始解S,定义M个邻域,记为Nk(k = 1, 2, 3……m),i = 1。
- 使用邻域结构Ni进行搜索,直到陷入局部最优解S′ 。
- 如果S′ 优于S,令S=S′,i=1; 否则,i++。
- 如果i≤m ,转步骤2。
- 输出最优解S。
我知道没图你们是不会点进来的……
VND的图解如下:

只说两点,再问自杀:
- 当在本邻域搜索找不出一个比当前解更优的解的时候,我们就跳到下一个邻域继续进行搜索。如图中虚黑线所示。
- 当在本邻域搜索找到了一个比当前解更优的解的时候,我们就跳回第一个邻域重新开始搜索。如图中红线所示。
之前我们把局部搜索比喻作爬山的过程,那么每变换一次邻域,也可以理解为切换了搜索的地形(landscape)。效果如下 :

每一次跳跃,得到都是一个新的世界……
伪代码描述如下:

2.5 shaking procedure
其实呀,这玩意儿。说白了就是一个扰动算子,类似于邻域动作的这么一个东西。通过这个算子,可以产生不同的邻居解。虽然名词很多看起来很高大上,扰动、抖动、邻域动作这几个本质上还是没有什么区别的。都是通过一定的规则,将一个解变换到另一个解而已。这里读者还是抓其本质,不要被表象所迷惑了就好。
2.6 VNS过程
在综合了前面这么多的知识以后,VNS的过程其实非常简单。可以描述为以下几步:
- 产生初始解s1。
- shaking s1,得到解s2。
- 对解s2进行VND,得到解s3。
- 如果达到边界条件,结束程序,输出最优解。否则跳回第二步。
结合伪代码,一目了然:

03 变邻域搜索解决TSP问题
本次代码还是基于求解TSP旅行商问题的。至于什么是TSP问题,小编这实在是不想科普啦……
代码是基于迭代搜索那个代码魔改过来的。其实看了这么多启发式算法解TSP问题的代码。想必各位都有了一个比较清晰的认识,其实呀。之前介绍的模拟退火、遗传算法、迭代搜索和现在的变邻域等等,是十分相似滴。最大的不同在于算法框架的不同而已,像什么扰动啦,邻域动作啦。代码基本是不变的。所以大家可以多联想,多思考,学习就是一个探求事物本质的过程嘛!
简要说说算法vnd里面两个邻域使用的算子:
-
two_opt_swap
没啥好说的,区间反转。直接上图: -
two_h_opt_swap
还是要说一点,随机产生两点,塞进新排列头部。其余的按顺序往后逐个塞进去。嗯,来看图片~
看代码吧。
1 2 //TSP问题 变邻域搜索求解代码 3 //基于Berlin52例子求解 4 //作者:infinitor 5 //时间:2018-04-12 6 7 8 9 #include <iostream> 10 #include <cmath> 11 #include <stdlib.h> 12 #include <time.h> 13 #include <vector> 14 #include <windows.h> 15 #include <memory.h> 16 #include <string.h> 17 #include <iomanip> 18 #include <algorithm> 19 #define DEBUG 20 21 using namespace std; 22 23 #define CITY_SIZE 52 //城市数量 24 25 26 //城市坐标 27 typedef struct candidate 28 { 29 int x; 30 int y; 31 }city, CITIES; 32 33 //解决方案 34 typedef struct Solution 35 { 36 int permutation[CITY_SIZE]; //城市排列 37 int cost; //该排列对应的总路线长度 38 }SOLUTION; 39 40 //城市排列 41 int permutation[CITY_SIZE]; 42 //城市坐标数组 43 CITIES cities[CITY_SIZE]; 44 45 46 //berlin52城市坐标,最优解7542好像 47 CITIES berlin52[CITY_SIZE] = 48 { 49 { 565,575 },{ 25,185 },{ 345,750 },{ 945,685 },{ 845,655 }, 50 { 880,660 },{ 25,230 },{ 525,1000 },{ 580,1175 },{ 650,1130 },{ 1605,620 }, 51 { 1220,580 },{ 1465,200 },{ 1530,5 },{ 845,680 },{ 725,370 },{ 145,665 }, 52 { 415,635 },{ 510,875 },{ 560,365 },{ 300,465 },{ 520,585 },{ 480,415 }, 53 { 835,625 },{ 975,580 },{ 1215,245 },{ 1320,315 },{ 1250,400 },{ 660,180 }, 54 { 410,250 },{ 420,555 },{ 575,665 },{ 1150,1160 },{ 700,580 },{ 685,595 }, 55 { 685,610 },{ 770,610 },{ 795,645 },{ 720,635 },{ 760,650 },{ 475,960 }, 56 { 95,260 },{ 875,920 },{ 700,500 },{ 555,815 },{ 830,485 },{ 1170,65 }, 57 { 830,610 },{ 605,625 },{ 595,360 },{ 1340,725 },{ 1740,245 } 58 }; 59 //优化值 60 int Delta1[CITY_SIZE][CITY_SIZE] = { 0 }; 61 62 63 //计算两个城市间距离 64 int distance_2city(city c1, city c2) 65 { 66 int distance = 0; 67 distance = sqrt((double)((c1.x - c2.x)*(c1.x - c2.x) + (c1.y - c2.y)*(c1.y - c2.y))); 68 69 return distance; 70 } 71 72 //根据产生的城市序列,计算旅游总距离 73 //所谓城市序列,就是城市先后访问的顺序,比如可以先访问ABC,也可以先访问BAC等等 74 //访问顺序不同,那么总路线长度也是不同的 75 //p_perm 城市序列参数 76 int cost_total(int * cities_permutation, CITIES * cities) 77 { 78 int total_distance = 0; 79 int c1, c2; 80 //逛一圈,看看最后的总距离是多少 81 for (int i = 0; i < CITY_SIZE; i++) 82 { 83 c1 = cities_permutation[i]; 84 if (i == CITY_SIZE - 1) //最后一个城市和第一个城市计算距离 85 { 86 c2 = cities_permutation[0]; 87 } 88 else 89 { 90 c2 = cities_permutation[i + 1]; 91 } 92 total_distance += distance_2city(cities[c1], cities[c2]); 93 } 94 95 return total_distance; 96 } 97 98 //获取随机城市排列 99 void random_permutation(int * cities_permutation) 100 { 101 int i, r, temp; 102 for (i = 0; i < CITY_SIZE; i++) 103 { 104 cities_permutation[i] = i; //初始化城市排列,初始按顺序排 105 } 106 107 random_shuffle(cities_permutation, cities_permutation + CITY_SIZE); //随机化排序 108 109 } 110 //对应two_opt_swap的去重 111 int calc_delta1(int i, int k, int *tmp, CITIES * cities) { 112 int delta = 0; 113 /* 114 以下计算说明: 115 对于每个方案,翻转以后没必要再次重新计算总距离