雙調歐幾里得旅行商問題

問題的描述如下(書中231頁):

平面上n個點,確定一條連接各點的最短閉合旅程。這個解的一般形式爲NP的(在多項式時間內可以求出)。

J.L. Bentley 建議通過只考慮雙調旅程(bitonic tours)來簡化問題,這種旅程即爲從最左點開始,嚴格地從左到右直至最右點,然後嚴格地從右到左直至出發點。下圖(b)顯示了同樣的7個點的最短雙調路線。在這種情況下,多項式的算法是可能的。事實上,存在確定的最優雙調路線的O(n*n)時間的算法。

閉合巡遊路線

                       (a)                               (b)

(a)圖是最短的閉合旅程,長度爲24.89。(b)圖是問題經簡化後,同樣的點集的最短雙調閉合旅程,長度爲25.58。

解題思路:

根據簡化後的雙調歐幾里得旅行問題的性質,將點集依據各點x座標單調遞增來進行編號,我們設b[i,j]是最短雙調閉合旅程P(i,j)的長度(i<=j),而最短雙條閉合旅程P(i,j)是指從點P[i]開始,嚴格地向左走(即是每次經過的點的x座標都比前一個點的x座標要小),直到最左點P[1],然後再嚴格向右走,直到終點P[j]爲止,在從P[i]到P[j]過程中的點有且只經過一次。設distance[i,j]是點P[i]到P[j]之間的歐式距離。

那麼,根據動態規劃的方法,我們要找出問題的最優子結構,並且遞歸定義出來,下面是問題的公式(大前提是i<=j):

b[1,2]=distance(1,2);最小的子問題,主要用於求解更大的子問題;

b[i,j]=b[i,j-1] + distance(j-1,j),如果i<j-1;

b[i,j]=min{ b[k,j-1] + distance(k,j) },其中1<=k<j-1,如果i=j-1;

下面講解公式的由來,最短雙調旅程P(i,j)在到達終點P[j]之前,正常來說,按照雙調的概念,一定經過了一個其x座標剛好比點P[j]小的點,也就是P[j-1](當然當i=j-1時就另當別論了),所以如果當i<j-1時,最短雙調旅程P(i,j)的長度應該可以看成是其子問題P(i,j-1)的長度和點distance(j-1,j)的和。但是當i=j-1時,問題就不同了,因爲我們不能一開始從點P[i]直接跳到P[j],因爲其他點都還沒有走過一次,所以在到達終點P[j]的前一個點不能再是點P[j-1](即P[i]),那如何是好呢?因爲在到達終點P[j]之前肯定要經過一個點的,但不知道是哪個點,我們不妨設該點是P[k],那麼k的範圍肯定是1<=k<j-1(因爲是除了點P[j-1]和P[j]之外的點),當然該點P[k]要使得雙調旅程P(i,j)的長度最短,於是在k的可能範圍中找,於是再使用一個min操作。i=j-1時的情況類似於矩陣鏈乘的問題,其實這也是動態規劃的慣用手法,假設一個最優選擇,然後再基於該最優選擇來定義問題。

這相當於我們每次求解問題P(i,j),都作了一次選擇,要麼是點P[j-1]或是點P[k]作爲P[j]的前一個點,其示意圖如下:

要知道,我們要求解的問題結果是b[n,n],於是

b[n,n]=b[n-1,n]+distance(n-1,n);

這裏要注意的一點,在之前的公式中,並不涉及求解類似b[i,i]的值,這裏定義了b[i,i]的情況。

除此之外,還涉及到了對最優解的重構的問題。我們將使用一個r[i][j]數組表示子問題P(i,j)在到達終點P[j]之前經過的一個點P[k]對應的k值(僅挨着點P[j]的點),則子問題的解可以組織爲其更小的子問題P(i,k)的解加上點P[k]和點P[j]。由之前的解題思路可知,對於問題P(i,j),當i=j-1時,k<i,當i<j-1時,k=j-1。

其實得到的最優解是個閉合旅程,所以從出發後的第一個點與到達之前的一個點的位置是等價的。如閉合旅程是76431257,也可以是75213467。

構造解的過程如下:

