数据¶
我们提供的集群车辆路径问题 (cluVRP) 实例来自论文 Exact Algorithms for the Clustered Vehicle Routing Problem。它们遵循 TSPLib 格式,具体如下:
- 节点数量跟在关键字 DIMENSION 之后(由于有一个仓库,客户数量等于节点数减 1)。
- 卡车载货能力跟在关键字 CAPACITY 之后。
- 卡车数量跟在关键字 VEHICLES 之后。
- 集群数量跟在关键字 GVRP_CAPACITY 之后。
- 关键字 NODE_COORD_SECTION 之后:对每个节点,给出其 ID 和 x、y 坐标。
- 关键字 GVRP_SET_SECTION 之后:对每个集群,给出其 ID 以及属于该集群的节点(以值 -1 结束)。
- 关键字 DEMAND_SECTION 之后:对每个集群,给出其 ID 和总需求(该集群内客户需求的总和)。
建模思路¶
集群车辆路径问题 (cluVRP) 的 OptAgent 模型使用列表决策变量。对于每辆卡车,我们定义一个列表变量来表示它访问的集群序列(truckSequences)。通过在所有这些列表上使用 partition 约束,我们确保每个集群恰好由一辆卡车服务。然后我们使用第二组列表决策变量来建模卡车访问每个集群内客户的顺序(clustersSequences)。为了确保我们访问所有客户,我们将这些列表的大小约束为等于集群中的客户数。
每辆卡车交付的总数量通过 lambda 函数 对所有访问过的集群应用 sum 算子来计算。注意,该求和中项的数量以及列表的大小在搜索过程中是变化的。我们将该数量约束为小于卡车的载货能力。
然后我们计算每个集群内部的行驶距离。使用另一个 lambda 函数,我们沿路径将从一个客户到下一个客户的距离累加起来。我们还记录每个集群中访问的第一个和最后一个节点(分别为 initialNodes 和 endNodes)。
然后我们可以通过累加以下各项来计算每辆卡车的行驶距离:
- 在每个访问过的集群内行驶的距离
- 从一个集群的最后一个客户到下一个集群的第一个客户的距离之和
- 从配送中心到第一个集群的第一个客户的距离
- 从最后一个集群的最后一个客户返回配送中心的距离。
最后,我们最小化总行驶距离。
Python 实现¶
import math
from pathlib import Path
from optagent import OptModel, solve
def read_elem(filename):
return Path(filename).read_text(encoding="utf-8").split()
def read_input_cvrp(filename):
file_it = iter(read_elem(filename))
while True:
token = next(file_it)
if token == "DIMENSION:":
nb_nodes = int(next(file_it))
nb_customers = nb_nodes - 1
elif token == "VEHICLES:":
nb_trucks = int(next(file_it))
elif token == "GVRP_SETS:":
nb_clusters = int(next(file_it))
elif token == "CAPACITY:":
truck_capacity = int(next(file_it))
elif token == "NODE_COORD_SECTION":
break
customers_x = [None] * nb_customers
customers_y = [None] * nb_customers
depot_x = 0
depot_y = 0
for node in range(nb_nodes):
node_id = int(next(file_it))
if node_id != node + 1:
raise ValueError("node identifiers must be consecutive")
x = int(float(next(file_it)))
y = int(float(next(file_it)))
if node_id == 1:
depot_x = x
depot_y = y
else:
customers_x[node_id - 2] = x
customers_y[node_id - 2] = y
distance_matrix = compute_distance_matrix(customers_x, customers_y)
distance_depots = compute_distance_depots(
depot_x, depot_y, customers_x, customers_y
)
if next(file_it) != "GVRP_SET_SECTION":
raise ValueError("missing GVRP_SET_SECTION")
clusters_data = [None] * nb_clusters
for cluster_index in range(nb_clusters):
if int(next(file_it)) != cluster_index + 1:
raise ValueError("cluster identifiers must be consecutive")
cluster = []
value = int(next(file_it))
while value != -1:
cluster.append(value - 2)
value = int(next(file_it))
clusters_data[cluster_index] = cluster
if next(file_it) != "DEMAND_SECTION":
raise ValueError("missing DEMAND_SECTION")
demands = [None] * nb_clusters
for cluster_index in range(nb_clusters):
if int(next(file_it)) != cluster_index + 1:
raise ValueError("demand identifiers must be consecutive")
demands[cluster_index] = int(next(file_it))
return (
nb_customers,
nb_trucks,
nb_clusters,
truck_capacity,
distance_matrix,
distance_depots,
demands,
clusters_data,
)
def compute_distance_matrix(customers_x, customers_y):
nb_customers = len(customers_x)
distance_matrix = [
[None for _ in range(nb_customers)] for _ in range(nb_customers)
]
for i in range(nb_customers):
distance_matrix[i][i] = 0
for j in range(nb_customers):
dist = compute_dist(
customers_x[i], customers_x[j], customers_y[i], customers_y[j]
)
distance_matrix[i][j] = dist
distance_matrix[j][i] = dist
return distance_matrix
def compute_distance_depots(depot_x, depot_y, customers_x, customers_y):
nb_customers = len(customers_x)
distance_depots = [None] * nb_customers
for i in range(nb_customers):
distance_depots[i] = compute_dist(
depot_x, customers_x[i], depot_y, customers_y[i]
)
return distance_depots
def compute_dist(xi, xj, yi, yj):
exact_dist = math.sqrt(math.pow(xi - xj, 2) + math.pow(yi - yj, 2))
return int(math.floor(exact_dist + 0.5))
def main(instance_file, output_file=None, time_limit=20):
(
nb_customers,
nb_trucks,
nb_clusters,
truck_capacity,
matrix_data,
depot_data,
demands_data,
clusters_data,
) = read_input_cvrp(instance_file)
model = OptModel()
matrix, depot_distances = model.array(matrix_data), model.array(depot_data)
demands, clusters = model.array(demands_data), model.array(clusters_data)
# 决定 cluster 内部的访问顺序(每个cluster内部有哪些客户都是确定的,即数量一致)
cluster_sequences = []
for cluster_id, cluster in enumerate(clusters_data):
sequence = model.list(
len(cluster),
)
model.constraint(sequence.count() == len(cluster))
cluster_sequences.append(sequence)
cluster_distances, initial_nodes, end_nodes = [], [], []
for cluster_id, sequence in enumerate(cluster_sequences):
count = sequence.count()
customers = clusters[cluster_id]
distance_lambda = model.lambda_function(
lambda position: matrix[
customers[sequence[position - 1]],
customers[sequence[position]],
]
)
cluster_distances.append(
model.sum(model.range(1, count), distance_lambda)
)
initial_nodes.append(customers[sequence[0]])
end_nodes.append(customers[sequence[count - 1]])
# 存放每个集群内部的行驶距离
cluster_distances_array = model.array(cluster_distances)
initial_nodes_array, end_nodes_array = model.array(initial_nodes), model.array(end_nodes)
# 分配truck所负责的cluster
truck_sequences = [
model.list(nb_clusters)
for truck in range(nb_trucks)
]
model.constraint(model.partition(truck_sequences))
# 各个部分的距离计算
route_distances = []
for sequence in truck_sequences:
count = sequence.count()
demand_lambda = model.lambda_function(lambda cluster: demands[cluster])
route_quantity = model.sum(sequence, demand_lambda)
model.constraint(route_quantity <= truck_capacity)
route_distance_lambda = model.lambda_function(
lambda position: cluster_distances_array[sequence[position]]
+ matrix[
end_nodes_array[sequence[position - 1]],
initial_nodes_array[sequence[position]],
]
)
route_distances.append(
model.sum(model.range(1, count), route_distance_lambda)
+ model.iif(
count > 0,
cluster_distances_array[sequence[0]]
+ depot_distances[initial_nodes_array[sequence[0]]]
+ depot_distances[end_nodes_array[sequence[count - 1]]],
0,
)
)
total_distance = model.sum(route_distances)
model.minimize(total_distance)
solution = solve(model, time_limit_s=float(time_limit))
values = {'total_distance': total_distance.value, **{f'truck_{truck}': route.value for truck, route in enumerate(truck_sequences)}, **{f'cluster_{cluster}': route.value for cluster, route in enumerate(cluster_sequences)}}
print(f"Total distance = {values['total_distance']}; Status = {solution.feasible}")
route_lines = []
for truck in range(nb_trucks):
customers = [
clusters_data[cluster][customer] + 2
for cluster in values[f"truck_{truck}"]
for customer in values[f"cluster_{cluster}"]
]
route_line = " ".join(map(str, customers))
route_lines.append(route_line)
print(f"Truck {truck + 1}: {route_line}")
if output_file is not None:
output_text = "\n".join(
[str(int(values["total_distance"])), *route_lines]
)
Path(output_file).write_text(output_text + "\n", encoding="utf-8")
return solution
INSTANCE_DIR = Path.cwd() / "instances"
solution = main(INSTANCE_DIR / "A-n32-k5-C11-V2.gvrp", time_limit=5)