好码分享:开源算法框架Open Tabu Search求解VRPTW的JAVA代码

简介: 好码分享:开源算法框架Open Tabu Search求解VRPTW的JAVA代码

一、前言

很早之前就想写这篇文章了,由于各种不可描述的原因拖延到了现在,今儿就把坑给填上吧~

前排提示:小伙伴们可以收藏下这篇文章哦,说不定那天你们就用上了。因为真的是很干货哦!

640.jpg

二、Open TS算法框架

做元启发式的小伙伴都知道,一开始需要学习一些固定的算法框架,这是理论基础。有了理论基础以后就可以针对各种奇奇怪怪问题把这些算法框架给套上去,总能跑出一些结果,无论是好的差的。

经常有很多人来问我,这个问题用什么算法比较好啊?那个问题用什么框架比较合适?一开始我还很耐心的跟他们扯淡说:没有最好的,只有更好的。。。其实不是的,按照我这几年做启发式的经验来说,算法框架这东西其实吧,只要是一个类别的,基本都不会有太大差别(比如TS和SA、LNS和ALNS、GA和AFAS等等)。我们做算法开发的时候,更应该把重心放在一些问题特性的推导,或者搜索算子的思考上。

网络异常,图片无法展示
|

好了又扯远了……今天的主题是分享一份代码,一个开源的Tabu Search框架,以及如何利用该框架进行二次开发。先介绍下今天的主角:Open TS。这个是由Robert Harder开发的一个基于java平台tabu search算法框架。用他的话说就是:

Use these classes to help build a  structured and efficient Java tabu search.

640.png

该package包含了实现一个tabu search框架需要的各种元素,可以说得上非常全面了。大家在编写tabu search相关的算法时,只需要extend相关的class或者implements相关的interface即可。

640.png

这就使得我们可以将更多的时间和精力放在算子的设计以及其他问题特性的考虑上,而不是将大量的时间浪费在维护算法框架上。当然了,这个框架由于考虑了很多general的东西,同时也做了很多额外的异常处理什么的从而使得代码更为健壮。thus,代码的速度可能就会有一点点损失。

嗯……我这里指的损失是相对那种超级大神级别的人来说的,毕竟他们写代码会把各种冗余的计算去掉,把所有的可能slow down算法速度的因素都杜绝掉,恨不得直接用汇编写的那种……咱这些普通打工人也还没到那么牛逼的地步嘛!

640.png

总之,这个算法框架还是非常牛逼的,平时撸撸论文,做做项目直接拿来做二次开发也是一个不错的选择啦。

三、二次开发

其实上面给了很多类,但是对于一个单线程的tabu search来说,并不需要用到所有的class,只需要继承一些基本的元素,然后针对你的问题将他们special化就行啦。下面我介绍下二次开发的要实现的一些东西吧。

1. SolutionAdapter

求解任何问题,首先还是要定义该问题的solution结构了。只需要extend Open TS的SolutionAdapter类即可,该类中只有一个成员变量为:

private double[] objectiveValue;

为该解的目标值向量,然后你就可以在你自己的solution中定义问题的其他变量了。比如存储路径啊,解的其他中间变量等等。

2. TabuSearchListener

该类呢为接口类,里面有几个抽象方法需要实现的,分别为:

public void newBestSolutionFound(TabuSearchEvent event) {}

找到一个全局最优解时,要做的事情可以写在里面。

public void newCurrentSolutionFound(TabuSearchEvent event) {}

找到一个新的当前解时要做的事情可以写在这个函数。

