回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

問題描述

某售貨員要到若干城市去推銷商品,已知各城市之間的路線(或旅費)。要選定一條從駐地出發,經過每個城市一遍,最後回到駐地的路線,使總的路程(或總旅費)最小。本文只考慮4個城市的情況,下面這個帶權圖即爲問題的轉化。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

由於只有4個城市,如果規定售貨員總是從城市1出發,那麼依據排列組合可以得到6種不同的旅行方案,比如12341、13241等等。在這些排列組合基礎上可以很容易繪製出一棵排列樹,也是該問題的解空間樹,排列樹如下:

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

根據解空間樹可以得到一些有用的信息:

  • 該樹的深度爲5
  • 兩個節點之間路徑上的標識數組代表所走城市
  • 樹上沒有體現出回到城市1的路徑,但實際上計算要考慮這段路程

下面利用回溯法和分支限界法分別求解該問題,對於回溯法會給出相應的Python實現代碼,對於分支限界法會採用優先隊列式介紹求該問題最優解的步驟。

回溯法

回溯法有點類似於暴力枚舉的搜索過程,回溯法的基本思想是按照深度優先搜索的策略,從根節點出發深度搜索解空間樹,當搜索到某一節點時,如果該節點可能包含問題的解,則繼續向下搜索;反之回溯到其祖先節點,嘗試其他路徑搜索。

如果問題只要求求得一個可行解,那麼搜索到問題的一個解即可結束;如果問題所求是最優解,那麼需要搜索整個解空間樹,得到所有解之後擇最優作爲問題的解,或者在搜索到葉子節點之前已經能確定該路徑不爲最優解時就可以進行剪枝,節省搜索時間,那麼本文的旅行售貨員問題屬於後者。

解空間樹除了葉子節點之外的每一個節點都可能擁有多個子節點,而回溯法每次搜索只能搜索一條路徑,這也是與分支限界法的區別,並且這能更好的幫助理解下面圖片的講解。

下圖中的F和L與原解空間樹有些偏差,但對步驟和結果沒有影響

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

首先已知售貨員於城市1起步,第一步可以畫下任意一條可行解,不受任何約束,上圖的路徑爲12341,算上F->A路程總和爲59,這也是當前的最優解。由於L只有F一個孩子節點,那麼只能回溯至C節點,因爲C還有另一條路徑可供搜索。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

可以看到此時搜索的路徑爲12431,算上M->A路程總和爲66,此時路程總和是大於最優解的,所以最優解仍爲59,而節點C所有孩子節點搜索結束,那麼需要向上回溯到C的祖先節點B。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

此時的路徑爲13241,並且路程總和爲25,當前解是遠遠小於之前所得最優解,所以需要更新最優解爲25,然後回溯到節點D,原理同上。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

可以看到這張圖中的路徑比之前幾張圖多了一個X(叉),也是這一步實現了對數的剪枝,依據呢?因爲城市1到城市3再到城市4的路程總和爲26,無論怎麼走也不會優於當前的最優解,所以Duck不必繼續向下搜索,直接回溯即可。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

解空間樹剩下兩條路徑可以按照上述方法繼續搜索,當遍歷所有可行路徑之後,最後得到的最優解best_length就爲該問題的全局最優解,對應的路徑即爲全局最優路徑。

下面介紹如何用代碼實現回溯法搜索問題最優解,這裏會省略一些定義變量之類的簡單代碼,只介紹關鍵部分,完整代碼在文末會給出獲取方式。

首先需要做的一定是對地圖轉化,對於這個圖我們可以得到其對應的鄰接矩陣如下:
$$
\begin{pmatrix}
-1 & 30 & 6 & 4 \
30 & -1 &5 & 10 \
6 & 5 & -1 & 20 \
4 & 10 & 20 & -1 \
\end{pmatrix}
$$

City_Graph = [[0, 0, 0, 0, 0],
              [0,-1, 30, 6, 4],
              [0,30, -1, 5, 10],
              [0,6, 5, -1, 20],
              [0,4, 10, 20, -1]]
