禁忌搜索 理解與c++實現 解決旅行商問題

禁忌搜索 理解與c++實現 解決旅行商問題

首先介紹一下旅行商問題本身,

旅行商問題,即TSP問題(Traveling Salesman Problem)又譯爲旅行推銷員問題、貨郎擔問題,是數學領域中著名問題之一。假設有一個旅行商人要拜訪n個城市,他必須選擇所要走的路徑,路徑的限制是每個城市只能拜訪一次,而且最後要回到原來出發的城市。路徑的選擇目標是要求得的路徑路程爲所有路徑之中的最小值。

和最小生成樹有點類似,不同的是,每個地點只能走一次,並且需要返回開始的地點。

TSP問題是一個組合優化問題。該問題可以被證明具有NPC計算複雜性。因此,任何能使該問題的求解得以簡化的方法,都將受到高度的評價和關注。

旅行推銷員問題是圖論中最著名的問題之一,即“已給一個n個點的完全圖,每條邊都有一個長度,求總長度最短的經過每個頂點正好一次的封閉迴路”。Edmonds,Cook和Karp等人發現,這批難題有一個值得注意的性質,對其中一個問題存在有效算法時,每個問題都會有有效算法。

迄今爲止,這類問題中沒有一個找到有效算法。傾向於接受NP完全問題(NP-Complete或NPC)和NP難題(NP-Hard或NPH)不存在有效算法這一猜想,認爲這類問題的大型實例不能用精確算法求解,必須尋求這類問題的有效的近似算法。

所以考慮用禁忌搜索嘗試解決,禁忌搜索的資料好像也比較少。

禁忌搜索

禁忌搜索(Tabu Search,TS,又稱禁忌搜尋法)是一種現代啓發式算法,由美國科羅拉多大學教授Fred Glover在1986年左右提出的,是一個用來跳脫局部最優解的搜索方法。其先創立一個初始化的方案;基於此,算法“移動”到一相鄰的方案。經過許多連續的移動過程,提高解的質量。

先介紹最核心的思路:

通過改進局部搜索,以跳脫局部最優解。

在一定時間裏不找以前選擇過的解,在一定情況下可以特赦。

原理聽起來很簡單,但是實現起來其實細節很多,這裏以前選擇過的解是廣義上的,不一定是解本身,可以是類似的轉移動作。

介紹一下幾個名詞

鄰域:
下一步所有可能的解的集合。

領域動作
產生領域的方法,在tsp問題中,爲交換兩個地方的位置。

候選集
有的時候領域數量比較多,不把所有領域都做爲候選集。可以採取一定的選取方式,或是隨機,或是最優的前k個,亦或是別的方式。選擇出候選集。

禁忌表
存儲已經走過的解,當時間超過禁忌長度的時候,就可以釋放。

禁忌長度:
禁忌表生效的時間,禁忌長度越短,內存佔用越少,解禁範圍越大(搜索範圍上限越大),容易造成循環搜索,過早陷入局部最優。反之相反。

特赦規則
當有的解存在於禁忌表中時,不能一味的全部捨棄,當滿足一定的規則時,就可以將他特赦,也就是參與選擇,一般有以下幾種特赦規則。
(1)基於評價值的規則,若出現一個解的目標值好於前面任何一個最佳候選解,可特赦;
(2)基於最小錯誤的規則,若所有對象都被禁忌,特赦一個評價值最小的解;
(3)基於影響力的規則,可以特赦對目標值影響大的對象。

候選集
候選集的大小,過大增加計算內存和計算時間,過小過早陷入局部最優。候選集的選擇一般由鄰域中的鄰居組成,可以選擇所有鄰居,也可以選擇表現較好的鄰居,還可以隨機選擇幾個鄰居。

評價函數
評價函數分爲直接評價函數和間接評價函數。
直接評價函數:上述例子,均直接使用目標值作爲評價函數。
間接評價函數:反映目標函數特性的函數(會比目標函數的計算更爲簡便,用以減少計算時間等)。

