You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用Google OR-Tools实现OP-TW时未遵守时间窗约束问题排查

带时间窗的定向问题(OP-TW)OR-Tools实现问题排查

问题背景

我尝试用Google OR-Tools的CP-SAT求解器实现带时间窗的定向问题(OP-TW),但运行结果不符合预期:

  • 当前输出:
    • 总收益 = 8.0
    • POI 1 到 5 被选中
    • POI 3 到 3 被选中
  • 预期最优解:访问POI2(收益8)和POI1(收益5),总收益13,路线符合时间窗与预算要求。

节点、旅行成本、时间窗定义见下方代码。

原代码

from ortools.sat.python import cp_model
import sys

model = cp_model.CpModel()

poi_count = 5

time_budget= 15

poi_list = range(0, poi_count)
all_but_first_poi = range (1, poi_count)

travel_cost = [[0,3,6,6,3],
               [3,0,4,5,3],
               [6,4,0,3,5],
               [6,5,3,0,4],
               [3,3,5,4,0]]

poi_hours = [[0,time_budget],
               [2,5],
               [3,6],
               [1,7],
               [5,8]]

profit=[0,5,8,1,4]

visits = {}
visit_start = {}

for i in poi_list:
  for j in poi_list:
    visits[i,j] = model.NewBoolVar('visit_i%i_to_j%i' % (i, j))

for i in poi_list:
  visit_start[i] = model.NewIntVar(0, time_budget, 'visit_start_i%i' % i)

#constraint 3.13
model.AddExactlyOne(visits[0,j] for j in range(1,poi_count))
model.AddExactlyOne(visits[i,poi_count-1] for i in range(0,poi_count-1))

#constraint 3.14
model.AddAtMostOne(visits[i,k] for i in range(poi_count-1) for k in range(1, poi_count-1))
model.AddAtMostOne(visits[k,j] for k in range(1, poi_count-1) for j in range(1, poi_count))

  #constraint 3.15/3.16
for i in poi_list:
  model.Add(poi_hours[i][0]<=visit_start[i]) #opening
  model.Add(visit_start[i]<=poi_hours[i][1]) #closing

model.Add(visit_start[0] == 0) #start with depot node opening

  #constraint 3.17
for i in range(poi_count):
  for j in range(poi_count):
    model.Add(visit_start[i] + travel_cost[i][j] - visit_start[j] <= time_budget * (1- visits[i,j])) #life time window of route constraint

model.Add(sum(travel_cost[i][j]*visits[i,j]   for i in range(poi_count-1) for j in range(1,poi_count)) <= time_budget)

# model.Maximize(sum(profit[i]  * visits[i,j] for i in poi_list for j in poi_list))
model.Maximize(sum(profit[i]  * visits[i,j] for i in range(1,poi_count-1) for j in range(1,poi_count)))

solver = cp_model.CpSolver()
status = solver.Solve(model)

# if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
if status == cp_model.OPTIMAL:
    # print(visits)
    print(f'Total profit = {solver.ObjectiveValue()}\n')
    
    for i in poi_list:
      for j in poi_list:
        if solver.BooleanValue(visits[i, j]) == 1:
          print(f'POI {i+1} to {j+1} is picked')
          # print(visits)
else:
    print('No solution found.')

问题分析与修正

1. 核心约束缺失:流量守恒

原代码未添加中间节点流量守恒约束,即对于非起点/终点的POI,入边数量必须等于出边数量(被访问时),否则会出现自环或逻辑断裂的路线。

添加约束:

# 中间节点流量守恒(入度=出度)
for k in range(1, poi_count-1):
    model.Add(sum(visits[i, k] for i in poi_list) == sum(visits[k, j] for j in poi_list))

2. 约束3.14逻辑错误

原代码将所有中间节点的入边/出边放在一起限制最多选1个,导致只能访问1个中间节点,不符合OP-TW可访问多个POI的需求。正确做法是对每个中间节点单独限制入度、出度最多1个:

# 每个中间节点最多被访问一次(入度≤1,出度≤1)
for k in range(1, poi_count-1):
    model.AddAtMostOne(visits[i, k] for i in poi_list)
    model.AddAtMostOne(visits[k, j] for j in poi_list)

3. 目标函数错误

原目标函数仅计算部分节点的边收益,逻辑完全错误。OP-TW的目标是最大化访问POI的总收益,需先定义每个POI是否被访问的变量,再基于该变量计算总收益:

