基于or-tools解决物流调度问题(一)

简介: 基于or-tools解决物流调度问题(一)

or-tools简介

示例1:假设一家公司要拜访地图上所有的客户,现在需要规划出总里程最短的方案。其中0为公司,即起点,其他的点为客户,即终点。

使用or-tools的VRP工具来解决示例

要解决以上示例的问题,我们先来了解一下一些基本的概念

数据模型

static class DataModel {
    //距离矩阵
  public final long[][] distanceMatrix = {
      {0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662},
      {548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674, 1016, 868, 1210},
      {776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164, 1130, 788, 1552, 754},
      {696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822, 1164, 560, 1358},
      {582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708, 1050, 674, 1244},
      {274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628, 514, 1050, 708},
      {502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856, 514, 1278, 480},
      {194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320, 662, 742, 856},
      {308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662, 320, 1084, 514},
      {194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388, 274, 810, 468},
      {536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764, 730, 388, 1152, 354},
      {502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114, 308, 650, 274, 844},
      {388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194, 536, 388, 730},
      {354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0, 342, 422, 536},
      {468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536, 342, 0, 764, 194},
      {776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274, 388, 422, 764, 0, 798},
      {662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730, 536, 194, 798, 0},
  };
  //最大车辆限制
  public final int vehicleNumber = 4;
    //起点
  public final int depot = 0;
}

:::tips

我们来详细看一下这些参数:

  • 距离矩阵distanceMatrix:首先我们可以看到,他是一个二维数组,共有17行17列,对应的是我们地图上的17个点。

distanceMatrix[0] = {0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354, 468, 776, 662}为例:

可以看到,该数组记录的是0这个点到地图上所有点之间的距离,地图上共17个点,所以该数组的容量就是17,同理我们可以看到其他的一维数组也是这么记录的。

一言蔽之:二维数组的行代表的是点,列代表的是点与点之间的距离。

  • 车辆限制vehicleNumber:在我们的建模中,车辆即路线。所以这里是最大路线的限制,要注意这里是给出最大的路线数量限制,而不是指定路线数量限制。假设算法认为两条路线就能规划出最优的结果,那么剩余的路线就会给出总里程为0的值,我们在最终提取结果时把这样的内容删去即可。
  • 起点depot:车辆开始的点以及返回的点
    :::

距离回调函数

使用距离回调函数,让求解器解析路径距离

final int transitCallbackIndex =
    routing.registerTransitCallback((long fromIndex, long toIndex) -> {
      int fromNode = manager.indexToNode(fromIndex);
      int toNode = manager.indexToNode(toIndex);
      return data.distanceMatrix[fromNode][toNode];
    });
routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);

:::tips

回调函数的作用是让求解器解析我们的数据

参数列表:fromIndex,toIndex

返回值:distanceMatrix[fromNode][toNode]

根据我们上面对二维数组的介绍,可以知道这里返回的就是两点之间的距离

:::

距离维度

维度是求解器用来计算各种数量的对象。我们可以将维度理解为一种限制或约束

我们先来看一下距离维度是怎么做的

routing.addDimension(transitCallbackIndex, 0, 3000,true,"Distance");
RoutingDimension distanceDimension = routing.getMutableDimension("Distance");
distanceDimension.setGlobalSpanCostCoefficient(100);

我们看一下addDimension这个方法的源码:

public boolean addDimension(int evaluator_index, long slack_max, long capacity, boolean fix_start_cumul_to_zero, String name) {
    return mainJNI.RoutingModel_addDimension(swigCPtr, this, evaluator_index, slack_max, capacity, fix_start_cumul_to_zero, name);
  }

我们重点关注一下long capacity这个参数,其作用是限制车辆的最大行驶距离。如果我们不需要对其进行限制,那么直接传入Long.MAX_VALUE即可

完成计划代码

注意数据模型的代码上面已经提供过了,因此这里不再赘述。

