使用動態規劃求解旅行商問題
旅行商問題是np問題,在集合表示那裡用set去實現效率很很低,而且要儲存的數都是不重複的比較小的整數,所以這裡用二進位制串表示集合。比如集合{1,3,5,6,7}表示成二進位制串用1110101,其中集合裡面有的數對應的位數寫成1,沒有的寫成0。要判斷第3位是不是1,就把 1110101右移(3-1)位,得到11101,然後結果和00001進行 & 運算,如果結果是1說明第3位是1,否則說明第3位是0。
推廣一下,對於數字x,要看它的第i位是不是1,那麼可以通過判斷布林表示式 (((x >> (i - 1) ) & 1) == 1的真值來實現。
對於下面這個測試用例,圖和鄰接矩陣如下,不能走的話用-1表示,實際儲存的時候用一個比較大的數字,比如0x7ffff:
要使用動態規劃,需要問題本身有最優子結構,我們需要找到要解決的問題的子問題。
題目要求,從0出發,經過[1,2,3]這幾個城市,然後回到0,使得花費最少。要實現這個要求,需要從下面三個實現方案中選擇花費最少的方案。
1、 從0出發,到1,然後再從1出發,經過[2,3]這幾個城市,然後回到0,使得花費最少。
2、 從0出發,到2,然後再從2出發,經過[1,3]這幾個城市,然後回到0,使得花費最少。
3、 從0出發,到3,然後再從3出發,經過[1,2]這幾個城市,然後回到0,使得花費最少。
可以發現,三個小的解決方案的最優解,構成了大的解決方案,所以這個問題具有最優子結構,可以用動態規劃來實現。
設定一個二維的動態規劃表dp,定義符號{1,2,3}表示經過[1,2,3]這幾個城市,然後回到0。
那麼題目就是求dp[0][{1,2,3}]。將{1,2,3}表示成二進位制,就是111,對應10進位制的7,所以題目是在求dp[0][7];
要求三個方案的最小值意味:
dp[0][{1,2,3}] = min{ C01+dp[1][{2,3}] ,C02+dp[2][{1,3}] ,C03+dp[3][{1,2}]}
其中C01 表示從0出發到1的距離。
dp[1][{2,3}] = min{ C12+dp[2][{3}] ,C13+dp[3][{1}]}
dp[2][{3}] = C23+dp[3][{}]
dp[3][{}]就是從3出發,不經過任何城市,回到0的花費,所以dp[3][{}] = C30
先確定一下dp表的大小,有n個城市,從0開始編號,那麼dp表的行數就是n,列數就是2^(n-1),即1 << (n – 1),集合{1,2,3}的子集個數。在求解的時候,第一列的值對應這從鄰接矩陣可以匯出,後面的列可以有前面的列和鄰接矩陣匯出。所以求出的動態規劃表就是:
j = 0 第一輪
for(int i =0;i <n;i++){ dp[i][0] = C[i][0]; }
第二輪
j = 1; //可以把j帶入 for(int i = 0;i < n;i++){ dp[i][j] = C[i][1]+dp[1][0] }
後面的規律比較麻煩的一點在於要集合和二進位制轉換,觀察發現:
dp[2][5] 表示從2出發,通過{1,3},最後回到起點。那麼:
dp[2][5] = min{C21 + dp[1][{3}],C23 + dp[3][{1}]} = min{C21 + dp[1][4],C23 + dp[3][1]} ;
從2出發,要去{1,3}。
先看去1的路,去了1集合{1,3}中只剩下{3} ,{3}對應4,所以要求的dp表就是dp[1][4],這個4可以通過(101) ^ (1)得到,(1) = 1<<(1-1)
再看去2的路,5 = 101的第二位是0,所以不能去2。判斷第二位為1,用(5>>(2-1)) &1==1。而且也由於從2出發,就更不能去了。
最後看去3的路,去了3集合{1,3}中只剩下{1},{1}對應這1,所以要求的dp表就是dp[3][1],1通過(101) ^ (100)得到。(100) = 1<<(3-1)
同樣求dp[0][7] = min{C01 + dp[1][6], C02+ dp[2][5], C03 + dp[3][3]}
從0出發,要去{1,2,3}
先看去1的路,去1然後去6 = {2,3},6通過(111) ^ (1)得到,(1) = 1<<(1-1)
再看去2的路,去2然後去5 = {1,3},5通過(111) ^ (10)得到。(10) = 1<<(2-1)
最後看去3的路,去3然後去3 = {1,2},3通過(111) ^ (100)得到。(100) = 1<<(3-1)
還要注意,求dp[2][3]的時候。就是求從2出發,經過{1,2},顯然不合理,在dp表中應為-1。對於這種情況,只用判斷數字3的二進位制位的第2位是不是1,是1就表示不合理。
根據以上的推導,最後求dp表的演算法就是:
for(int j = 1;j < 1 << (n - 1);j++){ for(int i= 0;i < n;i++){ dp[i][j] = 0x7ffff; 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] > C[i][k] + dp[k][j ^ (1 << (k - 1))]){ dp[i][j] = C[i][k] + dp[k][j ^ (1 << (k - 1))]; } } } }
最終程式的返回值就是dp表左上角的那個數字return dp[0][(1<<(cityCount - 1)) - 1];
一個完整執行的例子是:
public class TravelingSalesman { public static void main(String[] args) { int cityCount = 4; int[][] roadInfo = new int[][]{ {0, 1, 10}, {1, 0, 10}, {1, 3, 25}, {3, 1, 25}, {3, 2, 30}, {2, 3, 30}, {0, 2, 15}, {2, 0, 15}, {1, 2, 35}, {2, 1, 35} }; int roadmap[][] = new int[cityCount][cityCount]; //轉成鄰接矩陣方便取數 int dp[][] = new int [cityCount][1 << (cityCount - 1)]; for(int i = 0;i < cityCount;i++){ for(int j = 0;j < cityCount;j++){ roadmap[i][j] = 0x7ffff; //用0x7ffff表示無窮大 } } for(int i = 0;i < roadInfo.length;i++){ //鄰接矩陣 roadmap[roadInfo[i][0]][roadInfo[i][1]] = roadInfo[i][2]; } for(int i =0;i <cityCount;i++){ //先求dp表第一列 dp[i][0] = roadmap[i][0]; //求出了每個城市回到起點的距離了。 } for(int j = 1;j < 1 << (cityCount - 1);j++){ //再求其他列 for(int i= 0;i < cityCount;i++){ //從i出發,要去包含j = {010101}的 城市 dp[i][j] = 0x7ffff; if(((j >> (i - 1)) & 1) == 1){ //如果已經到過j了,就continue continue; } for(int k = 1;k < cityCount;k++){ //看能不能先到k城市 if(((j >> (k - 1)) & 1) == 0){ continue; //不能先到k城市,continue; } if(dp[i][j] > roadmap[i][k] + dp[k][j ^ (1 << (k - 1))]){ dp[i][j] = roadmap[i][k] + dp[k][j ^ (1 << (k - 1))]; } } } } System.out.println(dp[0][(1<<(cityCount - 1)) - 1]); } }
若要顯示dp表,可以在求出dp表以後加上一下程式碼:
System.out.printf("%10d",0); for(int j = 0;j < 1 << (cityCount - 1) ;j++){ System.out.printf("%10d",j); } System.out.println(); for(int i = 0;i < cityCount;i++){ System.out.printf("%10d",i); for(int j = 0;j < 1 << (cityCount - 1) ;j++){ if(dp[i][j] == 0x7ffff) dp[i][j] = -1; System.out.printf("%10d",dp[i][j]); } System.out.println(); }
本文章純原創,轉載請註明出處:http://www.cnblogs.com/youmuchen/