代码黑科技的分享区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源创计划”,欢迎正在阅读的你也加入,一块儿分享。