public void tabuSearchStarted(TabuSearchEvent event) {

算法开始时触发的事件。

public void tabuSearchStopped(TabuSearchEvent event) {}

算法结束时触发的事件。

基本上重新写一下上面几个抽象方法就可以满足大部分的需求了。当然里面也定义了一些nonimprove相关的时间,可以作为shake使用。

3. ObjectiveFunction

该interface比较重要,继承以后需要实现下面这个抽象方法:

public double[] evaluate(Solution solution, Move proposedMove) {}

它表示评估当前解solution经过proposedMove以后形成的邻居解的目标值向量,就是前面SolutionAdapter中那个objectiveValue哦。这是什么意思呢,其实在算法实现中,我们一般不直接生成邻居解,而是生成一个称之为的东西,这是个什么东西呢,画个图:

640.png

其中我用紫色标出来的就是一个,简单来说他记录了生成邻居解需要对当前解进行的一些操作,比如exchange(6, 15)。

因此每次生成邻域时,可以先生成邻居解对应的,然后去评估每个对应的邻居解的cost,以比较各个邻居解的好坏。

4. ComplexMove

为interface,该算法框架的邻居解是通过当前解+move获得的,因此你的问题中设计的operator算子需要实现该接口,它有一些抽象方法如下:

public abstract void operateOn( Solution soln );

该方法其实是更上一层extends来的,表示对该move对soln执行相应的操作,比如exchange(1, 2)或者relocate(1, 3)等等。

public abstract int[] attributesDelete();

执行该move时,删除一个元素时返回的信息提供给tabu list记录。比如{1,  3}表示exchange了1和3,那么tabu list就要记录起来,防止后面的迭代中再进行exchange(1, 3)这样的操作。

public abstract int[] attributesInsert();

执行该move时,插入一个元素时返回的信息提供给tabu list记录。和删除时类似的。

5. MoveManager

这也是一个interface,是不是很爽,只需要implements相关的接口即可完成一个算法,简直不要太轻松!它的抽象方法只有一个:

public Move[] getAllMoves( Solution solution ) { }

这个方法是需要我们自己实现的,而且非常重要,因为这里定义了我们设计的算子所生成的move集合。

我觉得这个框架最好的地方就是这里了,他把所有的move都放在一起集中进行管理,后面进行约束变更的时候只需要修改这里的生成规则即可。比如客户i不能插入路径j,那么你在这里生成的时候就进行这些限制即可。

6. ComplexTabuList

这是一个类,表示tabu search中的禁忌表,里面有一个多维的tabu list可以记录很多信息:

private int[][][][][] tabuList;      // Data structure used to store list

同时该类已经实现了setTabu和isTabu的方法。这两个方法需要结合之前设置的attributesInsert()和attributesDelete()方法一起使用,如果做出修改那么需要修改相应的这几部分,特别是tabu list要进行修改的话。。。

四、实例

好了以上就是一些简单的介绍,当然这样介绍可能大家没什么感觉,因为这东西在没有对代码有一个很好的全局掌控之前,很难体会到其中的精髓,反而很多人因为其中巨大的代码量感觉极为繁琐。

毕竟用别人的东西,万一出错了都不知道怎么调。这里呢为了让大家更好的熟悉这个框架,我贴上了一个使用该框架实现一个求解VRPTW问题的例子,这个代码是来源于GitHub(好像是意大利都灵理工大学一些masters的课程大作业吧……)原链接为oma-vrptw。

https://github.com/oma-vrptw/oma-vrptw

这个代码本身也有很多值得借鉴参考的地方的,比如它里面实现了一个relocate(代码中叫SWPA MOVE,但是我觉得relocate更合适点)算子,在评估一个move的时候就用到了此前我们讲过的以O(n)复杂度计算邻居解的一些操作:

如何实现一个高效的启发式算法?(VRPTW篇)

这个算子的效果还可以的,在Solomon的标准算例中C系列大部分能跑到最优,速度更是快得飞起。大家阅读源码时照着我上面贴出来的思路看即可。算例呢我也整合好了,我对源代码做了一些修改,使得他能够正常运行(不然待会又有很多人跑来问我代码咋不能运行呢?),更改算例在以下位置即可更改。

640.png

单线程tabu search的主体呢是在SingleThreadedTabuSearch这个类中,执行一次迭代的逻辑都写在了protected void performOneIteration()这个方法里面。

其实要写的比较高效的话,每个算子生成的move都应该定制好自己单独的evaluate函数,示例只写了一个算子,如果move是由多个算子生成的话,需要判断下move属于哪个算子的,然后进行相应的evaluate,可以更改ObjectiveFunction的evaluate函数成如下形式:

640.png

当然啦,你也可以修改框架中的代码以达到更多个性化的功能,不过我是不太推荐这样做的,因为别人封装好的东西,你一整的话,出错了都不知道去哪里找。不过熟悉以后可以尝试修改一下底层的代码。我就对那个tabu list进行了修改,因为感觉给的那个不是很好用吧~

五、代码下载

我把修改过的代码放在了GitHub上,地址为

https://github.com/dengfaheng/omatest

好了,大家可以慢慢去看代码了。。。have a nice day!看在小编这么勤劳的份上,帮我点个在看呗~万一你的老板喜欢看微信的看一看,看到你又在微信上学习代码,ta肯定要高兴得不得了呀!你就可以大胆告诉他:

微信图片_20220423180058.gif

相关文章
|
6天前
|
Java
在 Java 中捕获和处理自定义异常的代码示例
本文提供了一个 Java 代码示例,展示了如何捕获和处理自定义异常。通过创建自定义异常类并使用 try-catch 语句,可以更灵活地处理程序中的错误情况。
|
14天前
|
SQL 监控 数据可视化
完全开源!国内首个完全开源JAVA企业级低代码平台
JeeLowCode 是一款专为企业打造的 Java 企业级低代码开发平台,通过五大核心引擎(SQL、功能、模板、图表、切面)和四大服务体系(开发、设计、图表、模版),简化开发流程,降低技术门槛,提高研发效率。平台支持多端适配、国际化、事件绑定与动态交互等功能,广泛适用于 OA、ERP、IoT 等多种管理信息系统,帮助企业加速数字化转型。
|
16天前
|
jenkins Java 测试技术
如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例详细说明
本文介绍了如何使用 Jenkins 自动发布 Java 代码,通过一个电商公司后端服务的实际案例,详细说明了从 Jenkins 安装配置到自动构建、测试和部署的全流程。文中还提供了一个 Jenkinsfile 示例,并分享了实践经验,强调了版本控制、自动化测试等关键点的重要性。
48 3
|
17天前
|
分布式计算 Java MaxCompute
ODPS MR节点跑graph连通分量计算代码报错java heap space如何解决
任务启动命令:jar -resources odps-graph-connect-family-2.0-SNAPSHOT.jar -classpath ./odps-graph-connect-family-2.0-SNAPSHOT.jar ConnectFamily 若是设置参数该如何设置
|
16天前
|
Java
Java代码解释++i和i++的五个主要区别
本文介绍了前缀递增(++i)和后缀递增(i++)的区别。两者在独立语句中无差异,但在赋值表达式中,i++ 返回原值,++i 返回新值;在复杂表达式中计算顺序不同;在循环中虽结果相同但使用方式有别。最后通过 `Counter` 类模拟了两者的内部实现原理。
Java代码解释++i和i++的五个主要区别
|
10天前
|
安全 Java 测试技术
Java并行流陷阱:为什么指定线程池可能是个坏主意
本文探讨了Java并行流的使用陷阱,尤其是指定线程池的问题。文章分析了并行流的设计思想,指出了指定线程池的弊端,并提供了使用CompletableFuture等替代方案。同时,介绍了Parallel Collector库在处理阻塞任务时的优势和特点。
|
19天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
6天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
25 9
|
9天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
6天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin