Variable Neighborhood Search (VNS),变邻域搜索算法


1 Variable Neighborhood Search (VNS) 介绍

  变邻域搜索算法(VNS)就是一种改进型的局部搜索算法。它利用不同的动作构成的邻域结构进行交替搜索,在集中性和疏散性之间达到很好的平衡。其思想可以概括为“变则通”。

  变邻域搜索算法依赖于以下事实:

  1) 一个邻域结构的局部最优解不一定是另一个邻域结构的局部最优解。

  2) 全局最优解是所有可能邻域的局部最优解。

  变邻域搜索算法主要由以下两个部分组成:

  1) VARIABLE NEIGHBORHOOD DESCENT (VND)

  2) SHAKING PROCEDURE

1.1 邻域的介绍

  所谓邻域,简单的说即是给定点附近其它点的集合。在距离空间中,邻域一般被定义为以给定点为圆心的一个圆;而在组合优化问题中,邻域一般定义为由给定转化规则对给定的问题域上每结点进行转化所得到的问题域上结点的集合。实际上,邻域就是指对当前解进行一个操作(这个操作可以称之为邻域动作)可以得到的所有解的集合。那么不同邻域的本质区别就在于邻域动作的不同了。

  说到邻域,则要介绍邻域动作。邻域动作是一个函数,通过这个函数,对当前解s,产生其相应的邻居解集合。例如:对于一个bool型问题,其当前解为:s = 1001,当将邻域动作定义为翻转其中一个bit时,得到的邻居解的集合N(s)={0001,1101,1011,1000},其中N(s) ∈ S。同理,当将邻域动作定义为互换相邻bit时,得到的邻居解的集合N(s)={0101,1001,1010}。

1.2 Variable Neighborhood Descent (VND)

  VND其实就是一个算法框架,它的过程描述如下:

  1) 给定初始解S; 定义m个邻域,记为N_k(k = 1, 2, 3......m);i = 1。

  2) 使用邻域结构N_i(即 N_i(S))进行搜索,如果在N_i(S)里找到一个比S更优的解S′,则令S=S′, i=1 。

  3) 如果搜遍邻域结构N_i仍找不到比S更优的解,则令i++。

  4) 如果i≤m ,转步骤2。

  5) 输出最优解S。

  VND的图解如下:

VND图解

  如上图所示:

  1) 当在本邻域搜索找不出一个比当前解更优的解的时候,我们就跳到下一个邻域继续进行搜索。如图中虚黑线所示。

  2) 当在本邻域搜索找到了一个比当前解更优的解的时候,我们就跳回第一个邻域重新开始搜索。如图中实黑线所示。

  之前我们把局部搜索比作爬山的过程,那么每变换一次邻域,也可以理解为切换了搜索的地形(landscape)。效果如下 :

VND图示

  每一次跳跃,得到都是一个新的世界...

  VND伪代码如下:

  
VND伪代码

1.3 Shaking Procedure

  其实呀,这玩意儿。说白了就是一个扰动算子,类似于邻域动作的这么一个东
西。通过这个算子,可以产生不同的邻居解。虽然名词很多看起来很高大上,扰动、抖动、邻域动作这几个本质上还是没有什么区别的。都是通过一定的规则,将一个解变换到另一个解而已。


2 VNS 过程

  在综合了前面这么多的知识以后,VNS的过程其实非常简单, 直接看伪代码,一目了然:

VNS伪代码

  伪代码中N_k和N_l代表的邻域集合,分别是给Shaking和VND使用的,这两点希望大家要格外注意,区分开来哈。这两个邻域集合可以是一样的,也可以不一样。


3 VNS 实现

3.1 变邻域搜索算法解TSP问题

////////////////////////
//TSP问题 变邻域搜索求解代码
//基于Berlin52例子求解
//作者:infinitor
//时间:2018-04-12
////////////////////////


#include <iostream>
#include <cmath>
#include <stdlib.h>
#include <time.h>
#include <vector>
#include <windows.h>
#include <memory.h>
#include <string.h>
#include <iomanip>

#define DEBUG

using namespace std;

#define CITY_SIZE 52 //城市数量


//城市坐标
typedef struct candidate
{
    int x;
    int y;
}city, CITIES;