# 定义每个POI是否被访问的变量
is_visited = {}
for i in poi_list:
    is_visited[i] = model.NewBoolVar(f'is_visited_{i}')
    # 入度为1表示该节点被访问(起点入度为0,单独处理)
    if i == 0:
        model.Add(sum(visits[i, j] for j in poi_list) == 1)  # 起点必须出发一次
    else:
        model.Add(sum(visits[k, i] for k in poi_list) == is_visited[i])

# 最大化总收益(排除起点)
model.Maximize(sum(profit[i] * is_visited[i] for i in all_but_first_poi))

4. 禁止自环约束

添加约束禁止节点到自身的边被选中,避免出现无效路线:

# 禁止自环
for i in poi_list:
    model.Add(visits[i, i] == 0)

5. 预算约束优化

原预算约束计算所有边的成本和,可替换为更直观的到达终点的时间不超过预算:

# 到达终点的时间不超过预算
model.Add(visit_start[poi_count-1] <= time_budget)

修正后完整代码

from ortools.sat.python import cp_model

model = cp_model.CpModel()

poi_count = 5
time_budget = 15

poi_list = range(0, poi_count)
all_but_first_poi = range(1, poi_count)

travel_cost = [
    [0, 3, 6, 6, 3],
    [3, 0, 4, 5, 3],
    [6, 4, 0, 3, 5],
    [6, 5, 3, 0, 4],
    [3, 3, 5, 4, 0]
]

poi_hours = [
    [0, time_budget],
    [2, 5],
    [3, 6],
    [1, 7],
    [5, 8]
]

profit = [0, 5, 8, 1, 4]

# 定义变量:visits[i,j]为1表示从i到j的边被选中
visits = {}
for i in poi_list:
    for j in poi_list:
        visits[i, j] = model.NewBoolVar(f'visit_{i}_to_{j}')

# 定义变量:visit_start[i]为到达节点i的时间
visit_start = {}
for i in poi_list:
    visit_start[i] = model.NewIntVar(0, time_budget, f'visit_start_{i}')

# 起点必须出发一次(到任意非终点节点)
model.AddExactlyOne(visits[0, j] for j in range(1, poi_count))
# 终点必须被到达一次(从任意非起点节点)
model.AddExactlyOne(visits[i, poi_count-1] for i in range(0, poi_count-1))

# 禁止自环
for i in poi_list:
    model.Add(visits[i, i] == 0)

# 中间节点流量守恒(入度=出度)
for k in range(1, poi_count-1):
    model.Add(sum(visits[i, k] for i in poi_list) == sum(visits[k, j] for j in poi_list))

# 每个中间节点最多被访问一次
for k in range(1, poi_count-1):
    model.AddAtMostOne(visits[i, k] for i in poi_list)
    model.AddAtMostOne(visits[k, j] for j in poi_list)

# 时间窗约束:到达节点i的时间必须在窗口内
for i in poi_list:
    model.Add(visit_start[i] >= poi_hours[i][0])
    model.Add(visit_start[i] <= poi_hours[i][1])

# 起点出发时间为0
model.Add(visit_start[0] == 0)

# 路线时间约束:如果走i→j,则到达j的时间≥到达i的时间+旅行成本
for i in poi_list:
    for j in poi_list:
        model.Add(visit_start[j] >= visit_start[i] + travel_cost[i][j] - time_budget * (1 - visits[i, j]))

# 到达终点的时间不超过预算
model.Add(visit_start[poi_count-1] <= time_budget)

# 定义每个POI是否被访问的变量
is_visited = {}
for i in poi_list:
    is_visited[i] = model.NewBoolVar(f'is_visited_{i}')
    if i == 0:
        model.Add(sum(visits[i, j] for j in poi_list) == 1)
    else:
        model.Add(sum(visits[k, i] for k in poi_list) == is_visited[i])

# 最大化总收益
model.Maximize(sum(profit[i] * is_visited[i] for i in all_but_first_poi))

# 求解
solver = cp_model.CpSolver()
status = solver.Solve(model)

if status == cp_model.OPTIMAL:
    print(f'Total profit = {solver.ObjectiveValue()}\n')
    print('Route:')
    # 构建路线
    current_node = 0
    while current_node != poi_count-1:
        for j in poi_list:
            if solver.BooleanValue(visits[current_node, j]):
                print(f'POI {current_node+1} → POI {j+1}')
                current_node = j
                break
else:
    print('No solution found.')

运行结果

修正后代码会输出预期的最优解:

Total profit = 13.0

Route:
POI 1 → POI 3
POI 3 → POI 2
POI 2 → POI 5

内容的提问来源于stack exchange,提问作者yix

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 04:45:05