path_num = len(City_Graph) 
isin = [0]*(path_num) #用來檢測該節點是否已經添加到路徑中
path = [0]*(path_num) #用於儲存路徑
best_path = [0]*(path_num) #用於儲存最優路徑
best_length = 100000 #初始化最優路徑的路程總和

那麼可以一個二維數組存儲該地圖,可以看到這與圖所對應的鄰接矩陣有些不同,數組的第一行與第一列都爲0,爲什麼這麼做呢?舉個例子,例如城市1到城市2的路程爲30,在鄰接矩陣中對應(1,2),此時在二維數組對應的索引也爲(1,2),填0就是爲了統一索引,並且這裏城市本身位置填入-1,表示不存在路徑。

整個程序可由一個條件分割成兩個部分,向下搜索還是向上回溯,如果先不計剪枝操作,那麼搜索到解空間樹的葉子節點就是向上回溯的標誌,中間節點一般則是做向下搜索操作。

而現在的問題就是這個條件是什麼,對於一個由1、2、3...n構成的解空間樹,它是由[1:n]所有排列構成,如果我們暫定搜索樹的深度爲t的話,會有下面兩種情況。

  • 當t<=n時,即當前擴展節點位於第t-1層:如果在t-1層的擴展節點與在t層的擴展節點之間有路徑存在,並且[1:t]對應路徑的路程總和小於最優解,則向下繼續搜索,否則進行剪枝。
  • 當t>n時,即當前節點爲葉子節點:整個路徑只差回到城市1,這時需要判斷是否存在這個迴路,如果存在且得到的路程總和優於當前最優解,那麼需要更新當前最優解。

這兩種情況只是這麼說可能會有一些抽象,可以結合上面的解空間樹加以理解。可以看到t>n就是判斷是否需要回溯的條件,也就是本文條件下的t>path_num-1。

if t > path_num-1:  # 搜索至葉子節點
    for i in range(1, path_num):  # 輸出當前路徑
        ThePath += str(path[i])
        ThePath += '->'
    temp = int(ThePath.split('->')[3])
    if City_Graph[temp][1] > 0: # 判斷是否存在迴路
        ThePath += '1'  # 路徑加上回路回到城市1
        print("當前路徑:%s" % ThePath)
        back_length = now_length+City_Graph[temp][1] # 迴路路程也需要相加
        print("當前路徑總和:%d" % back_length)
        if back_length < best_length: #更新最優解
            for i in range(1, path_num):
                best_path[i] = path[i]
            best_length = back_length
            BestPath = ThePath # 更新最優路徑
        return #返回

如果t>path_num-1條件成立,就說明解空間樹其中一條路徑搜索完成,所以需要輸出路徑和對應的路程總和。但在輸出之前,需要判斷在地圖中這條路徑是否存在回到城市1的路徑,若存在迴路,就輸出當前搜索路徑及其對應的路程總和。如果當前路程總和優於當前最優解,則需要做更新最優解和最優路徑的操作。

else:
    for j in range(1, path_num):
        if City_Graph[path[t - 1]][j] != -1 and (not isin[j]): #兩城市間存在路徑並且還未走過
            isin[j] = 1 #表示該城市已經來過
            path[t] = j #將該城市存至path中
            now_length = now_length + City_Graph[path[t - 1]][j] #加路徑對應路程和
            TSP(t+1)
            isin[j] = 0
            path[t] = 0
            now_length = now_length - City_Graph[path[t - 1]][j]

如果條件不成立,則說明正在搜索樹的中間節點。對於每一條正在搜索的路徑,如果兩城市之間存在路徑,並且還未走過該路徑,則需要將其填入路徑path中,並將這個路徑對應權值需要加至當前路程總和now_length中,然後繼續向下搜索。

