1. 程式人生 > >使用動態規劃求解旅行商問題

使用動態規劃求解旅行商問題

  旅行商問題是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/