每次加入的點總是在序號大的點下,因爲問題P(i,j)總是分解爲子問題P(i,k),不管k是等於j-1,還是小於j-1,然後確定點P[k]是到達P[j]之前的一個點,這也是問題每次選擇的結果。使用一個數組存放序號,一邊從0開始,一邊從末尾開始。

以下是問題的實現:

  1. #include <iostream> 
  2. #include <cmath> 
  3. #include <fstream> 
  4. using namespace std; 
  5.  
  6. #define N 7 
  7. struct Point{ 
  8.     double x; 
  9.     double y; 
  10. }; 
  11. struct Point points[N+1]; 
  12. double b[N+1][N+1]; 
  13. int r[N+1][N+1]; 
  14.  
  15.  
  16. double distance(int i,int j);//第i,j點的歐式距離 
  17. double Euclidean_TSP();//最短閉合旅程長度 
  18. void my_print_path();//打印旅程 
  19.  
  20. void main(int argc, char **argv){ 
  21.     ifstream infile; 
  22.     infile.open("input.txt");//讀入一個有各點座標的文檔 
  23.     if (!infile) 
  24.     { 
  25.         cout<<"error!"<<endl; 
  26.     } 
  27.     int i=1; 
  28.     while (infile>>points[i].x>>points[i].y) 
  29.     { 
  30.         i++; 
  31.     } 
  32.     cout<<"最短雙調閉合旅程長度是:"<<Euclidean_TSP()<<endl; 
  33.     my_print_path(); 
  34.  
  35. double distance(int i,int j){ 
  36.     return sqrt((points[i].x-points[j].x)*(points[i].x-points[j].x) 
  37.         +(points[i].y-points[j].y)*(points[i].y-points[j].y)); 
  38.  
  39. double Euclidean_TSP(){ 
  40.     b[1][2]=distance(1,2);//最小的子問題 
  41.      
  42.     for (int j=3;j<=N;j++) 
  43.     { 
  44.         //i<j-1且i>=1時的情況 
  45.         for (int i=1;i<j-1;i++) 
  46.         { 
  47.             b[i][j] = b[i][j-1]+distance(j-1,j); 
  48.             r[i][j] = j-1; 
  49.         } 
  50.         //i=j-1的情況 
  51.         b[j-1][j] = b[1][j-1]+distance(1,j);//先設初值爲k=1時的值 
  52.         r[j-1][j] = 1; 
  53.         for (int k=1;k<j-1;k++) 
  54.         { 
  55.             double q = b[k][j-1]+distance(k,j); 
  56.             if (q < b[j-1][j]) 
  57.             { 
  58.                 b[j-1][j] = q; 
  59.                 r[j-1][j] = k; 
  60.             } 
  61.         } 
  62.     } 
  63.     b[N][N] = b[N-1][N]+distance(N-1,N); 
  64.     return b[N][N]; 
  65.  
  66. void my_print_path(){ 
  67.     int string[N]; 
  68.     string[0]=N; 
  69.     string[1]=N-1; 
  70.     int k=N-1; 
  71.     int left_hand=N-1,right_hand=N,begin=2,end=N-1; 
  72.     for (int i=N-1,j=N;k!=1;) 
  73.     { 
  74.         k=r[i][j]; 
  75.         if (left_hand>right_hand) //比較那邊的點的序號大
  76.         { 
  77.             left_hand=k; 
  78.             string[begin]=k; 
  79.             begin++; 
  80.         }else
  81.             right_hand=k; 
  82.             string[end]=k; 
  83.             end--; 
  84.         } 
  85.         if (i==j-1) 
  86.         { 
  87.             j=i; 
  88.             i=k; 
  89.         }else if (i<j-1) 
  90.         { 
  91.             j=k; 
  92.         } 
  93.     } 
  94.     cout<<"該旅程是:"
  95.     for (int index=0;index<N;index++) 
  96.     { 
  97.         cout<<string[index]; 
  98.     } 
  99.     cout<<endl; 
input.txt:

 

0 6
1 0
2 3
5 4
6 1
7 5
8 2

 

運行後:

最短雙調閉合旅程長度是:25.584

該旅程是:7643125

其實旅程也可以是7521346

 

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