旅行商問題(動態規劃方法,超級詳細的)

一、題目

  • 一個售貨員必須訪問n個城市,恰好訪問每個城市一次,並最終回到出發城市。
    售貨員從城市i到城市j的旅行費用是一個整數,旅行所需的全部費用是他旅行經過的的各邊費用之和,而售貨員希望使整個旅行費用最低。
  • (等價於求圖的最短哈密爾頓迴路問題)令G=(V, E)是一個帶權重的有向圖,頂點集V=(v0, v1, ..., vn-1)。從圖中任一頂點vi出發,經圖中所有其他頂點一次且只有一次,最後回到同一頂點vi的最短路徑。

二、測試用例

 

其中1,2,3,4,5代表五個城市。此模型可抽象爲圖,可用鄰接矩陣c表示,如下圖所示:

三、動態規劃方程

 假設從頂點s出發,令d(i, V)表示從頂點i出發經過V(是一個點的集合)中各個頂點一次且僅一次,最後回到出發點s的最短路徑長度。

        推導:(分情況來討論)

        ①當V爲空集,那麼d(i, V),表示直接從i回到s了,此時d(i,V) = c_{is} 且 (i\neq s)

        ②如果V不爲空,那麼就是對子問題的最優求解。你必須在V這個城市集合中,嘗試每一個,並求出最優解。

          d(i, V)=min(C_{ik} + d(k, V-(k))

           注:c_{ik}表示選擇的城市和城市i的距離,d(k, V-(k))是一個子問題。

        綜上所述,TSP問題的動態規劃方程就出來了:

四、用例分析

 

現在對問題定義中的例子來說明TSP的求解過程。(假設出發城市是 0城市)

這裏只畫出了d(1,{2,3,4}),由於篇幅有限這裏就不畫了。

 

①我們要求的最終結果是d(0,{1,2,3,4}),它表示,從城市0開始,經過{1,2,3,4}之中的城市並且只有一次,求出最短路徑.。
②d(0,{1,2,3,4})是不能一下子求出來的,那麼他的值是怎麼得出的呢?看上圖的第二層,第二層表明了d(0,{1,2,3,4})所需依賴的值。那麼得出:
      
     ③d(1,{2,3,4}),d(2,{1,3,4}),d(3,{1,2,4}),d(4,{1,2,3})同樣也不是一步就能求出來的,它們的解一樣需要有依賴,就比如說d(1,{2,3,4})

      d(2,{1,3,4}),d(3,{1,2,4}),d(4,{1,2,3})同樣需要這麼求。

    ④按照上面的思路,只有最後一層的,當V爲空集時,就可以滿足d(i,V) = c_{is} 且 (i\neq s)該條件,直接求出dp數組部分的值。

五、數據結構

由上述動態規劃公式d(i,V)表示從頂點i出發經過V(是一個點的集合)中各個頂點一次且僅一次,最後回到出發點s的最短路徑長度。根據上述給的測試用例有5個城市編號0,1,2,3,4。那麼訪問n個城市,恰好訪問每個城市一次,並最終回到出發城市的嘴短距離可表示爲d(0,{1,2,3,4}),那麼問題來了我們用什麼數據結構表示d(i,V),這裏我們就可二維數據dp[N][M]來表示,N表示城市的個數,M表示集合的數量,即M = 2^{N-1},之所以這麼表示因爲集合V有2^{N-1}個子集。根據測試用例可得出如下dp數組表格:

 

 

那麼你們可能就有疑問了,爲什麼這麼表示?這裏說明一下比如集合{1,2,3,4}爲什麼用15表示,我們可以把 集合中元素看成二進制1的位置(二進制從右開始看),1表示從右開始第一位爲1,2表示從又開始第二位爲1,所以集合{1,2,3,4}可表示二進制(1111)轉化爲十進制爲15。再舉個例子比如集合{1,3}表示爲二進制爲0101,十進制爲5。所以我們求出dp[0][15](通用表示dp[0][2^{N-1}-1])就是本題的最終解。

注意:

  • 對於第y個城市,他的二進制表達爲,1<<(y-1)。
  • 對於數字x,要看它的第i位是不是1,那麼可以通過判斷布爾表達式 (((x >> (i - 1) ) & 1) == 1或者(x  & (1<<(i-1)))!= 0的真值來實現。
  • 由動態規劃公式可知,需要從集合中剔除元素。假如集合用索引x表示,要剔除元素標號爲i,我們異或運算實現減法,其運算表示爲: x = x ^ (1<<(i - 1))。

六、最短路徑頂點的計算

我們先計算dp[N][M]數組之後,我可以用dp數組來反向推出其路徑。其算法思想如下:

比如在第一步時,我們就知道那個值最小,如下圖所示:

因爲dp[][]數組我們已經計算出來了,由計算可知C01+d(1,{2,3,4})最小,所以一開始從起始點0出發,經過1。接下來同樣計算d(1,{2,3,4})

由計算可知C14+d(4,{2,3})所以0--->1---->4,接下來同理求d(4,{2,3}),這裏就省略,讀者可以自行計算。最終計算出來的路徑爲:0--->1--->4--->2--->3--->0

七、代碼編寫

 

#include <iostream>
#include <cmath>
#include <cstring>
#include <vector>

using namespace std;

#define N 5
#define INF 10e7
#define min(a,b) ((a>b)?b:a)

static const int M = 1 << (N-1);
//存儲城市之間的距離
int g[N][N] = {{0,3,INF,8,9},
               {3,0,3,10,5},
               {INF,3,0,4,3},
               {8,10,4,0,20},
               {9,5,3,20,0}};
//保存頂點i到狀態s最後回到起始點的最小距離
int dp[N][M] ;
//保存路徑
vector<int> path;

//核心函數,求出動態規劃dp數組
void TSP(){
    //初始化dp[i][0]
    for(int i = 0 ; i < N ;i++){
        dp[i][0] = g[i][0];
    }
    //求解dp[i][j],先跟新列在更新行
    for(int j = 1 ; j < M ;j++){
        for(int i = 0 ; i < N ;i++ ){
            dp[i][j] = INF;
            //如果集和j(或狀態j)中包含結點i,則不符合條件退出
            if( ((j >> (i-1)) & 1) == 1){
                continue;
            }
            for(int k = 1 ; k < N ; k++){
                if( ((j >> (k-1)) & 1) == 0){
                    continue;
                }
                if( dp[i][j] > g[i][k] + dp[k][j^(1<<(k-1))]){
                    dp[i][j] = g[i][k] + dp[k][j^(1<<(k-1))];
                }
            }
        }
    }

}
//判斷結點是否都以訪問,不包括0號結點
bool isVisited(bool visited[]){
    for(int i = 1 ; i<N ;i++){
        if(visited[i] == false){
            return false;
        }
    }
    return true;
}
//獲取最優路徑,保存在path中,根據動態規劃公式反向找出最短路徑結點
void getPath(){
    //標記訪問數組
    bool visited[N] = {false};
    //前驅節點編號
    int pioneer = 0 ,min = INF, S = M - 1,temp ;
    //把起點結點編號加入容器
    path.push_back(0);

    while(!isVisited(visited)){
        for(int i=1; i<N;i++){
            if(visited[i] == false && (S&(1<<(i-1))) != 0){
                if(min > g[i][pioneer] + dp[i][(S^(1<<(i-1)))]){
                    min = g[i][pioneer] + dp[i][(S^(1<<(i-1)))] ;
                    temp = i;
                }
            }
        }
        pioneer = temp;
        path.push_back(pioneer);
        visited[pioneer] = true;
        S = S ^ (1<<(pioneer - 1));
        min = INF;
    }
}
//輸出路徑
void printPath(){
    cout<<"最小路徑爲:";
    vector<int>::iterator  it = path.begin();
    for(it ; it != path.end();it++){
        cout<<*it<<"--->";
    }
    //單獨輸出起點編號
    cout<<0;
}

int main()
{
    TSP();
    cout<<"最小值爲:"<<dp[0][M-1]<<endl;
    getPath();
    printPath();
    return 0;
}

 

八、測試結果及性能分析 

時間複雜度:O(2^{n}n^{2})

空間複雜度:O(2^{n})

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