如何理解TSP问题中的两个Drools软约束规则?
首先明确一个核心前提:TSP要求的闭环总距离,本质是所有路径分段长度的累加,只要所有约束规则的计算贡献总和等于这个总目标,具体怎么拆分规则和直接计算最终值没有本质区别——这套拆分规则的核心目的,是为了适配求解器的增量式得分计算机制,同时清晰拆解路径的距离构成。
两个规则的计算总和刚好等价于TSP闭环总距离
我们先把TSP的标准闭环路径拆成独立的分段:
- 起点(Domicile)到第一个访问点的距离
- 每两个相邻访问点之间的距离
- 最后一个访问点返回起点的距离
两个规则的覆盖范围刚好完全对应这三类分段,没有重叠也没有遗漏:
- 规则
distanceToPreviousStandstill- 触发条件:所有已经被分配了前序节点(
previousStandstill不为空)的访问点,直接取该访问点预存的「到前序节点的距离」作为扣分依据 - 这里的
distanceFromPreviousStandstill是Visit对象在被分配前序节点时自动计算并存入的字段,不需要在规则逻辑里重新计算两点距离,进一步降低规则执行的开销。 - 覆盖范围:包含了从起点出发,沿着路径走到最后一个访问点的所有分段长度——如果访问点的前序是起点,就对应起点到第一个访问点的距离;如果前序是其他访问点,就对应两个相邻访问点之间的距离。所有这些分段的贡献累加,就是从起点走到路径最后一个节点的总长度。这种拆分把所有“访问点到自身前序(不管前序是起点还是其他访问点)”的计算统一处理,不需要单独为“起点到第一个访问点”写一条独立规则,减少了规则冗余。
- 触发条件:所有已经被分配了前序节点(
- 规则
distanceFromLastVisitToDomicile- 触发条件:已经分配了前序节点、且不存在任何其他访问点将其作为前序节点的访问点,计算该点到起点的距离作为扣分依据
- 这里的
not Visit(previousStandstill == $visit)逻辑,就是精准定位路径的尾节点:在合法的单一路径结构里,除了最后一个访问点,其他所有节点都会有一个后继节点将其作为前序,只有最后一个节点没有任何后继指向它。 - 覆盖范围:对应最后一个访问点返回起点的距离。
两个规则的扣分全部累加后,得到的负向得分刚好等于“TSP闭环总距离的相反数”——因为求解器会优先选择得分更高的方案,负向的距离值就天然实现了“总距离越短,得分越高”的优化目标。
举个直观的数值例子
假设3个访问点A、B、C组成的合法路径为 起点→A→B→C→起点,各段距离分别为:起点到A为2,A到B为3,B到C为4,C到起点为5,总闭环距离为14,对应目标得分应为-14。
- 第一个规则触发3次,分别贡献-2(A段)、-3(B段)、-4(C段),累计贡献-9
- 第二个规则只会触发1次:只有C没有后继节点指向,贡献-5(C返回起点的距离)
- 总得分累计为-9 + (-5) = -14,和目标完全一致。
为什么不直接写一条规则计算全路径总距离
这套拆分设计最核心的价值是大幅提升求解性能:
如果直接写一条全量计算总距离的规则,每次求解器调整路径(比如调换两个节点的顺序、把某个节点插到其他位置),都需要遍历所有节点重新计算全路径的总长度,时间复杂度极高。
而拆分后的规则可以支持增量计算:每次路径调整只会影响少数几个相邻分段的贡献值,不需要重新遍历全量节点。比如上述例子中如果调换B和C的顺序为 起点→A→C→B→D→起点,只需要删除原来A-B、B-C、C-D、D-起点的旧贡献,加上A-C、C-B、B-D、D-起点的新贡献即可,不需要重新计算所有节点的距离,在节点规模较大时性能差异非常明显。
除此之外,这种拆分方式的逻辑和路径结构一一对应,规则可读性强、后续维护成本低,也不会出现漏算、重复计算的问题。
内容的提问来源于stack exchange,提问作者Rukawaa11

