【论文阅读及复现】(2016)An Exact Algorithm for the Elementary Shortest Path Problem with Resource Constraints


论文来源:(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ˉ=Qp, 以 剩余载客量 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+lclu, 则说明该路径继续扩展下去也不会产生比当前最优解更好的解, 故路径可以被剪枝。

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++cqncjn,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);
        }
    }
}

猜你喜欢

转载自blog.csdn.net/weixin_51545953/article/details/127654808