代碼黑科技的分享區node
1、前言git
今年開年那會還在作一個課題的實驗,那時候想用large neighborhood search來作一個問題,可是後來發現常規的一些repair、destroy算子效果並非很好。後來才知道,large neighborhood search以及它的衍生算法,這類框架給人一種很是通用的感受,就是不管啥問題都能往裏面套。github
每每的結果是套進去效果也是通常。這也是不少剛入行的小夥伴常常喜歡乾的事吧,各類算法框架套一個問題,發現結果很差了就感受換下一個。最後復現了N多個算法發現依然no process,這時候就會懷疑人生了。其實要想取得好的performance,確定仍是要推導一些問題特性,設計相應的算子也好,鄰域結構也好。web
好了,回到正題。當時我試了好幾個large neighborhood search算子,發現沒啥效果的時候,內心難受得很。那幾天晚上基本上是轉輾反側,難以入眠,固然了是在思考問題。而後一個idea忽然浮如今個人腦瓜子裏,常規的repair算子難以在問題中取得好的performance,是由於約束太多了,插入的時候很容易違背約束。算法
在不違背約束的條件下又難以提高解的質量,我就想能不能插入的啥時候採起branch and bound。遍歷全部的可能插入方式,而後記錄過程當中的一個upper bound用來刪掉一些分支。編程
感受是有搞頭的,後來想一想,這個branch的方法以及bound的方法彷佛是有點難設計。而後又擱置了幾天,最後沒進展的時候忽然找了一篇論文,是好多年前的一篇文章了。裏面詳細講解了large neighborhood search中如何利用branch and bound進行插入,後來實現瞭如下感受還能夠。感受這個方法仍是有必定的參考價值的,所以今天就來寫寫(其實當時就想寫了,只不過一直拖到了如今。。。)微信
2、large neighborhood search
關於這個算法,我在此前的推文中已經有過相應的介紹,詳情小夥伴們能夠戳這篇的連接進行查看:app
自適應大鄰域搜索(Adaptive Large Neighborhood Search)入門到精通超詳細解析-概念篇框架
我把其中的一段話摘出來:eclipse
大多數鄰域搜索算法都明肯定義它們的鄰域。在LNS中,鄰域是由 和 方法隱式定義的。 方法會破壞當前解的一部分,然後 方法會對被破壞的解進行重建。 方法一般包含隨機性的元素,以便在每次調用 方法時破壞解的不一樣部分。
那麼,解 的鄰域 就能夠定義爲:首先經過利用 方法破壞解 ,而後利用 方法重建解 ,從而獲得的一系列解的集合。LNS算法框架以下:
有關該算法更詳細的介紹能夠參考Handbook Of Metaheuristics這本書2019版本中的Chapter 4 Large Neighborhood Search(David Pisinger and Stefan Ropke),文末我會放出下載的連接。
關於destroy算子呢,有不少種,好比隨機移除幾個點,貪心移除一些比較差的點,或者基於後悔值排序移除一些點等,這裏我給出文獻中的一種移除方式,Shaw (1998)提出的基於 進行移除:
假設須要從解中全部的 移除 個,它首先隨機選擇一個 放進 (已經移除的 列表)(第1行),而後迭代地(3–6行)移除剩下的 個 。每次這樣的迭代都會先從 中隨機選擇一個 ,並根據相關標準對其他未移除的 進行排序(第3-4行)。在第5行中計算要插入的新 的下標,而後插入到 中(第6行),直到迭代結束。關聯度的定義如Shaw(1998)所述:
其中,customer 和 在不一樣的路徑中時 ,不然爲0。
3、branch and bound
上面講了Large Neighborhood Search以及介紹了一個 方法,下面就是重頭戲,如何利用branch and bound進行插入了。
3.1 branch
其實插入的分支方式仍是挺好設計的,這玩意兒呢我將也比較難講清楚,我就畫圖好了,仍是基於VRP問題示例,其餘問題相似,假如咱們如今有這樣一個解 :
爲了演示我就不畫太多點太多路徑了,省得你們看得心累。
紅色箭頭就是可以插入的位置。如今,假如咱們插入 (因爲branch and bound是須要遍歷全部可能的插入組合,所以先插入哪一個後插入哪一個都是能夠的,可是分支定界的速度可能會受到很大的影響,這個我們暫時不討論):
爲了讓你們看得更加清楚,我把 的位置用粉紅色給標記出來了,一共有3條分支,有個候選的位置就有多少條分支。
如今,還剩下 ,插入 的時候,咱們須要繼續進行分支:
55~畫分支樹真是畫死我啦(你們必定要給個贊,點個在看呀~),能夠看到,最後每條路徑就是一個完成的解。在兩個點的 插入兩個點最後分支完成的 竟然有12個!!!你們能夠自行腦補下,在90個點的 中插入10個點最終造成的分支會有多少。毫無疑問會不少不少,多到你沒法想象。下面是DFS搜索分支樹的過程:
若是要插入的客戶組爲空,則能夠認爲全部客戶已經插入到solution中,造成了一個 ,所以判斷找到的一個upper bound是否比最優的upper bound還要好,是的話就對upper bound進行更新。不然,它會選擇插入效果最好的客戶,這會使目標函數降低得最大(Shaw 1998中也使用了這種啓發式方法)。而後,對全部插入客戶後造成的分支按照lower bound進行排序,從lower bound低的分支開始繼續往下分支(能夠算是一種加速的策略)。一樣,請注意,該算法僅探索其lower bound比upper bound更好的分支。
3.2 bound
開始以前你們想一想bound的難點在哪裏呢?首先想一想bound中最重要的兩個界:upper bound和lower bound:
-
lower bound是指搜索過程當中一個 partial solution(好比上圖插入 後造成的3個 )的目標值,由於 並不能算完整的一個解,繼續往下的時候只可能增長(最小化問題)或者減小(最大化問題),所以它的意思是說當前支路的最終造成解的目標值下界(最終目標值不可能比這個lower bound更好)。 -
upper bound是指搜索過程當中找到的一個 feasible solution(好比上圖插入 後造成的12個 中知足全部約束的就是 )的目標值,若是存在某支路的lower bound比upper bound還要差,那麼該支路顯然是沒有繼續往下的必要了,能夠剪去。
顯然可使用LNS在destroy以前的解的目標值做爲upper bound,由於咱們老是指望找到比當前解更好的解,纔會去進行destroy和repair。如今的問題是如何對一個 的lower bound應該怎樣計算。下面講講幾種思路:
(1) 文獻中給出的思路,利用最小生成樹:
這個方案我試了,可是找到的lower bound實在是過低了,這個lower bound只考慮了距離因素,但問題中每每還存在時間窗等約束。所以這個方法在我當時作的問題中只能說聊勝於無。
(2) 按照greedy的方法將全部未插入的Customer插入到他們最好的位置上,造成一個 ,而後該 的目標值做爲lower bound。
可是這個lower bound是有缺陷的,由於很難保證不會錯過某些比較有潛力的分支。
(3) 直接利用當前的 的目標值做爲lower bound,也比較合理。可是該值每每過低了,這可能會致使要遍歷更多的分支,消耗更多時間。
以上就是一些思路,至於有沒有更好的bound方法,我後面也沒有往下深究了。當時實現出來之後效果是有的,就是時間太長了,而後也放棄了。
固然這篇paper後面也給了一個利用LDS進行搜索以加快算法的速度,這裏就不展開了,有空再說。感興趣的小夥伴能夠去看看原paper,我會放到留言區的。
4、代碼環節
代碼實現放兩個,一個是我當時寫的一個DFSEXPLORER,採用的是思路2做爲bound的,(代碼僅僅提供思路)以下:
private void DFSEXPLORER5(LNSSolution node, LNSSolution upperBound, int dep) {
Queue<LNSSolution> queue = new LinkedList<LNSSolution>();
LNSSolution s_c_ = node;
queue.add(s_c_);
int es = 1;
while (!queue.isEmpty()) {
s_c_ = queue.remove();
//v是一個完整的解
if(s_c_.removalCustomers.isEmpty()) {
if(s_c_.cost < upperBound.cost && Math.abs(s_c_.cost-upperBound.cost)>0.001) {
//System.out.println("new found > "+s_c_.cost+" feasible = "+s_c_.feasible());
upperBound.cost = s_c_.cost;
upperBound.routes = s_c_.routes;
}
}else {
//System.out.println("l > "+s_c_.removalCustomers.size() + " cost = "+s_c_.cost);
double minIDelta = Double.POSITIVE_INFINITY;
int minIndex = -1;
Customer c=null;
for(int i = 0; i < s_c_.removalCustomers.size(); ++i) {
Customer cu = s_c_.removalCustomers.get(i);
double d1 = s_c_.minInsertionDeltas[cu.getCustomerNo()];
if(minIDelta > d1) {
minIDelta = d1;
c = cu;
minIndex = i;
}
}
ArrayList<LNSSolution> neighborI_c = new ArrayList<LNSSolution>();
for( int i = 0; i < s_c_.routes.length; ++i) {
Route route = s_c_.routes[i];
if(!MyUtil.checkCompatibility(c, route.getAssignedVehicle())) {
continue;
}
for (int j = 0; j <= route.getCustomersLength(); j++) {
LNSSolution s_i = s_c_.solClones();
s_i.insertCustomer(s_i.routes[i], s_i.removalCustomers.get(minIndex), j, minIndex);
//updateIDAfterOneInserted(s_i, s_i.routes[i]);
//s_i.calcLowerBound();
double o_c = s_i.lb;
updateInsertionDelta(s_i);
double n_c = s_i.lb;
//if(o_c != n_c)System.out.println("o = "+o_c+" n = "+n_c);
neighborI_c.add(s_i);
}
}
Collections.sort(neighborI_c);
for(LNSSolution s:neighborI_c) {
//System.out.println("lBound "+s.lb+" upperBound = "+upperBound.cost);
//updateInsertionDelta(s);
//s.calcLowerBound();
if(s.lb < upperBound.cost /*&& dep > 0*/) {
//System.out.println("lBound "+s.lb+" upperBound = "+upperBound.cost);
//System.out.println(s.removalCustomers.size());
queue.add(s);
es++;
dep--;
}
}
}
}
//System.out.println(es);
}
第二個是GitHub上找到的一我的復現的,我已經fork到個人倉庫中了:
https://github.com/dengfaheng/vrp
這個思路bound的思路呢沒有按照paper中的,應該仍是用的貪心進行bound。看起來在R和RC系列的算例中效果其實也通常般,由於用了LDS吧可能。下面是運行的c1_2_1的截圖:
導入idea或者eclipse後等他安裝完依賴,運行下面的文件便可,更改算例的位置如圖所示:
這個思路是直到借鑑的,你們在用LNS的時候也能夠想一想有什麼更好的bound方法。
好了,這就是今天的分享了。可能有不少地方沒寫的很明白,由於涉及的點太多了我也只能講個大概提供一個思路而已。你們來了就幫我點個在看再走吧~
推薦閱讀:
乾貨 | 學習算法,你須要掌握這些編程基礎(包含JAVA和C++)
本文分享自微信公衆號 - 程序猿聲(ProgramDream)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。