文章目录
论文来源:(2016)An Exact Algorithm for the Elementary Shortest Path Problem with Resource Constraints
作者:Leonardo Lozano 等人
一、摘要
- 资源约束下的初等最短路径问题(ESPPRC)是车辆路径问题列生成中经常出现的np难问题。
- 我们提出了一种基于隐式枚举的精确求解方法,该方法具有一种新颖的边界方案,极大地缩小了搜索空间。
- 我们将我们的算法嵌入到一个列生成中,以解决具有时间窗口的车辆路径问题(VRPTW)的线性松弛(根节点),并发现与著名的所罗门VRPTW试验台上的最先进的ESPPRC算法相比,所提出的算法表现良好。
二、我自己的理解
2.1 Pulse 简介
Pulse 算法是 Lozano 等人于2016年提出的用于解决资源受限最短路问题的一个高效精确算法。
该算法与常规标号算法不同的是,它基于深度优先的搜索框架,并结合多种剪枝策略来终止不能得到最优解的搜索,起到修剪搜索空间的作用,从而快速找到最优解。
脉冲算法分为两个阶段:定界阶段和脉冲阶段。
大体思想为通过定界过程确定在一定资源下每个节点的最低cost,然后pulse进行路径搜索,而之前bound求出来的最低cost就可以在pulse搜索的过程中起到定界的作用,从而筛选掉一些不好的路径。
下图为Pulse算法的伪代码:
2.2 定界阶段
在这一阶段, 建立一个矩阵, 来存储每个点在已知一定资源消耗前提下, 余下路径 cost 的下界。为了后续介绍, 我们定义下界矩阵为 B , B [ i , q ˉ ] \mathrm{B}, B[i, \bar{q}] B,B[i,qˉ] 表示在剩余载客量为 q ˉ \bar{q} qˉ 的 情况下, 从站点 i \mathrm{i} i 到达最终目的地的最低成本。
下图为定界过程的伪代码:
2.3 脉冲阶段
利用深度优先搜索, 递归传播脉冲, 直到脉冲扩展到终点或者被剪掉才停止。每当 有一个 pulse 到达终点, 就更新当前最优解, 直到所有可能都尝试过为止, 就找到了问 题的最优解。这一过程中如果没有剪枝策略, 数据量是非常庞大的, 所以合理有力的剪枝策略显得尤为重要
下图为脉冲阶段的伪代码:
2.4 三种剪枝策略
2.4.1 不可行剪枝策略
不可行剪枝策略 (Infeasibility Pruning): 在 pulse 过程到达任一点时, 不可行剪枝 策略会判断加入该点是否会构成环、是否会超出容量约束, 若有任意一条违背, 就会停 止当前 pulse 过程。
2.4.2 边界剪枝策略
边界剪枝策略 (Bounds Pruning): 设当前 pulse 过程到达站点 i i i, 当前成本消耗为 c c c, 当前载客量为 p p p, 当前考虑车型的最大载客量为 Q Q Q, 则剩余载客量为 q ˉ = Q − p \bar{q}=Q-p qˉ=Q−p, 以 剩余载客量 q ˉ \bar{q} qˉ 从站点 i \mathrm{i} i 到达终点的最低花费为 l c = B [ i , q ˉ ⌉ l_c=B[i, \bar{q}\rceil lc=B[i,qˉ⌉, 当前最优解成本为 l u l_u lu, 如果 满足不等式 c + l c ⩾ l u c+l_c \geqslant l_u c+lc⩾lu, 则说明该路径继续扩展下去也不会产生比当前最优解更好的解, 故路径可以被剪枝。
2.4.3 回滚剪枝策略
回滚剪枝策略 (Rollback Pruning): 假设 pulse 过程当前点为点 i \mathrm{i} i, 接下来到达点 n n n, 回㳖剪枝策略会先回退到点 i \mathrm{i} i 的前驱节点 j \mathrm{j} j, 评估从点 j \mathrm{j} j 直接到点 n \mathrm{n} n 是否是更好的选择, 如果是, 那么当前路径可以被剪枝。设 c i j c_{i j} cij 为点 i \mathrm{i} i 到点 j \mathrm{j} j 所需要花费的成本, 则回滚剪枝 条件的数学表达式如下:
c j i + … + c q n ⩾ c j n , q 为 n 的前驱节点 c_{j i}+\ldots+c_{q n} \geqslant c_{j n}, q \text { 为 } n \text { 的前驱节点 } cji+…+cqn⩾cjn,q 为 n 的前驱节点
下图为回滚剪枝策略的示意图:
三、Java代码复现
文章中有提到定界过程可以用多线程加速。所以我复现的版本也加入了多线程功能。是否开启多线程由变量 multiThread 控制。但是有时候开启多线程会更慢,我也不知道为什么,所以大家用的时候可以试一下开启和不开启多线程哪个更快用哪个。
下面是 Pulse 算法的代码
public class Pulse {
// 距离矩阵
double[][] distance;
// 起点
int start;
// 终点
int end;
// 当前车容量
int capacity;
// 人数
int[] pArr;
// 误差
double error = 1e-06;
// 最佳节点
Node[][] bestNodeArr;
// 是否开启多线程
boolean multiThread;
//创建锁对象
static final Object lock = new Object();
/**
* @param distance: 距离矩阵
* @param start: 起点索引
* @param end: 终点索引
* @param capacity: 车的容量约束
* @param pArr: 每个地点的乘客数量
* @param multiThread: 是否开启多线程(有时候开启多线程会更慢,我也不知道为什么)
*/
public Pulse(double[][] distance, int start, int end, int capacity, int[] pArr, boolean multiThread) {
this.distance = distance;
this.start = start;
this.end = end;
this.capacity = capacity;
this.pArr = pArr;
this.multiThread = multiThread;
}
public Node getInitNodeByFirstIndex(int firstIndex) {
List<Integer> initPath = new ArrayList<>();
initPath.add(firstIndex);
int[] initUsed = new int[distance.length];
initUsed[firstIndex]++;
return new Node(firstIndex, 0,
pArr[firstIndex],
initPath, initUsed);
}
public void solve() {
bestNodeArr = new Node[distance.length][capacity + 1];
bound();
pulse(getInitNodeByFirstIndex(start), capacity);
System.out.println("最短路径为:" + bestNodeArr[start][capacity].getCurPath());
System.out.println("最短路程长度为:" + bestNodeArr[start][capacity].getPathReducedCost());
CheckUtil.checkPath(bestNodeArr[start][capacity].getCurPath(), distance, bestNodeArr[start][capacity].getPathReducedCost(), pArr, capacity);
}
public void bound() {
int minCapacity = 1;
// int maxCapacity = Math.min(capacity, 20);
int maxCapacity = capacity / 2;
// int maxCapacity = capacity - 1;
int curCapacity = minCapacity;
int stepSize = 1;
if (multiThread) {
// 获取线程池
ThreadPoolExecutor threadPoolExecutor = createThreadPool();
ArrayList<CompletableFuture> completableFutureList = new ArrayList<>();
while (curCapacity <= maxCapacity) {
int finalCurCapacity = curCapacity;
for (int i = 0; i < distance.length; i++) {
int finalI = i;
completableFutureList.add(CompletableFuture.supplyAsync(() -> {
if (finalI != start && finalI != end) {
pulse(getInitNodeByFirstIndex(finalI), finalCurCapacity);
}
return 0;
}, threadPoolExecutor));
}
curCapacity += stepSize;
}
CompletableFuture<Void> allOf = CompletableFuture.allOf(completableFutureList.toArray(new CompletableFuture[0]));
try {
allOf.get();
} catch (Exception e) {
e.printStackTrace();
}
} else {
while (curCapacity <= maxCapacity) {
for (int i = 0; i < distance.length; i++) {
if (i != start && i != end) {
pulse(getInitNodeByFirstIndex(i), curCapacity);
}
}
curCapacity += stepSize;
}
}
}
int c = 0;
public void pulse(Node curNode, int curCapacity) {
// 还没到达终点,继续脉冲
if (isFeasible(curNode.getCurIndex(), curNode.getPeopleSum(), curNode.getCurPath(), curNode.getUsed(), curCapacity)) {
// 到达终点,更新最佳路线
if (curNode.getCurIndex() == end) {
synchronized (lock) {
c++;
int firstIndex = curNode.getCurPath().get(0);
int cs = curCapacity - pArr[firstIndex];
if (bestNodeArr[firstIndex][cs] == null || curNode.getPathReducedCost() <= bestNodeArr[firstIndex][cs].getPathReducedCost() - error) {
bestNodeArr[firstIndex][cs] = copyNode(curNode);
}
return;
}
}
if (!rollback(curNode)) {
List<Node> nodeList = new ArrayList<>();
for (int i = 0; i < curNode.getUsed().length; i++) {
if (i != start && curNode.getUsed()[i] == 0) {
if (curCapacity != capacity || checkBounds(i, curNode.peopleSum, curNode.pathReducedCost, curNode.curPath, curCapacity)) {
nodeList.add(getNextNode(curNode, i));
}
}
}
nodeList.sort(new Comparator<Node>() {
@Override
public int compare(Node o1, Node o2) {
return Double.compare(o1.getPathReducedCost(), o2.getPathReducedCost());
}
});
for (Node node : nodeList) {
pulse(node, curCapacity);
}
}
}
}
public Node getNextNode(Node curNode, int nextIndex) {
int pNum = pArr[nextIndex];
List<Integer> curPath = new ArrayList<>(curNode.getCurPath());
curPath.add(nextIndex);
int[] used = curNode.getUsed().clone();
used[nextIndex]++;
return new Node(
nextIndex,
curNode.getPathReducedCost() + distance[curNode.getCurIndex()][nextIndex],
curNode.getPeopleSum() + pNum,
curPath,
used
);
}
public boolean isFeasible(int curIndex, int peopleSum, List<Integer> curPath, int[] used, int curCapacity) {
if (used[curIndex] >= 2) {
return false;
}
return peopleSum <= curCapacity;
}
public boolean checkBounds(int nextIndex, int peopleSum, double pathReducedCost, List<Integer> curPath, int curCapacity) {
int firstIndex = curPath.get(0);
int initPNum = curCapacity - pArr[firstIndex];
double ub = Double.MAX_VALUE;
for (int i = initPNum; i >= 0; i--) {
if (bestNodeArr[firstIndex][i] != null) {
ub = bestNodeArr[firstIndex][i].getPathReducedCost();
break;
}
}
if (ub == Double.MAX_VALUE) {
return true;
}
int cs = curCapacity - peopleSum - pArr[nextIndex];
if (cs < 0) {
return false;
}
for (int i = cs; i <= curCapacity; i++) {
if (bestNodeArr[nextIndex][i] != null) {
return bestNodeArr[nextIndex][i].getPathReducedCost() + pathReducedCost + distance[curPath.get(curPath.size() - 1)][nextIndex] <= ub;
}
}
return true;
}
public boolean rollback(Node curNode) {
if (false) {
// 1
if (curNode.getCurPath().size() >= 3) {
List<Integer> path = new ArrayList<>(curNode.getCurPath());
path.remove(curNode.getCurPath().size() - 2);
double r = r(path);
return r <= curNode.getPathReducedCost() - error;
} else {
return false;
}
} else {
// 2
if (curNode.getCurPath().size() >= 3) {
int lastIndex = curNode.getCurPath().get(curNode.getCurPath().size() - 1);
List<Integer> path = new ArrayList<>();
for (int i = 0; i < curNode.getCurPath().size() - 2; i++) {
if (!path.isEmpty()) {
path.remove(path.size() - 1);
}
path.add(curNode.getCurPath().get(i));
path.add(lastIndex);
double r = r(path);
if (r <= curNode.getPathReducedCost() - error) {
return true;
}
}
}
return false;
}
}
// 根据路径获取reducd cost
public double r(List<Integer> path) {
if (path.size() <= 1) {
return 0d;
}
double r = 0d;
for (int i = 0; i < path.size() - 1; i++) {
r += distance[path.get(i)][path.get(i + 1)];
}
return r;
}
public Node copyNode(Node in) {
return new Node(in.curIndex, in.pathReducedCost, in.peopleSum, new ArrayList<>(in.curPath), in.used.clone());
}
@Data
@AllArgsConstructor
class Node {
// 当前点索引
int curIndex;
double pathReducedCost;
int peopleSum;
List<Integer> curPath;
int[] used;
}
// 创建线程池
private ThreadPoolExecutor createThreadPool() {
// 核心线程数,会一直占用在线程池中
int corePoolSize = 6;
// 最大线程数(控制资源)
int maximumPoolSize = 2000;
// 存活时间(单位与unit一致,类型为long)。如果当前线程数大于核心线程数
long keepAliveTime = 2000L;
// unit:时间单位 这里使用毫秒
// MILLISECONDS(毫秒),SECONDS(秒),MINUTES(分钟),HOURS(小时),DAYS(天)
TimeUnit timeUnit = TimeUnit.MILLISECONDS;
// 阻塞队列(如果没有空闲线程,且当前线程已经达到max,则进入阻塞队列等待)
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<Runnable>(300);
// threadFactory: 线程的创建工厂
ThreadFactory defaultThreadFactory = Executors.defaultThreadFactory();
// handler: 如果阻塞队列满了,则按照我们指定的拒绝策略执行任务
// 自带的几种拒绝策略:
// 1. AbortPolicy:当任务添加到线程池中被拒绝时,它将抛出 RejectedExecutionException 异常(拒绝新来的任务,并抛出异常)
// 2. CallerRunsPolicy:当任务添加到线程池中被拒绝时,会在线程池当前正在运行的Thread线程池中处理被拒绝的任务。(直接允许Run方法,同步调用)
// 3. DiscardPolicy:当任务添加到线程池中被拒绝时,线程池将丢弃被拒绝的任务。(拒绝新来的任务,但不抛异常)
// 4. DiscardOldestPolicy:当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。(为接收新任务,丢弃最老的任务)
ThreadPoolExecutor.CallerRunsPolicy policy = new ThreadPoolExecutor.CallerRunsPolicy();
// 创建线程池
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
timeUnit,
blockingQueue,
defaultThreadFactory,
policy);
return threadPoolExecutor;
}
}
下面是检验结果是否正确的代码:CheckUtil
public class CheckUtil {
public static void checkPath(List<Integer> path, double[][] distance, double target, int[] pArr, int c) {
double dis = 0d;
int p = pArr[path.get(0)];
for (int i = 1; i < path.size(); i++) {
dis += distance[path.get(i - 1)][path.get(i)];
p += pArr[path.get(i)];
}
if (Math.abs(dis - target) > 1e-06) {
LogUtil.error("路径长度计算错误:" + path + " , " + target + " , 正确长度为:" + dis);
}
if (p > c) {
LogUtil.error("超出容量约束,人数为: " + p + " , 但约束为: " + c);
}
}
public static void checkPath(int[] path, double[][] distance, double target, int[] pArr, int c) {
double dis = 0d;
int p = pArr[path[0]];
for (int i = 1; i < path.length; i++) {
dis += distance[path[i-1]][path[i]];
p += pArr[path[i]];
}
if (Math.abs(dis - target) > 1e-06) {
LogUtil.error("路径长度计算错误:" + path + " , " + target + " , 正确长度为:" + dis);
}
if (p > c) {
LogUtil.error("超出容量约束,人数为: " + p + " , 但约束为: " + c);
}
}
}