//解决方案
typedef struct Solution
{
    int permutation[CITY_SIZE]; //城市排列
    int cost;                        //该排列对应的总路线长度
}SOLUTION;

//城市排列
int permutation[CITY_SIZE];
//城市坐标数组
CITIES cities[CITY_SIZE];


//berlin52城市坐标,最优解7542好像
CITIES berlin52[CITY_SIZE] =
{
{ 565,575 },{ 25,185 },{ 345,750 },{ 945,685 },{ 845,655 },
{ 880,660 },{ 25,230 },{ 525,1000 },{ 580,1175 },{ 650,1130 },{ 1605,620 },
{ 1220,580 },{ 1465,200 },{ 1530,5 },{ 845,680 },{ 725,370 },{ 145,665 },
{ 415,635 },{ 510,875 },{ 560,365 },{ 300,465 },{ 520,585 },{ 480,415 },
{ 835,625 },{ 975,580 },{ 1215,245 },{ 1320,315 },{ 1250,400 },{ 660,180 },
{ 410,250 },{ 420,555 },{ 575,665 },{ 1150,1160 },{ 700,580 },{ 685,595 },
{ 685,610 },{ 770,610 },{ 795,645 },{ 720,635 },{ 760,650 },{ 475,960 },
{ 95,260 },{ 875,920 },{ 700,500 },{ 555,815 },{ 830,485 },{ 1170,65 },
{ 830,610 },{ 605,625 },{ 595,360 },{ 1340,725 },{ 1740,245 }
};


//随机数产生函数,利用系统时间,精确到微妙,再加一个变量i组合产生随机数。
//单单用srand(time(NULL)) + rand()达不到效果,因为函数执行太快。时间间隔太短
int randEx(int i)
{
    LARGE_INTEGER seed;
    QueryPerformanceFrequency(&seed);
    QueryPerformanceCounter(&seed);
    srand((unsigned int)seed.QuadPart + i);

    return rand();
}


//计算两个城市间距离
int distance_2city(city c1, city c2)
{
    int distance = 0;
    distance = sqrt((double)((c1.x - c2.x)*(c1.x - c2.x) + (c1.y - c2.y)*(c1.y - c2.y)));

    return distance;
}

//根据产生的城市序列,计算旅游总距离
//所谓城市序列,就是城市先后访问的顺序,比如可以先访问ABC,也可以先访问BAC等等
//访问顺序不同,那么总路线长度也是不同的
//p_perm 城市序列参数
int cost_total(int * cities_permutation, CITIES * cities)
{
    int total_distance = 0;
    int c1, c2;
    //逛一圈,看看最后的总距离是多少
    for (int i = 0; i < CITY_SIZE; i++)
    {
        c1 = cities_permutation[i];
        if (i == CITY_SIZE - 1) //最后一个城市和第一个城市计算距离
        {
            c2 = cities_permutation[0];
        }
        else
        {
            c2 = cities_permutation[i + 1];
        }
        total_distance += distance_2city(cities[c1], cities[c2]);
    }

    return total_distance;
}

//获取随机城市排列
void random_permutation(int * cities_permutation)
{
    int i, r, temp;
    for (i = 0; i < CITY_SIZE; i++)
    {
        cities_permutation[i] = i; //初始化城市排列,初始按顺序排
    }


    for (i = 0; i < CITY_SIZE; i++)
    {
        //城市排列顺序随机打乱
        srand((unsigned int)time(NULL));
        int j = rand();
        r = randEx(++j) % (CITY_SIZE - i) + i;
        temp = cities_permutation[i];
        cities_permutation[i] = cities_permutation[r];
        cities_permutation[r] = temp;
    }
}


//颠倒数组中下标begin到end的元素位置
void swap_element(int *p, int begin, int end)
{
    int temp;
    while (begin < end)
    {
        temp = p[begin];
        p[begin] = p[end];
        p[end] = temp;
        begin++;
        end--;
    }
}


//邻域结构0 利用swap_element算子搜索
void neighborhood_zero(SOLUTION & solution, CITIES * cities)
{
    SOLUTION current_solution = solution;

    int count = 0;
    int max_no_improve = 10;

    do
    {
        count++;
        for (int i = 0; i < CITY_SIZE - 1; i++)
        {
            for (int k = i + 1; k < CITY_SIZE; k++)
            {
                current_solution = solution;
                swap_element(current_solution.permutation, i, k);

                current_solution.cost = cost_total(current_solution.permutation, cities);
                if (current_solution.cost < solution.cost)
                {
                    solution = current_solution;
                    count = 0; //count复位
                }

            }
         }

    } while (count <= max_no_improve);



}




// two_opt_swap算子
void two_opt_swap(int *cities_permutation, int b, int c)
{
    vector<int> v;
    for (int i = 0; i < b; i++)
    {
        v.push_back(cities_permutation[i]);
    }
    for (int i = c; i >= b; i--)
    {
        v.push_back(cities_permutation[i]);
    }
    for (int i = c + 1; i < CITY_SIZE; i++)
    {
        v.push_back(cities_permutation[i]);
    }

    for (int i = 0; i < CITY_SIZE; i++)
    {
        cities_permutation[i] = v[i];
    }

}

//邻域结构1 使用two_opt_swap算子
void neighborhood_one(SOLUTION & solution, CITIES *cities)
{
    int i, k, count = 0;
    int max_no_improve = 10;
    SOLUTION current_solution = solution;
    do
    {
        count++;
        for (i = 0; i < CITY_SIZE - 1; i++)
        {
            for (k = i + 1; k < CITY_SIZE; k++)
            {
                current_solution = solution;
                two_opt_swap(current_solution.permutation, i, k);

                current_solution.cost = cost_total(current_solution.permutation, cities);
                if (current_solution.cost < solution.cost)
                {
                    solution = current_solution;

                    count = 0; //count复位
                }

             }
          }
    }while (count <= max_no_improve);

}
//two_h_opt_swap算子
void two_h_opt_swap(int *cities_permutation, int a, int d)
{
    int n = CITY_SIZE;
    vector<int> v;
    v.push_back(cities_permutation[a]);
    v.push_back(cities_permutation[d]);
    // i = 1 to account for a already added
    for (int i = 1; i < n; i++)
    {
        int idx = (a + i) % n;
        // Ignore d which has been added already
        if (idx != d)
        {
            v.push_back(cities_permutation[idx]);
        }
    }

    for (int i = 0; i < v.size(); i++)
    {
        cities_permutation[i] = v[i];
    }

}


//邻域结构2 使用two_h_opt_swap算子
void neighborhood_two(SOLUTION & solution, CITIES *cities)
{
    int i, k, count = 0;
    int max_no_improve = 10;
    SOLUTION current_solution = solution;
    do
    {
        count++;
        for (i = 0; i < CITY_SIZE - 1; i++)
        {
            for (k = i + 1; k < CITY_SIZE; k++)
            {
                current_solution = solution;
                two_h_opt_swap(current_solution.permutation, i, k);

                current_solution.cost = cost_total(current_solution.permutation, cities);

                if (current_solution.cost < solution.cost)
                {
                    solution = current_solution;
                    count = 0; //count复位
                }

            }
        }
    } while (count <= max_no_improve);
}


//VND
//best_solution最优解
//current_solution当前解
void variable_neighborhood_descent(SOLUTION & solution, CITIES * cities)
{

    SOLUTION current_solution = solution;
    int l = 0;
    cout  <<"=====================VariableNeighborhoodDescent=====================" << endl;
    while(true)
    {
        switch (l)
        {
        case 0:
            neighborhood_zero(current_solution, cities);
            cout << setw(45) << setiosflags(ios::left) << "Now in neighborhood_zero, current_solution = " << current_solution.cost << setw(10) << setiosflags(ios::left) << "  solution = " << solution.cost << endl;
            if (current_solution.cost < solution.cost)
            {
                solution = current_solution;
                l = -1;
            }
            break;
        case 1:
            neighborhood_one(current_solution, cities);
            cout << setw(45) << setiosflags(ios::left)  <<"Now in neighborhood_one , current_solution = " << current_solution.cost << setw(10) << setiosflags(ios::left) << "  solution = " << solution.cost << endl;
            if (current_solution.cost < solution.cost)
            {
                solution = current_solution;
                l = -1;
            }
            break;
        case 2:
            neighborhood_two(current_solution, cities);
            cout << setw(45) << setiosflags(ios::left) << "Now in neighborhood_two , current_solution = " << current_solution.cost << setw(10) << setiosflags(ios::left) << "  solution = " << solution.cost << endl;
            if (current_solution.cost < solution.cost)
            {
                solution = current_solution;
                l = -1;
            }
            break;

        default:
            return;
        }
        l++;

    }

}