終止規則
禁忌搜索是一個啓發式算法,我們不可能讓搜索過程無窮進行,所以一些直觀的終止規則就出現了
(1)確定步數終止,無法保證解的效果,應記錄當前最優解;
(2)頻率控制原則,當某一個解、目標值或元素序列的頻率超過一個給定值時,終止計算;
(3)目標控制原則,如果在一個給定步數內,當前最優值沒有變化,可終止計算。

由此可見

我認爲禁忌搜索是一種思想,上面的參數多到出奇,禁忌的方法也不限於狹義的解,例如在tsp問題中,將所有領域進行編號,禁忌的是領域的編號,在狀態轉換後,領域編號所對應的解已經不同,但是仍然放在禁忌表中。所以我認爲禁忌搜索靈活且可拓展性大。具體到自己實現的時候,也確實遇到很多困難。

網上分析了一位大佬的代碼,照着分析了一下。並自己劃分了一下功能。

我將一次禁忌搜索分爲進行K次小搜索

初始化全局最優解,禁忌表
循環K次小型搜索
	初始化搜索相關解(小型搜索最優解,特赦最優解,迭代過程中最優解)
	迭代N次
		1.遍歷鄰域
			if 在禁忌表中,更新特赦最優解
			else 不在表中,更新迭代最優解
		2.判斷特設最優解是否滿足特赦條件(常見爲兩種)
		3.選擇迭代最優解進行轉移,並更新禁忌表//可用優先隊列 但是禁忌表往往規模不大
	檢查全局最優解和搜索最優解

按照這個思路寫了代碼,相關注意點也寫在了註釋裏

#include<bits/stdc++.h>
using namespace std;
#define rand(a,b) ((rand()%(b-a))+a)
const int INF=INT_MAX;
const int K=25;//小型搜索的次數
const int ITERATIONS=100 ;//小型搜索中的迭代次數
const int TABU_SIZE=10;//禁忌長度
const int SWAPSIZE=5;//交換數目 ,理解爲候選集,此代碼中沒有用到

int city[60][2];//記錄城市座標
double adj[60][60];//記錄城市之間的距離
int nowPath[60];//當前路徑
int finalBestPath[60];//最優路徑
int TabuList[2000][3];//第一維度 鄰域id 第二維度 0:start 1:end 2.tabu
//原文件裏還有個dis但是個人覺得沒有必要 因爲沒有複用到
//在最大規模爲60的問題中 鄰域數量最大爲60*59/2 給到2000是足夠的



void readAndInit(int n);
void getRandomOrder(int path[],int n);//將path初始化爲隨機序列 值域[0,n)
int TabuSearch(int n);//禁忌搜索主體
int smallSearch(int n);//小型搜索
double getPathValue(int path[],int n);//獲取給定路徑的數值


int main(){
    int n=17;
     readAndInit(n);//讀入數據以及舉例 數據初始化
     TabuSearch(n);



}

void readAndInit(int n){
    for(int i=0;i<n;i++)
        for(int j=0;j<=i;j++){
            scanf("%lf",&adj[i][j]);
            adj[j][i]=adj[i][j];
        }
/*
 * for(int i=0;i<n*2;i++)
            scanf("%d",&city[i/2][i%2]);
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            adj[i][j]=sqrt((city[i][0]-city[j][0])*(city[i][0]-city[j][0])+(city[i][1]-city[j][1])*(city[i][1]-city[j][1]));

 */
}