public static void main(String[] args) throws Exception {
    //加载本地方法库
    Loader.loadNativeLibraries();
    // 初始化数据模型
    final DataModel data = new DataModel();
    //创建求解器manager对象,初始化求解器数据
    RoutingIndexManager manager =
        new RoutingIndexManager(data.distanceMatrix.length, data.vehicleNumber, data.depot);
    // 初始化求解器
    RoutingModel routing = new RoutingModel(manager);
    // 注册回调函数
    final int transitCallbackIndex =
        routing.registerTransitCallback((long fromIndex, long toIndex) -> {
            int fromNode = manager.indexToNode(fromIndex);
            int toNode = manager.indexToNode(toIndex);
            return data.distanceMatrix[fromNode][toNode];
        });
    // 定义回调函数至每条路线
    routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);
    // 增加距离维度约束
    routing.addDimension(transitCallbackIndex, 0, 3000,
                         true, 
                         "Distance");
    RoutingDimension distanceDimension = routing.getMutableDimension("Distance");
    distanceDimension.setGlobalSpanCostCoefficient(100);
    //设置搜索方法
    RoutingSearchParameters searchParameters =
        main.defaultRoutingSearchParameters()
        .toBuilder()
        .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PATH_CHEAPEST_ARC)
        .build();
    // 执行算法
    Assignment solution = routing.solveWithParameters(searchParameters);
    // 打印路线
    printSolution(data, routing, manager, solution);
}
static void printSolution(
      DataModel data, RoutingModel routing, RoutingIndexManager manager, Assignment solution) {
    long maxRouteDistance = 0;
    for (int i = 0; i < data.vehicleNumber; ++i) {
      long index = routing.start(i);
      logger.info("Route for Vehicle " + i + ":");
      long routeDistance = 0;
      String route = "";
      while (!routing.isEnd(index)) {
        route += manager.indexToNode(index) + " -> ";
        long previousIndex = index;
        index = solution.value(routing.nextVar(index));
        routeDistance += routing.getArcCostForVehicle(previousIndex, index, i);
      }
      logger.info(route + manager.indexToNode(index));
      logger.info("Distance of the route: " + routeDistance + "m");
      maxRouteDistance = Math.max(routeDistance, maxRouteDistance);
    }
    logger.info("Maximum of the route distances: " + maxRouteDistance + "m");
  }

执行后得到以下结果

:::tips

Route for Vehicle 0:

0 -> 8 -> 6 -> 2 -> 5 -> 0

Distance of the route: 1552m

Route for Vehicle 1:

0 -> 7 -> 1 -> 4 -> 3 -> 0

Distance of the route: 1552m

Route for Vehicle 2:

0 -> 9 -> 10 -> 16 -> 14 -> 0

Distance of the route: 1552m

Route for Vehicle 3:

0 -> 12 -> 11 -> 15 -> 13 -> 0

Distance of the route: 1552m

Maximum of the route distances: 1552m

:::

图示:

以上,我们解决了一个基本的车辆路线问题。但是我们可以看到在上述示例中,规划出来的最终路径是无序的,即无法保证取送的顺序。因此我们需要引入其他的约束保证顺序。

车辆取送问题

在示例1的基础之上,我们要对其进行改进。首先我们需要对配送的顺序进行限制,保证路线的方向性,并且需要先取到货才能送货。

在取送的问题中,我们还需要考虑多仓库的问题,即起点也会有多个,每条路线的起点可以是不同的,而不是从一个相同的仓库出发,而且我们也不需要车辆返回起点。这里为了简化,我们暂时不考虑起点终点重合的问题。

取货与配送矩阵

在以上需求中,我们首先需要考虑的就是路线的方向。这里or-tools也给我们提供了可以直接设置的参数。

public final int[][] pickupsDeliveries = {{1, 2},
                {3, 4},
                {5, 6},
                {7, 8},
                {9, 10},
                {11, 12},
                {13, 14},
                {15, 16}};

可以看到这里也是一个二维数组对象。但是需要注意的是这个二维数组中的每个一维数组有且只有两个值。第一个值代表起点,第二个值代表的是终点,数据值即是他们在distanceMatrix中对应的下标,每个下标都是唯一的,不允许重复

接下来我们需要往求解器中添加这些约束因子

Solver solver = routing.solver();
//添加路径取送约束
for (int[] request : data.pickupsDeliveries) {
    long pickupIndex = manager.nodeToIndex(request[0]);
    long deliveryIndex = manager.nodeToIndex(request[1]);
    routing.addPickupAndDelivery(pickupIndex, deliveryIndex);
    solver.addConstraint(
      solver.makeEquality(routing.vehicleVar(pickupIndex), routing.vehicleVar(deliveryIndex)));
      solver.addConstraint(solver.makeLessOrEqual(
        distanceDimension.cumulVar(pickupIndex), distanceDimension.cumulVar(deliveryIndex)));
        }
在以上这段代码中,我们对配送约束矩阵进行遍历。`routing.addPickupAndDelivery(pickupIndex, deliveryIndex);`这段代码会向求解器中创建自提和配送请求。

接着我们使用下面的代码添加了另外的约束:每张单据必须由同一辆车取货和配送

solver.addConstraint(
    solver.makeEquality(routing.vehicleVar(pickupIndex), routing.vehicleVar(deliveryIndex)));