//将城市序列分成4块,然后按块重新打乱顺序。
//用于扰动函数
void double_bridge_move(int * cities_permutation)
{
    srand((unsigned int)time(NULL));
    int j = rand();
    int pos1 = 1 + randEx(++j) % (CITY_SIZE / 4);
    int pos2 = pos1 + 1 + randEx(++j) % (CITY_SIZE / 4);
    int pos3 = pos2 + 1 + randEx(++j) % (CITY_SIZE / 4);

    int i;
    vector<int> v;
    //第一块
    for (i = 0; i < pos1; i++)
    {
        v.push_back(cities_permutation[i]);
    }

    //第二块
    for (i = pos3; i < CITY_SIZE; i++)
    {
        v.push_back(cities_permutation[i]);
    }
    //第三块
    for (i = pos2; i < pos3; i++)
    {
        v.push_back(cities_permutation[i]);
    }

    //第四块
    for (i = pos1; i < pos2; i++)
    {
        v.push_back(cities_permutation[i]);
    }


    for (i = 0; i < (int)v.size(); i++)
    {
        cities_permutation[i] = v[i];
    }


}

//抖动
void shaking(SOLUTION &solution, CITIES *cities)
{
    double_bridge_move(solution.permutation);
    solution.cost = cost_total(solution.permutation, cities);
}


void variable_neighborhood_search(SOLUTION & best_solution, CITIES * cities)
{

    int max_iterations = 5;

    int count = 0, it = 0;

    SOLUTION current_solution = best_solution;

    //算法开始
    do
    {
        cout << endl << "\t\tAlgorithm VNS iterated  " << it << "  times" << endl;
        count++;
        it++;
        shaking(current_solution, cities);

        variable_neighborhood_descent(current_solution, cities);

        if (current_solution.cost < best_solution.cost)
        {
            best_solution = current_solution;
            count = 0;
        }

        cout << "\t\t全局best_solution = " << best_solution.cost << endl;

    } while (count <= max_iterations);


}


int main()
{

    SOLUTION best_solution;

    random_permutation(best_solution.permutation);
    best_solution.cost = cost_total(best_solution.permutation, berlin52);

    cout << "初始总路线长度 = " << best_solution.cost << endl;

    variable_neighborhood_search(best_solution, berlin52);

    cout << endl << endl << "搜索完成! 最优路线总长度 = " << best_solution.cost << endl;
    cout << "最优访问城市序列如下:" << endl;
    for (int i = 0; i < CITY_SIZE; i++)
    {
        cout << setw(4) << setiosflags(ios::left) << best_solution.permutation[i];
    }

    cout << endl << endl;

    return 0;
}











Reference:

  1. 干货 | 变邻域搜索算法(Variable Neighborhood Search,VNS)超详细一看就懂
  2. 干货 | 变邻域搜索算法(VNS)求解TSP(附C++详细代码及注释)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容

  • 局部搜索算法 目录: 1、数学定义 2、过程描述 3、算法简介 4、总结 1、数学定义 局部搜索是解决最优化问题的...
    迷之菌阅读 9,780评论 0 3
  • 是不是对生活不太满意 很久没有笑过又不知为何 既然不快乐又不喜欢这里 不如一路向西去大理 路程有点波折空气有点稀薄...
    佛头夜雨阅读 367评论 2 2
  • 如梦令·名家进校园 名家走进我校, 进行写作指导。 学生陶醉中, 认真聆听受教。 甚妙,甚妙 川小明日更好! 如梦...
    沐与曦阅读 322评论 0 0
  • 她在这里等着,火车站。 人们说,她每天早上都要在这里等,足足等了十八年。有人说她的儿子出去当兵了,再也...
    18Z阅读 589评论 0 0
  • 我不再明白的向你表达爱意 像男生的第一次遗精 像女生的第一次初潮 兴奋,而且神秘 我也不再想遇见你在某个转角 你一...
    子兮子兮丶阅读 171评论 0 0