這部分代碼最後三行的作用是在進行回溯操作時,需要將相應節點的數據還原。比如路徑12341,回溯搜索對應12431,在回溯之前需要將3和4對應數據還原才方便加入4和3對應數據。

可以看到上面代碼中的循環都是從1開始,這也是爲了與上文二維數組中的地圖索引相對應。最後運行程序可以得到每條可行路徑及其對應路程總和。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

在程序中有設置最優解及最優路徑參數,所以可以直接調用並輸出:

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

分支限界法

分支限界法是利用廣度優先搜索的策略或者以最小耗費(最大效益)優先的方式搜索問題的解空間樹,對於解空間樹中的活節點只有一次機會成爲拓展節點,活節點一旦成爲擴展節點,那麼將一次性產生其所有兒子節點。

對於優先隊列式的分支限界法,這些兒子節點中,不可行解或者一定不能成爲最優解的兒子節點會被捨棄,其餘兒子節點將會按照優先級依次存入一個活節點表(隊列),此後會挑出活節點表優先級最高的節點作爲下次擴展節點,重複此過程,直至找到問題的最優解。

下面講解圖片中會有兩個新的簡寫變量,首先聲明一下對應含義:

  • nl:now_length,當前所走路程長度。
  • Lb:lower bound,所有可行解的下界,即每一個節點出邊之和。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

左上角爲圖的鄰接矩陣,右上角爲活節點表,當擴展節點爲B時,他需要一次性產生自己的所有子節點。可以看到B出的Lb爲18,這個18怎麼得到的呢?就是每一行或者每一列除-1之外最小權值相加,即4+5+5+4=18。下界通常不是一個可行解,但是它爲可行解提供了一個界定,方便找出最小消耗的那條路徑(最優解)。

對於B的子節點,共有3條路徑,可以看到C、D、E對應的Lb都爲14,這是因爲從城市1到另外3個城市的路徑已經確定,所以下界也應該減去4。這就好比最初的下界是我們猜測的4個最短路徑相加,但是現在第一個路徑的實際值已經確定了,那麼下界就應該減去第一個路徑的猜測值,此時的下界就是後三個路徑猜測值的總和。

此時應該將C、D、E三個節點按照優先級存入活節點表,路程總和越小則優先級越高,當前的路程總和=nl+Lb。根據計算節點E的優先級最高,所以E也是下次的擴展節點。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

對於擴展節點E,路徑1和4的值已經確定了,所以子節點的Lb=18-4-4=10,計算兩個子節點當前路程總和,並按照優先級插入活節點表,D成爲下次的擴展節點。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

對於擴展節點D,路徑1和3的值已經確定了,所以子節點的Lb=18-4-5=9,按照上述方法更新活節點表,H成爲下次的擴展節點。

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

因爲H是葉子節點的父節點,所以需要判斷地圖中是否有從4回到1的迴路,如果存在迴路,那麼整個路徑組合就是該問題的一個可行解,並且它也是當前的最優解,但並不一定是全局的最優解。

因爲此時K節點對應的路程總和爲24,是要優於當前最優解25的,所以還需將節點N插入至活節點表中,按照上述方法,讓K成爲擴展節點繼續向下搜索,最後得到的路程總和也爲25,並不優於當前最優解。那麼按照活節點表的順序,節點N再次成爲擴展節點,如下圖:

回溯法、分支限界法兩種思想幫你輕鬆搞定旅行售貨員問題(TSP)

自此活節點表中沒有節點優先級高於N,所以節點N對應的路徑13241成爲最優路徑,對應的路程總和25成爲問題的最優解。

總結

回溯法和分支限界法都是在問題的解空間樹上搜索問題解的算法。回溯法主要利用深度優先搜索策略,通常目標是找到問題的所有可行解;分支限界法主要利用廣度優先搜索策略,通常目標是儘快找到一個滿足問題約束條件的解,所以對於TSP這類求最優解的問題,分支限界法比回溯法更加適合。

公衆號【奶糖貓】後臺回覆“TSP”可獲取文中提及的源碼供參考

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