乍一看標題,大家是不是覺得“動態(tài)規(guī)劃”這四個字組合在一起有點眼熟?似乎哪會兒學過來著……但是吧,細細一琢磨,又忘了它具體是什么、怎么用、用來解決哪些問題了。 莫方,小編出現(xiàn)就是為了解決大家一切在學(zhuang)習(bi)上的需求的。動態(tài)規(guī)劃忘了是吧,那今天小編就陪你好好回憶一下。 什么是TSP和動態(tài)規(guī)劃 簡單來說,Travelling Salesman Problem (TSP) 是最基本的路線問題。它尋求的是旅行者由起點出發(fā),通過所有給定的需求點后,再次返回起點所花費的最小路徑成本,也叫旅行商問題、旅行推銷員問題、貨郎擔問題…… 當然,如果你非要把TSP理解成“內(nèi)容服務提供者”(Telematics Service Provider)小編也不會打你……計算機網(wǎng)絡學得不錯啊,四級過了嗎? 說完TSP問題,咱們再來聊聊什么是動態(tài)規(guī)劃。 動態(tài)規(guī)劃算法(Dynamic Programming,簡稱DP)通常用于求解具有某種最優(yōu)性質(zhì)的問題,其基本思想是將待求解問題分解成若干個子問題,先求解子問題,然后由這些子問題的解再得到原問題的解。 看到這里想必你已經(jīng)明白了,動態(tài)規(guī)劃恰是一種求解TSP問題的好方法,具體如何求解,我們可以舉例實操一下。 實例操作 假設現(xiàn)在有四個城市,它們分別是0,1,2,3,他們之間往來的代價如下圖所示: 為了方便起見,我們把它化成二維表的形式: 好了這里要敲黑板劃橫線了!現(xiàn)在,我們要從城市0出發(fā),期間1,2,3每個城市都必須經(jīng)過并且只能經(jīng)過一次,最后回到0,使得路上花費的代價最小。請問你要怎么走? 事實上,這是一個最基本的TSP問題,且構(gòu)成最優(yōu)子結(jié)構(gòu)性質(zhì),所以可以使用動態(tài)規(guī)劃求解,下面來驗證一下此方法求解的可行性。 設 s,s1,s2…s為滿足題意的最短回路。假設從s到s1的路徑已經(jīng)確定,則問題轉(zhuǎn)化為從s1到s的最短路徑問題。而很顯然,s1,s2…s一定可以構(gòu)成一條最短路徑,所以構(gòu)成最優(yōu)子結(jié)構(gòu)性質(zhì),可以用動態(tài)規(guī)劃求解。 明確問題可解,那下一步就是列方程求解了。 簡單推導一下動態(tài)規(guī)劃方程 用 V’ 表示一個點的集合,假設從頂點 s 出發(fā), d ( i , V’ ) 表示當前到達頂點 i,經(jīng)過 V’ 集合中所有頂點一次的最小花費。 ① 當 V’ 為僅包含起點的集合,也就是: d ( s , { s } ) = 0 ; ② 其他情況,則對子問題求最優(yōu)解。需在 V’ 這個城市集合中,嘗試每一個城市結(jié)點,并求出最優(yōu)解。 ③ 最后的求解方式為: 其中 S 為包含所有點的集合。 把公式一套,題就解了。是不是很簡單?但是,小編還有更簡單的方法。 其實,絕大部分TSP問題都比例子中復雜許多,用程序求解是更好的選擇。在這里小編給大家提供一種較為簡單的方法,只要把動態(tài)規(guī)劃算法原理掌握好了,代碼自然就不難理解了。 用代碼前,你需要做哪些準備? 理解狀態(tài)壓縮DP 所謂狀態(tài)壓縮,就是利用二進制以及位運算來實現(xiàn)對于本來應該很大的數(shù)組的操作。而求解動態(tài)規(guī)劃問題,很重要的一環(huán)就是狀態(tài)的表示,一般來說,一個數(shù)組即可保存狀態(tài)。但是有這樣的一些題目,它們具有DP問題的特性,但是狀態(tài)中所包含的信息過多,如果要用數(shù)組來保存狀態(tài)的話需要四維以上的數(shù)組。于是,我們就需要通過狀態(tài)壓縮來保存狀態(tài),而使用狀態(tài)壓縮來保存狀態(tài)的DP就叫做狀態(tài)壓縮DP。 例題TSP的動態(tài)規(guī)劃方程中,V’ 是一個集合,而對于集合的狀態(tài)表示最簡單的辦法就是利用C++中STL里的set,但是這個時候就要考慮一個問題,在代碼實現(xiàn)的時候,我們不能用一個集合去做一個數(shù)組的下標。自然而然,我們想到可以利用集合的特征值,但這個方法很復雜,而且不容易實現(xiàn)。 小編在這里給大家普及一下位運算的知識。最簡單的與(and),或 ( or ),非 ( not ), 大家都很熟悉,和邏輯電路是相通的。而對于異或 ( xor ), 則是很有趣的一種位運算,它的運算規(guī)則是相同為 0,不同為 1。例如: 1 xor 1 = 0,0 xor 0 = 0, 1 xor 0 = 1,0 xor 1 = 1; 它的運算滿足交換律以及結(jié)合律。除此之外,xor 還有很多神奇的操作,有興趣的同學可以自己去查閱。 再復雜一點的有左移 ( shl ), 右移 ( shr ),相當于對于二進制數(shù)的位置移動。例如10001(2) shl 1,就是10001(2)左移一位,變成了100010(2),換算成十進制,相當于擴大了 2 倍,同理右移則是縮減兩倍。那么對于任意的一個二進制數(shù),左移 k 位就是乘 2k, 右移就是整除 2k 。 |||| 小貼士: 在C++中,位運算操作符分別是: 與 &,或 |,非 ~,異或 ^, 左移 ,右移 >> 推到動態(tài)規(guī)劃方程時,我們注意到 V’ 是一個數(shù)的集合,而且解決的問題規(guī)模比較小,于是可以用一個二進制數(shù)來存儲這個集合。簡單來說就是——如果城市 k 在集合 V’ 中,那么存儲集合的變量 i 的第 k 位就為 1,否則為 0。由于有 n 個城市,所有的狀態(tài)總數(shù)我們用 M 來表示,那么很明顯:M = 2^n,而 0 到 2^n -1 的所有整數(shù)則構(gòu)成了 V’ 的所有狀態(tài)。這樣,結(jié)合位運算,動歸方程的狀態(tài)表示就很容易了。 準備所需工具 還有兩樣你需要準備的東西,那就是城市數(shù)據(jù)文件和編譯軟件。代碼中使用的城市數(shù)據(jù)文件可以有兩種保存格式:一種是上例提到的矩陣式,也可以是 “城市名 城市X坐標 城市Y坐標” 式。大家可以根據(jù)實際情況自行調(diào)整。 至于編譯軟件,小編在這里給大家提供的是C++代碼,用你用得最順手的編譯器就可以了。小編在這里強烈推薦DEV-CPP!體積小,編譯方便,代碼還很美觀。 好了不啰嗦了,上代碼~ 代碼示例(C++) #include using namespace std; // 定義常量 const int INF = 0x3f3f3f3f; #define sqr(x) ((x)*(x)) // 定義變量 string file_name; int type; // type == 1 滿秩矩陣格式, type == 2 二維坐標式 int s; int N;// 城市結(jié)點數(shù)量 int init_point; double **dp; // 動態(tài)規(guī)劃狀態(tài)數(shù)組dp[i][j],i表示集合V’,j表示當前到達的城市結(jié)點 double **dis; // 兩個城市結(jié)點之間的距離 double ans; // 定義結(jié)構(gòu)體 struct vertex{ double x, y; // 城市結(jié)點的坐標 int id; // 城市結(jié)點的id int input(FILE *fp){ return fscanf(fp, '%d %lf %lf', &id, &x, &y); } }*node; double EUC_2D(const vertex &a, const vertex &b){ return sqrt(sqr(a.x - b.x) + sqr(a.y - b.y)); } void io(){ // 數(shù)據(jù)讀入 printf('input file_name and data type\n'); cin >> file_name >> type; FILE *fp = fopen(file_name.c_str(), 'r'); fscanf(fp, '%d', &N); node = new vertex[N + 5]; dis = new double*[N + 5]; if (type == 1){ for (int i = 0; i < n;="" i="">{ dis[i] = new double[N]; for (int j = 0; j < n;="" j=""> fscanf(fp, '%lf', &dis[i][j]); } } else{ for (int i = 0; i < n;="" i=""> node[i].input(fp); for (int i = 0; i < n;="" i="">{ dis[i] = new double[N]; for (int j = 0; j < n;="" j=""> dis[i][j] = EUC_2D(node[i], node[j]);// 計算城市之間的距離 } } fclose(fp); return; } void init(){ // 數(shù)據(jù)初始化 dp = new double*[(1 < n)="" +=""> for(int i = 0; i < (1="">< n);="">{ dp[i] = new double[N + 5]; for(int j = 0; j < n;=""> dp[i][j] = INF; } // 初始化,除了dp[1][0],其余值都為INF ans = INF; return; } double slove(){ int M = (1 <> // M就是第四部分所說的V’狀態(tài)總數(shù),1<> dp[1][0] = 0; // 假設固定出發(fā)點為0,從0出發(fā)回到0的花費為0。TSP只要求是一個環(huán)路,所以出發(fā)點可以任選 for (int i = 1; i < m;="" i="">{ // 枚舉V’的所有狀態(tài) for (int j = 1; j < n;="" j="">{ // 選擇下一個加入集合的城市 if (i & (1 < j))=""> // 城市已經(jīng)存在于V’之中 if (!(i & 1)) continue; // 出發(fā)城市固定為0號城市 for (int k = 0; k < n;="" k="">{ // 在V’這個城市集合中嘗試每一個結(jié)點,并求出最優(yōu)解 if (i & (1 <>{ // 確保k已經(jīng)在集合之中并且是上一步轉(zhuǎn)移過來的結(jié)點 dp[(1 < j)="" |="" i][j]="min(dp[(1">< j)="" |="" i][j],="" dp[i][k]="" +="" dis[k][j]);="">// 轉(zhuǎn)移方程 } // 將j點加入到i集合中 } } } for (int i = 0; i < n;="" i=""> ans = min(dp[M - 1][i] + dis[i][0], ans); // 因為固定了出發(fā)點,所以要加上到城市0的距離。另外要從所有的完成整個環(huán)路的集合V’中選擇,完成最后的轉(zhuǎn)移 return ans; } int main(){ io(); init(); string tmp = file_name + '.sol'; FILE *fp = fopen(tmp.c_str(), 'w'); fprintf(fp, '%.2lf\n', slove()); delete[] dp; delete[] node; delete[] dis; fclose(fp); return 0; } 算例運行 例1:滿秩矩陣式(type==1) 就拿前文的例子,其文件存儲的格式應如下: 4 0 3 6 7 5 0 2 3 6 4 0 2 3 7 5 0 運行結(jié)果 11 例2:二維坐標式(type==2) 若城市數(shù)據(jù)文件如下所示: 16 運行結(jié)果 73.99 總結(jié) 動態(tài)規(guī)劃通過迭代方式尋找每一個子問題的最優(yōu)解法,因此該解法可以得出TSP的最優(yōu)解。但算法時間效率較差,因此在問題規(guī)模逐漸變大的過程中計算量會急劇膨脹。所以,本算法只適用于小規(guī)模求精確解的TSP問題,但對于你平常遇到的大多數(shù)TSP問題,這也足夠了。 所以,你學會了嗎? |
|