int TabuSearch(int n){
    int finalDis=INF;
    //初始化禁忌表
    int now=0;
    for(int i=0;i<n;i++)
        for(int j=i+1;j<n;j++){
            TabuList[now][0]=i;
            TabuList[now][1]=j;
            TabuList[now][2]=0;
            now++;
        }
    //
    for(int i=0;i<K;i++){//值得注意 每次搜索禁忌表是共享的 也就是新的小搜索不會重置禁忌表
        int smallSearchDis=smallSearch(n);
        if(finalDis>smallSearchDis){//如果此次小型搜索的最優解優於finalDis,則更新
            finalDis=smallSearchDis;
            memcpy(finalBestPath,nowPath,sizeof (nowPath));//路徑複製
        }

    }


    //show
    cout<<finalDis<<'\n';
    for(int i=0;i<n;i++)
        printf("->%d",finalBestPath[i]);


}

int smallSearch(int n){
    getRandomOrder(nowPath,n);
    double bestDis=getPathValue(nowPath,n);//初始化小型搜索最優解

    int pardon[2], curBest[2];//特赦最優解和搜索最優解
    pardon[0]=pardon[1]=curBest[0]=curBest[1]=INF;//初始化
    int LNum=n*(n-1)/2;//鄰域數量
    for(int i=0;i<ITERATIONS;i++){//迭代

        for(int j=0;j<LNum;j++){//領域搜索
            swap(nowPath[TabuList[j][0]],nowPath[TabuList[j][1]]);
            double tmpDis=getPathValue(nowPath,n);
            if(TabuList[j][2]==0){//沒有被禁忌
                if(tmpDis<curBest[1]){
                    curBest[0]=j;
                    curBest[1]=tmpDis;
                }
            }
            else{//被禁忌
                if(tmpDis<pardon[1]){
                    pardon[0]=j;
                    pardon[1]=tmpDis;
                }
            }
            swap(nowPath[TabuList[j][0]],nowPath[TabuList[j][1]]);
        }
        //鄰域搜索結束
        //判斷特設最優解是否滿足特赦條件
        //第一個條件對應領域全被禁忌,在鄰域數量大於禁忌長度時不可能出現
        //第二個條件似乎存疑,此刻curBest並沒有更新到bestDis上,
        //如果此時curBest比pardon更優,則curBest無法被更新,可能這樣是增加搜索前面的可能性?
        //另一方面如果第二個條件成立 必然出現在另一次小型搜索過程中
        //因爲重新搜索並沒有重置禁忌表,但bestDis被重置,在同一次小型搜索中不可能滿足第二個條件
        if(curBest[1] == INF || pardon[1] < bestDis){
            curBest[0]=pardon[0];
            curBest[1]=pardon[1];
        }
        //嘗試更新 小型搜索的最優解bestDis
        if(curBest[1]<bestDis){
            bestDis=curBest[1];
            //交換位置
            swap(nowPath[TabuList[curBest[0]][0]],nowPath[TabuList[curBest[0]][1]]);
            //更新禁忌表 可考慮用其他數據結構維護 例如鏈表、優先隊列維護禁忌表,在禁忌表更新上不是一個數量級
            TabuList[curBest[0]][2]=TABU_SIZE;
            for(int j=0;j<LNum;j++)
                if(TabuList[j][2]>0)
                    TabuList[j][2]--;
        }
        //隨機性可能導致某一次搜索永遠在某一個較高點,附近鄰域都沒有比他大的,即便是禁忌表特赦也沒有用

    }
    return bestDis;




}

void getRandomOrder(int path[],int n){
    //srand(time(0));//保證結果可復現 註釋此句
    for(int i=0;i<n;i++)
        path[i]=i;
    for(int i=0;i<n;i++)
        swap(path[i],path[rand(i,n)]);
}

double getPathValue(int path[],int n){
    double value=adj[path[n-1]][path[0]];
    for(int i=0;i<n-1;i++)
        value+=adj[path[i]][path[i+1]];
     return value;
}

用tsp的數據集測試了一下,效果確實拔羣。
我是用的是gr17,大部分都能找到最優解,偶爾幾次爲近似解。
數據集提供在百度網盤,有興趣可以自己去測試。
鏈接 提取碼: wtan

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章