最后我们还需要限制每张单据在配送前必须要先完成取货。所以需要限制车辆在到达取货点行驶的累计距离必须要不超过其到达配送点的累计行驶距离
solver.addConstraint(solver.makeLessOrEqual(
        distanceDimension.cumulVar(pickupIndex), distanceDimension.cumulVar(deliveryIndex)));

以上,我们便完成了对于车辆配送路线的约束。

任意的起点和终点

:::tips

只需修改矩阵的第一行和第一列均全零,即可修改距离矩阵到任何其他地点的距离为 0。这会将仓库变为虚拟位置,对最佳路线没有任何影响

:::

在实际的应用中,我们只需要新增一个默认的点来作为我们的虚拟仓库,使得他到所有点的距离都为0。将他放在我们的第一行,下标为0,我们在使用的时候在创建求解器对象的时候将depot的值传入0就可以实现任意的起点和终点。

这里还需要注意,在最后的返回的结果中,还是会打印出下标为0的这个点,所以我们在最终的打印方法里需要做处理,过滤掉0

车辆取送问题代码实现

我们来看一下完整的代码

static class DataModel {
    public final long[][] distanceMatrix = {
            {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
            {0, 0, 19801, 11006, 19801, 18919, 22227, 20719, 25550, 5698, 40312, 110962, 12084, 14385, 4566, 18368, 21985},
            {0, 19801, 0, 12014, 0, 2441, 5925, 1473, 7095, 14143, 45184, 91440, 10836, 5613, 15977, 6469, 8001},
            {0, 11006, 12014, 0, 12014, 10149, 12175, 12297, 19006, 7401, 35848, 102899, 1311, 8708, 6449, 14329, 17783},
            {0, 19801, 0, 12014, 0, 2441, 5925, 1473, 7095, 14143, 45184, 91440, 10836, 5613, 15977, 6469, 8001},
            {0, 18919, 2441, 10149, 2441, 0, 4389, 2164, 9379, 13414, 42745, 92867, 8899, 5653, 14806, 8336, 10333},
            {0, 22227, 5925, 12175, 5925, 4389, 0, 4572, 10998, 17013, 41082, 91020, 10867, 9968, 17843, 12380, 13688},
            {0, 20719, 1473, 12297, 1473, 2164, 4572, 0, 7363, 15118, 44552, 90798, 11056, 6805, 16737, 7903, 9152},
            {0, 25550, 7095, 19006, 7095, 9379, 10998, 7363, 0, 19877, 51847, 85429, 17874, 11321, 22242, 7857, 5662},
            {0, 5698, 14143, 7401, 14143, 13414, 17013, 15118, 19877, 0, 41367, 105271, 8044, 8687, 3282, 12886, 16561},
            {0, 40312, 45184, 35848, 45184, 42745, 41082, 44552, 51847, 41367, 0, 124228, 36330, 44170, 38348, 49669, 52691},
            {0, 110962, 91440, 102899, 91440, 92867, 91020, 90798, 85429, 105271, 124228, 0, 101602, 96612, 107404, 92948, 89626},
            {0, 12084, 10836, 1311, 10836, 8899, 10867, 11056, 17874, 8044, 36330, 101602, 0, 7936, 7518, 13548, 16909},
            {0, 14385, 5613, 8708, 5613, 5653, 9968, 6805, 11321, 8687, 44170, 96612, 7936, 0, 10922, 5624, 9111},
            {0, 4566, 15977, 6449, 15977, 14806, 17843, 16737, 22242, 3282, 38348, 107404, 7518, 10922, 0, 15709, 19408},
            {0, 18368, 6469, 14329, 6469, 8336, 12380, 7903, 7857, 12886, 49669, 92948, 13548, 5624, 15709, 0, 3699},
            {0, 21985, 8001, 17783, 8001, 10333, 13688, 9152, 5662, 16561, 52691, 89626, 16909, 9111, 19408, 3699, 0}};
    public final int[][] pickupsDeliveries = {{1, 2},
            {3, 4},
            {5, 6},
            {7, 8},
            {9, 10},
            {11, 12},
            {13, 14},
            {15, 16}};
    public final Integer vehicleNumber = 7;
}
private static Plan compute(DataModel data) {
    long[][] distanceMatrix = data.distanceMatrix;
    RoutingIndexManager manager =
            new RoutingIndexManager(distanceMatrix.length, data.vehicleNumber, 0);
    RoutingModel routing = new RoutingModel(manager);
    final int transitCallbackIndex =
            routing.registerTransitCallback((long fromIndex, long toIndex) -> {
                int fromNode = manager.indexToNode(fromIndex);
                int toNode = manager.indexToNode(toIndex);
                return distanceMatrix[fromNode][toNode];
            });
    routing.setArcCostEvaluatorOfAllVehicles(transitCallbackIndex);
    routing.addDimension(transitCallbackIndex, 0, Long.MAX_VALUE, true, "Distance");
    RoutingDimension distanceDimension = routing.getMutableDimension("Distance");
    Solver solver = routing.solver();
    //添加路径取送约束
    for (int[] request : data.pickupsDeliveries) {
        long pickupIndex = manager.nodeToIndex(request[0]);
        long deliveryIndex = manager.nodeToIndex(request[1]);
        routing.addPickupAndDelivery(pickupIndex, deliveryIndex);
        solver.addConstraint(
                solver.makeEquality(routing.vehicleVar(pickupIndex), routing.vehicleVar(deliveryIndex)));
        solver.addConstraint(solver.makeLessOrEqual(
                distanceDimension.cumulVar(pickupIndex), distanceDimension.cumulVar(deliveryIndex)));
    }
    RoutingSearchParameters searchParameters =
            main.defaultRoutingSearchParameters()
                    .toBuilder()
                    .setFirstSolutionStrategy(FirstSolutionStrategy.Value.PARALLEL_CHEAPEST_INSERTION)
                    .build();
    Assignment solution = routing.solveWithParameters(searchParameters);
    return printSolution(data.vehicleNumber, routing, manager, solution);
}
static Plan printSolution(Integer vehicleNumber, RoutingModel routing, RoutingIndexManager manager, Assignment solution) {
    long totalDistance = 0;
    List<String> list = new ArrayList<>();
    for (int i = 0; i < vehicleNumber; ++i) {
        long index = routing.start(i);
        System.out.println("车辆 " + i + ":");
        long routeDistance = 0;
        String route = "";
        while (!routing.isEnd(index)) {
            int resIndex = manager.indexToNode(index);
            if (resIndex != 0) {
                route += resIndex + ",";
            }
            long previousIndex = index;
            index = solution.value(routing.nextVar(index));
            routeDistance += routing.getArcCostForVehicle(previousIndex, index, i);
        }
        int lastIndex = manager.indexToNode(index);
        String msg = route;
        if (lastIndex != 0) {
            msg += lastIndex;
        }
        if (msg.endsWith(",")) {
            msg = msg.substring(0, msg.length() - 1);
        }
        list.add(msg);
        System.out.println(msg);
        System.out.println("路线里程: " + routeDistance + "m");
        totalDistance += routeDistance;
    }
    double res = (double) totalDistance / 1000;
    System.out.println("最后的路线列表:" + list);
    System.out.println("该方案总里程: " + res + "km");
    Plan plan = new Plan();
    plan.setCarNum(vehicleNumber);
    plan.setTotalDistance(totalDistance);
    return plan;
}
public static void main(String[] args) throws Exception {
    Loader.loadNativeLibraries();
    final DataModel data = new DataModel();
    Plan compute = compute(data);
}

最后的执行结果

:::tips

车辆 0:

15,16

路线里程: 3699m

车辆 1:

5,6

路线里程: 4389m

车辆 2:

7,8

路线里程: 7363m

车辆 3:

13,9,14,10

路线里程: 50317m

车辆 4:

1,3,2,4

路线里程: 23020m

车辆 5:

11,12

路线里程: 101602m

车辆 6:

路线里程: 0m

最后的路线列表:[15,16, 5,6, 7,8, 13,9,14,10, 1,3,2,4, 11,12, ]

该方案总里程: 190.39km

:::

以上的执行结果也印证了之前我们讲过的,车辆数量参数vehicleNumber实际上是最大车辆数量限制,可以看到车辆6是没有安排线路的。并且最终的路线结果也符合我们的约束矩阵的要求,必须要先取到货才能配送。

车辆容量限制

在取送问题的示例中,我们保证了发货单配送的取送顺序,但是还没有加上车辆的容载限制。我们在之前提到过,维度这个概念可以理解为就是给求解器加上一些约束,因此这里我们加上一个容量的维度即可。

数据模型

//每一个点的商品的数量
public final long[] demands = {0, 1, 1, 2, 4, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8};
//车辆最大容载
public final long[] vehicleCapacities = {25, 25, 25, 25, 25, 25, 25};

demands:这个数组记录的是每个点上面对应的商品数量,即车辆抵达这个点,就会累计数量。事实上,按照我们的取送逻辑,这里应该设置成起点为正(取货),终点为负(卸货),但是为了演示的方便,我们这里就没有做设置。

vehicleCapacities:每辆车的最大容载数量。只要每行驶到一个点,就会累计商品数量,不允许超过车辆的容载。

要注意,在使用的时候每个点都需要指定商品数量,每辆车必须指定容量。即demands的长度需要与我们本次需要计算路径的点的数量一致(包含我们的虚拟仓库0),vehicleCapacities的长度也需要等于我们指定的最大车辆数量。

容量维度

// 添加容量限制
final int demandCallbackIndex = routing.registerUnaryTransitCallback((long fromIndex) -> {
    int fromNode = manager.indexToNode(fromIndex);
    return data.demands[fromNode];
});
routing.addDimensionWithVehicleCapacity(demandCallbackIndex, 0, data.vehicleCapacities, true, "Capacity");
Solver solver = routing.solver();

在取送问题的示例中,加上以上的容量维度代码限制即可

总结

在本文中,我们使用or-tools解决了基本的带有容量限制的车辆取送问题。但是在现实的业务场景中,显然要比这些问题要复杂的多,在下一篇中我们将详细的讨论一个真实的业务场景问题。

目录
相关文章
|
6月前
|
监控 供应链 物联网
ERP系统中的在制品管理与工艺路线规划
ERP系统中的在制品管理与工艺路线规划
201 2
|
6月前
|
数据挖掘 BI 调度
点晴PMS港口管理系统对港口全面管理和跟踪
点晴PMS港口管理系统是一套专业用于集装箱港口的信息管理软件,致力于提升港口管理的效率与智能化水平。以下是关于点晴PMS港口系统的一些主要特点和功能:
91 3
|
6月前
|
存储 监控 安全
智慧工地平台,工程全生命周期管理,实现对人、机、料、法、环的全方位实时监控
智慧工地运行的系统包括工程质量管理系统、劳务人员实名制系统、项目人员管理系统、智能监控系统、绿色施工系统、危大工程管理系统、物料管理系统、智能水电监测系统、安全隐患排查系统、互联网远程会议系统等。智能工地能够有效提高建筑施工质量,这些系统的运行能够使我们的建筑行业更加高效和安全。
184 0
|
1月前
|
传感器 机器学习/深度学习 人工智能
智能物流:自动化仓库与配送系统
【10月更文挑战第22天】在21世纪的全球化经济中,物流行业作为连接生产与消费的桥梁,其效率与准确性至关重要。本文深入探讨智能物流的核心技术,如自动化仓储、无人驾驶配送和物联网,以及应用案例和未来发展趋势,揭示自动化仓库与配送系统如何引领物流行业的智能化转型。
|
2月前
|
监控 数据可视化 数据挖掘
点晴PMS码头港口集装箱管理系统精准预测高效调度
随着码头集装箱运输的不断发展,码头集装箱管理系统软件的市场前景广阔。对于码头企业来说,引入这样一套适合码头管理的系统,将能够提高运营效率、降低成本、增强安全性,更好地适应和应对市场需求。
53 8
|
3月前
|
达摩院 供应链 JavaScript
网络流问题--仓储物流调度【数学规划的应用(含代码)】阿里达摩院MindOpt
本文通过使用MindOpt工具优化仓储物流调度问题,旨在提高物流效率并降低成本。首先,通过考虑供需匹配、运输时间与距离、车辆容量、仓库储存能力等因素构建案例场景。接着,利用数学规划方法,包括线性规划和网络流问题,来建立模型。在网络流问题中,通过定义节点(资源)和边(资源间的关系),确保流量守恒和容量限制条件下找到最优解。文中还详细介绍了MindOpt Studio云建模平台和MindOpt APL建模语言的应用,并通过实例展示了如何声明集合、参数、变量、目标函数及约束条件,并最终解析了求解结果。通过这些步骤,实现了在满足各仓库需求的同时最小化运输成本的目标。
|
4月前
|
监控 算法 数据挖掘
ERP系统中的生产线排程与调度优化解析
【7月更文挑战第25天】 ERP系统中的生产线排程与调度优化解析
130 6
|
4月前
|
监控 算法 数据可视化
ERP系统中的生产调度与计划排程解析
【7月更文挑战第25天】 ERP系统中的生产调度与计划排程解析
273 1
|
6月前
|
存储 JSON 定位技术
基于or-tools解决物流调度问题(二)
基于or-tools解决物流调度问题(二)
96 6
|
6月前
|
传感器 监控 BI
MES系统怎么实现车间管理中的生产计划和排产计划
生产计划和排产计划都是制造企业中非常重要的概念,它们的目的是为了确保企业能够按时交付高质量的产品,同时还要保持生产效率和成本效益。
185 0