目錄
1 問題描述
何為旅行商問題?按照非專業的說法,這個問題要求找出一條n個給定的城市間的最短路徑,使我們在回到觸發的城市之前,對每個城市都只訪問一次。這樣該問題就可以表述為求一個圖的最短哈密頓回路的問題。(哈密頓回路:定義為一個對圖的每個頂點都只穿越一次的回路)
很容易看出來,哈密頓回路也可以定義為n+1個相鄰頂點v1,v2,v3,...,vn,v1的一個序列。其中,序列的第一個頂點和最后一個頂點是相同的,而其它n-1個頂點都是互不相同的。並且,在不失一般性的前提下,可以假設,所有的回路都開始和結束於相同的特定頂點。因此,可以通過生成n-1個中間城市的組合來得到所有的旅行線路,計算這些線路的長度,然后求取最短的線路。下圖是該問題的一個小規模實例,並用該方法得到了它的解,具體如下:
圖1 使用蠻力法求解旅行商問題
2 解決方案
2.1 蠻力法
此處使用蠻力法解決旅行商問題,取的是4個城市規模,並已經定義好各個城市之間的距離(PS:該距離使用二維數組初始化定義,此處的距離是根據圖1中所示距離定義)。此處主要是在體驗使用蠻力法解決該問題的思想,如要豐富成普遍規模問題,還請大家自己稍微修改一下噠。對於代碼中如碰到不能理解的地方,可以參考文章末尾給出的參考資料鏈接,以及相關代碼注解~
具體代碼如下:
package com.liuzhen.chapterThree; public class TravelingSalesman { public int count = 0; //定義全局變量,用於計算當前已行走方案次數,初始化為0 public int MinDistance = 100; //定義完成一個行走方案的最短距離,初始化為100(PS:100此處表示比實際要大很多的距離) public int[][] distance = {{0,2,5,7},{2,0,8,3},{5,8,0,1},{7,3,1,0}}; //使用二維數組的那個音圖的路徑相關距離長度 /* * start為開始進行排序的位置 * step為當前正在行走的位置 * n為需要排序的總位置數 * Max為n!值 */ public void Arrange(int[] A,int start,int step,int n,int Max){ if(step == n){ // 當正在行走的位置等於城市總個數時 ++count; //每完成一次行走方案,count自增1 printArray(A); //輸出行走路線方案及其總距離 } if(count == Max) System.out.println("已完成全部行走方案!!!,最短路徑距離為:"+MinDistance); else{ for(int i = start;i < n;i++){ /*第i個數分別與它后面的數字交換就能得到新的排列,從而能夠得到n!次不同排序方案 * (PS:此處代碼中遞歸的執行順序有點抽象,具體解釋詳見本人另一篇博客:) *算法筆記_017:遞歸執行順序的探討(Java)
*/ swapArray(A,start,i); Arrange(A,start+1,step+1,n,Max); swapArray(A,i,start); } } } //交換數組中兩個位置上的數值 public void swapArray(int[] A,int p,int q){ int temp = A[p]; A[p] = A[q]; A[q] = temp; } //輸出數組A的序列,並輸出當前行走序列所花距離,並得到已完成的行走方案中最短距離 public void printArray(int[] A){ for(int i = 0;i < A.length;i++) //輸出當前行走方案的序列 System.out.print(A[i]+" "); int tempDistance = distance[A[0]][A[3]]; //此處是因為,最終要返回出發地城市,所以總距離要加上最后到達的城市到出發點城市的距離 for(int i = 0;i < (A.length-1);i++) //輸出當前行走方案所花距離 tempDistance += distance[A[i]][A[i+1]]; if(MinDistance > tempDistance) //返回當前已完成方案的最短行走距離 MinDistance = tempDistance; System.out.println(" 行走路程總和:"+tempDistance); } public static void main(String[] args){ int[] A = {0,1,2,3}; TravelingSalesman test = new TravelingSalesman(); test.Arrange(A,0,0,4,24); //此處Max = 4!=24 } }
運行結果:
0 1 2 3 行走路程總和:18
0 1 3 2 行走路程總和:11
0 2 1 3 行走路程總和:23
0 2 3 1 行走路程總和:11
0 3 2 1 行走路程總和:18
0 3 1 2 行走路程總和:23
1 0 2 3 行走路程總和:11
1 0 3 2 行走路程總和:18
1 2 0 3 行走路程總和:23
1 2 3 0 行走路程總和:18
1 3 2 0 行走路程總和:11
1 3 0 2 行走路程總和:23
2 1 0 3 行走路程總和:18
2 1 3 0 行走路程總和:23
2 0 1 3 行走路程總和:11
2 0 3 1 行走路程總和:23
2 3 0 1 行走路程總和:18
2 3 1 0 行走路程總和:11
3 1 2 0 行走路程總和:23
3 1 0 2 行走路程總和:11
3 2 1 0 行走路程總和:18
3 2 0 1 行走路程總和:11
3 0 2 1 行走路程總和:23
3 0 1 2 行走路程總和:18
已完成全部行走方案!!!,最短路徑距離為:11
2.2 減治法
旅行商問題的核心,就是求n個不同城市的全排列,通俗一點的話,就是求1~n的全排列。下面兩種方法都是基於減治思想進行的,此處只實現求取1~n的全排列。對於每一種排列,在旅行商問題中還得求取其相應路徑長度,最后,在進行比較從而得到最短路徑,對於求取最短路徑的思想在2.1蠻力法中已經體現,此處不在重復,感興趣的同學可以自己再動手實現一下~
2.2.1 Johson-Trotter算法
此處算法思想借用《算法設計與分析基礎》第三版上講解,具體如下:
具體實現代碼如下:
package com.liuzhen.chapter4; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; public class Arrange { //使用JohnsonTrotter算法獲取1~n的全排列 public HashMap<Integer , String> getJohnsonTrotter(int n){ HashMap<Integer , String> hashMap = new HashMap<Integer , String>(); int count = 0; //用於計算生成排列的總個數,初始化為0 int[] arrayN = new int[n]; int[] directionN = new int[n+1]; //directionN[i]用於標記1~n中數字i上的箭頭方向,初始化值為0,表示箭頭方向向左;值為1 表示箭頭方向向右 for(int i = 0;i < n;i++) arrayN[i] = i+1; String result = getArrayString(arrayN); hashMap.put(count, result); //將原始排列添加到哈希表中 while(judgeMove(arrayN,directionN)){ //存在一個移動元素 int maxI = getMaxMove(arrayN,directionN); if(directionN[arrayN[maxI]] == 0) //箭頭指向左方 swap(arrayN,maxI,--maxI); if(directionN[arrayN[maxI]] == 1) //箭頭指向右方 swap(arrayN,maxI,++maxI); for(int i = 0;i < n;i++){ //調轉所有大於arrayN[maxI]的數的箭頭方向 if(arrayN[i] > arrayN[maxI]){ if(directionN[arrayN[i]] == 0) directionN[arrayN[i]] = 1; else directionN[arrayN[i]] = 0; } } count++; result = getArrayString(arrayN); hashMap.put(count, result); //將得到的新排列添加到哈希表中 } return hashMap; } //判斷數組arrayN中是否存在可移動元素 public boolean judgeMove(int[] arrayN,int[] directionN){ boolean judge = false; for(int i = arrayN.length - 1;i >= 0;i--){ if(directionN[arrayN[i]] == 0 && i != 0){ //當arrayN[i]數字上的箭頭方向指向左邊時 if(arrayN[i] > arrayN[i-1]) return true; } if(directionN[arrayN[i]] == 1 && i != (arrayN.length-1)){ //當arrayN[i]數字上的箭頭方向指向右邊時 if(arrayN[i] > arrayN[i+1]) return true; } } return judge; } //獲取數組arrayN中最大的可移動元素的數組下標 public int getMaxMove(int[] arrayN,int[] directionN){ int result = 0; int temp = 0; for(int i = 0;i < arrayN.length;i++){ if(directionN[arrayN[i]] == 0 && i != 0){ //當arrayN[i]數字上的箭頭方向指向左邊時 if(arrayN[i] > arrayN[i-1]){ int max = arrayN[i]; if(max > temp) temp = max; } } if(directionN[arrayN[i]] == 1 && i != (arrayN.length-1)){ //當arrayN[i]數字上的箭頭方向指向右邊時 if(arrayN[i] > arrayN[i+1]){ int max = arrayN[i]; if(max > temp) temp = max; } } } for(int i = 0;i < arrayN.length;i++){ if(arrayN[i] == temp) return i; } return result; } //交換數組中兩個位置上的數值 public void swap(int[] array,int m,int n){ int temp = array[m]; array[m] = array[n]; array[n] = temp; } //把數組array中所有元素按照順序以字符串結果返回 public String getArrayString(int[] array){ String result = ""; for(int i = 0;i < array.length;i++) result = result + array[i]; return result; } public static void main(String[] args){ Arrange test = new Arrange(); HashMap<Integer , String> hashMap = test.getJohnsonTrotter(3); Collection<String> c1 = hashMap.values(); Iterator<String> ite = c1.iterator(); while(ite.hasNext()) System.out.println(ite.next()); System.out.println(hashMap); } }
運行結果:
123
132
312
321
231
213
{0=123, 1=132, 2=312, 3=321, 4=231, 5=213}
2.2.2 基於字典序的算法
此處算法思想也借用《算法設計與分析基礎》第三版上講解,具體如下:
具體實現代碼如下:
package com.liuzhen.chapter4; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; public class Arrange1 { public HashMap<Integer,String> getLexicographicPermute(int n){ HashMap<Integer,String> hashMap = new HashMap<Integer,String>(); int count = 0; //用於計算生成排列的總個數,初始化為0 int[] arrayN = new int[n]; for(int i = 0;i < n;i++) arrayN[i] = i+1; String result = getArrayString(arrayN); hashMap.put(count, result); //將原始排列添加到哈希表中 while(riseTogetherArray(arrayN)){ //數組中存在兩個連續的升序元素 int i = getMaxI(arrayN); //找出使得ai<ai+1的最大i: ai+1>ai+2>...>an int j = getMaxJ(arrayN); //找到使得ai<aj的最大索引j: j>=i,因為ai<ai+1 swap(arrayN,i,j); reverseArray(arrayN,i+1,arrayN.length-1); result = getArrayString(arrayN); count++; hashMap.put(count, result); //將新得到的排列添加到哈希表中 } System.out.println("排列總個數count = "+(count+1)); return hashMap; } //判斷數組中是否 包含兩個連續的升序元素 public boolean riseTogetherArray(int[] arrayN){ boolean result = false; for(int i = 1;i < arrayN.length;i++){ if(arrayN[i-1] < arrayN[i]) return true; } return result; } //返回i:滿足ai<ai+1,ai+1>ai+2>...>an(PS:an為數組中最后一個元素) public int getMaxI(int[] arrayN){ int result = 0; for(int i = arrayN.length-1;i > 0;){ if(arrayN[i-1] > arrayN[i]) i--; else return i-1; } return result; } //返回j:ai<aj的最大索引,j>=i+1,因為ai<ai+1(此處i值為上面函數getMaxI得到值) public int getMaxJ(int[] arrayN){ int result = 0; int tempI = getMaxI(arrayN); for(int j = tempI+1;j < arrayN.length;){ if(arrayN[tempI] < arrayN[j]){ if(j == arrayN.length-1) return j; j++; } else return j-1; } return result; } //交換數組中兩個位置上的數值 public void swap(int[] array,int m,int n){ int temp = array[m]; array[m] = array[n]; array[n] = temp; } //將數組中a[m]到a[n]一段元素反序排列 public void reverseArray(int[] arrayN,int m,int n){ while(m < n){ int temp = arrayN[m]; arrayN[m++] = arrayN[n]; arrayN[n--] = temp; } } //把數組array中所有元素按照順序以字符串結果返回 public String getArrayString(int[] array){ String result = ""; for(int i = 0;i < array.length;i++) result = result + array[i]; return result; } public static void main(String[] args){ Arrange1 test = new Arrange1(); HashMap<Integer,String> hashMap = test.getLexicographicPermute(3); Collection<String> c1 = hashMap.values(); Iterator<String> ite = c1.iterator(); while(ite.hasNext()) System.out.println(ite.next()); System.out.println(hashMap); } }
運行結果:
排列總個數count = 6
123
132
213
231
312
321
{0=123, 1=132, 2=213, 3=231, 4=312, 5=321}
參考資料: