示例 17:带时间窗的车辆路径问题
问题与数据
本示例建模带服务时间窗的容量约束车辆路径问题。当前源码包含一个起点、100 个需求节点、一个终点和 25 辆相同车辆,共 102 个节点。起点和终点都位于
源码使用欧氏几何:Node.distance 返回两个 Point2 位置之间的距离,Node.cost 和 Node.time 都返回该距离。因此旅行成本和旅行时间共用同一距离单位,没有独立的成本矩阵或速度矩阵。
集合与参数
令
对需求节点
决策变量
对
表示车辆 k 是否使用弧
对每个
中间值
对每辆车
对每个需求节点
对每辆车:
源码注册两个目标:
目标
Demo17 分别调用两次 minimize,先注册 UsedCost,再注册 TravelCost。源码没有构造加权和,也没有定义二者之间的标量系数;具体的多目标处理交由当前模型/求解器策略。
约束与定义域
车辆使用和路线流量:
源码用大于等于和小于等于两条约束实现
对每个节点和车辆:
对每辆车:
不允许的弧在表达式注册前被固定为零。源码仍然对所有节点对创建时间约束,固定为零的项使不允许弧上的蕴含约束失效。
实现差异与注意事项
源码依次通过 initVariable、initSymbol、initObject、initConstraint、solve 和 analyzeSolution 构建模型。它使用当前 core 符号,并用配置为 300 秒时间限制的 ScipLinearSolver 求解。两个目标注册、1236 的大 M、允许弧过滤、非负实数时间变量都是实现事实。本示例不是允许选客的通用 VRPTW 模型:每个需求节点都必须恰好服务一次。
预期结果
成功求解后会返回覆盖全部 100 个需求节点的路线和服务时间,并满足每个源码时间窗及每辆车容量。当前构建测试只验证模型构建,不断言路线列表、目标值或唯一最优解。实例可能求解耗时较长,并受五分钟求解器时间限制影响。
当前 Kotlin 最小示例
import kotlin.time.Duration.Companion.seconds
import fuookami.ospf.kotlin.utils.concept.*
import fuookami.ospf.kotlin.multiarray.*
import fuookami.ospf.kotlin.math.*
import fuookami.ospf.kotlin.math.algebra.number.*
import fuookami.ospf.kotlin.math.algebra.value_range.*
import fuookami.ospf.kotlin.math.geometry.*
import fuookami.ospf.kotlin.math.geometry.point2
import fuookami.ospf.kotlin.math.symbol.operation.*
import fuookami.ospf.kotlin.math.symbol.polynomial.*
import fuookami.ospf.kotlin.core.model.intermediate.*
import fuookami.ospf.kotlin.core.model.mechanism.*
import fuookami.ospf.kotlin.core.solver.config.*
import fuookami.ospf.kotlin.core.solver.scip.*
import fuookami.ospf.kotlin.core.symbol.*
import fuookami.ospf.kotlin.core.variable.*
import fuookami.ospf.kotlin.example.solveLinearMetaModel
val model = LinearMetaModel<Flt64>("demo17", converter = flt64Converter)
val x = BinVariable3("x", Shape3(nodes.size, nodes.size, vehicles.size))
for (from in nodes) for (to in nodes) for (vehicle in vehicles) {
val xi = x[from, to, vehicle]
if (from !is EndNode && to !is OriginNode && from != to) model.add(xi)
else xi.range.eq(false)
}
val s = URealVariable2("s", Shape2(nodes.size, vehicles.size))
model.add(s)
val origin = LinearIntermediateSymbols1<Flt64>("origin", Shape1(vehicles.size)) { i, _ ->
LinearExpressionSymbol(
sum(nodes.filterIsInstance<OriginNode>().flatMap { node -> x[node, _a, vehicles[i]] }),
name = "origin_$i"
)
}
val destination = LinearIntermediateSymbols1<Flt64>("destination", Shape1(vehicles.size)) { i, _ ->
LinearExpressionSymbol(
sum(nodes.filterIsInstance<EndNode>().flatMap { node -> x[_a, node, vehicles[i]] }),
name = "destination_$i"
)
}
val service = LinearIntermediateSymbols1<Flt64>("service", Shape1(nodes.size)) { i, _ ->
LinearExpressionSymbol(
sum(nodes.filterIsNotInstance<OriginNode, Node>().flatMap { node -> x[nodes[i], node, _a] }),
name = "service_$i"
)
}
val capacity = LinearIntermediateSymbols1<Flt64>("capacity", Shape1(vehicles.size)) { i, _ ->
LinearExpressionSymbol(
sum(nodes.flatMap { from ->
nodes.mapNotNull { to -> (to as? DemandNode)?.demand?.let { it * x[from, to, vehicles[i]] } }
}),
name = "capacity_$i"
)
}
model.add(origin)
model.add(destination)
model.add(service)
model.add(capacity)
model.minimize(sum(vehicles.map { it.fixedUsedCost * origin[it] }), "used cost")
model.minimize(
sum(nodes.flatMap { from -> nodes.map { to -> from.cost(to) * sum(x[from, to, _a]) } }),
"trans cost"
)
for (vehicle in vehicles) model.addConstraint(origin[vehicle] leq 1)
for (node in nodes.filterIsInstance<DemandNode>()) {
model.addConstraint(service[node] eq 1)
for (vehicle in vehicles) {
model.addConstraint(inFlow[node, vehicle] geq outFlow[node, vehicle])
model.addConstraint(inFlow[node, vehicle] leq outFlow[node, vehicle])
}
}
for (vehicle in vehicles) {
model.addConstraint(destination[vehicle] leq 1)
model.addConstraint(capacity[vehicle] leq vehicle.capacity)
}
val m = nodes.filterIsInstance<EndNode>().maxOf { it.timeWindow.upperBound.value.unwrap() }
for (from in nodes) for (to in nodes) for (vehicle in vehicles) {
model.addConstraint(
s[from, vehicle] +
((from as? DemandNode)?.serviceTime ?: UInt64.zero).toFlt64() +
from.time(to) -
m.toFlt64() * (1 - x[from, to, vehicle]) leq s[to, vehicle]
)
}
for (node in nodes) for (vehicle in vehicles) {
model.addConstraint(s[node, vehicle] geq node.timeWindow.lowerBound.value.unwrap())
model.addConstraint(s[node, vehicle] leq node.timeWindow.upperBound.value.unwrap())
}
suspend fun solve() = solveLinearMetaModel(
ScipLinearSolver(config = SolverConfig(time = 300.seconds)),
model
)源码与验证
Kotlin/Rust 对照
这是两个独立模型:Rust 是 4 个客户的紧凑 VRPTW,Kotlin 当前 core 实现是 100 个客户、102 个节点;两者不共享数据、Big-M 或目标组织,API 也独立。
// See the linked Kotlin implementation for the complete model.
``
```rust [Rust]
// See the linked Rust implementation for the equivalent model.
``