Решение задачи об оптимальных маршрутах — это классическая отраслевая задача, имеющая множество практических приложений в сфере оптимизации цепочки поставок. Можно рассматривать это как вариант задачи коммивояжёра, где у нас есть отправная точка, например наш продуктовый склад, и набор городов для доставки, каждый маршрут между двумя городами имеет стоимость, например стоимость топлива и пошлины и налоги. Мы также можем предположить, что каждая дорога имеет форму общей доступной пропускной способности, а каждый город имеет определенный спрос и излишек, который он может дать.
Итак, теперь наша проблема заключается в следующем: мы хотим максимизировать количество отправляемых товаров, минимизируя расстояние и стоимость и доставляя их в каждый отдельный город.
это сложная задача комбинаторной оптимизации, и в этой статье мы рассмотрим, как мы можем решить ее, используя вышеупомянутые методы.
Мы начнем с простого, а затем добавим больше сложности по ходу дела.
1- Генерация данных
Предположим, наша судоходная компания находится во Франции, наш склад находится в Париже, и мы должны доставить товар в несколько городов по всей стране. Мы будем использовать библиотеку networkx в python для создания графических данных, представляющих эту проблему.
import networkx as nx import geopandas as gpd from shapely.geometry import Point, LineString import matplotlib.pyplot as plt # Set the number of nodes (cities) num_nodes = 10 # Define the city data city_data = { 'city': ['Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille'], 'lat': [48.8567, 43.2964, 45.7600, 43.6045, 43.7034, 47.2181, 43.6119, 48.5833, 44.8400, 50.6278], 'lng': [2.3522, 5.3700, 4.8400, 1.4440, 7.2663, -1.5528, 3.8772, 7.7458, -0.5800, 3.0583] } # Create a complete graph with num_nodes nodes G = nx.complete_graph(num_nodes) # Add nodes to the graph with city names as labels for i in range(num_nodes): G.nodes[i]['city'] = city_data['city'][i] # Create a GeoDataFrame for the cities with Point geometries gdf_cities = gpd.GeoDataFrame(city_data, geometry=gpd.points_from_xy(city_data['lng'], city_data['lat'])) # Create a GeoDataFrame for the routes (edges) routes = [] for edge in G.edges(): city1 = G.nodes[edge[0]]['city'] city2 = G.nodes[edge[1]]['city'] route = LineString([(gdf_cities[gdf_cities['city'] == city1].geometry.x.iloc[0], gdf_cities[gdf_cities['city'] == city1].geometry.y.iloc[0]), (gdf_cities[gdf_cities['city'] == city2].geometry.x.iloc[0], gdf_cities[gdf_cities['city'] == city2].geometry.y.iloc[0])]) routes.append(route) gdf_routes = gpd.GeoDataFrame(geometry=routes) # Create a plot for the map of France fig, ax = plt.subplots(figsize=(10, 8)) world = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres')) france_shapefile = world[world.name == 'France'] france_shapefile.boundary.plot(ax=ax, color='lightgray', linewidth=0.8) gdf_cities.plot(ax=ax, marker='o', color='red', markersize=100, alpha=0.7) gdf_routes.plot(ax=ax, color='blue', linewidth=1, alpha=0.6) # Add labels for the cities for x, y, label in zip(gdf_cities.geometry.x, gdf_cities.geometry.y, gdf_cities['city']): ax.annotate(label, xy=(x, y), xytext=(3, 3), textcoords="offset points", color='black', fontsize=10) # Set the axis limits to focus on France ax.set_xlim(-5, 10) ax.set_ylim(40, 54) # Show the plot plt.title('Cities and Routes in France') plt.show()
Теперь давайте начнем с простой задачи, где мы хотим только минимизировать стоимость расстояния, мы предположим, что стоимость составляет 0,25 евро за км, и вычислим веса для каждого ребра как такового.
# Compute the distance and add weight to each edge from geopy.distance import geodesic import numpy as np for edge in G.edges(): city1 = G.nodes[edge[0]]['city'] city2 = G.nodes[edge[1]]['city'] coord1 = (city_data['lat'][city_data['city'].index(city1)], city_data['lng'][city_data['city'].index(city1)]) coord2 = (city_data['lat'][city_data['city'].index(city2)], city_data['lng'][city_data['city'].index(city2)]) distance_km = geodesic(coord1, coord2).kilometers G.edges[edge]['weight'] = np.round(0.25 * distance_km) # Create a plot for the graph data with assigned weights fig, ax = plt.subplots(figsize=(10, 8)) # Create a GeoDataFrame for the cities with Point geometries gdf_cities = gpd.GeoDataFrame(city_data, geometry=gpd.points_from_xy(city_data['lng'], city_data['lat'])) # Draw the cities as nodes pos = {i: (city_data['lng'][i], city_data['lat'][i]) for i in range(num_nodes)} nx.draw_networkx_nodes(G, pos, node_size=200, node_color='red', alpha=0.7, ax=ax) # Draw the routes as edges with weights as labels edges = nx.draw_networkx_edges(G, pos, edge_color='blue', alpha=0.6, width=1) edge_labels = nx.get_edge_attributes(G, 'weight') nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8, ax=ax) # Add labels for the cities for x, y, label in zip(gdf_cities.geometry.x, gdf_cities.geometry.y, gdf_cities['city']): ax.annotate(label, xy=(x, y), xytext=(3, 3), textcoords="offset points", color='black', fontsize=10) # Set the axis limits to focus on the cities ax.set_xlim(min(city_data['lng']) - 1, max(city_data['lng']) + 1) ax.set_ylim(min(city_data['lat']) - 1, max(city_data['lat']) + 1) # Show the plot plt.title('Graph Data with Assigned Weights (Distances in Euros)') plt.axis('equal') plt.show()
2- Простая проблема: путь Shotest минимизирует только стоимость расстояния
Хорошо, теперь мы можем попытаться найти решение этой проблемы, мы установим нашу начальную точку как Париж и используем алгоритм Кристофидеса–Сердюкова для аппроксимации решения, это можно сделать с помощью библиотеки networkx , в частности, функция: nx.приближение.traveling_salesman_problem(G, cycle=True)),Затем мы нанесем на график найденное решение
# Convert the problem to finding a Hamiltonian cycle starting and ending at the specified start point start_point = 'Paris' # Add the start point to the city_data dictionary start_lat = city_data['lat'][city_data['city'].index(start_point)] start_lng = city_data['lng'][city_data['city'].index(start_point)] city_data['city'].append(start_point) city_data['lat'].append(start_lat) city_data['lng'].append(start_lng) # Create a new node representing the start point new_node_id = num_nodes G.add_node(new_node_id, city=start_point) # Connect the new node to all other nodes for i in range(num_nodes): city = G.nodes[i]['city'] coord = (city_data['lat'][city_data['city'].index(city)], city_data['lng'][city_data['city'].index(city)]) distance_km = geodesic(coord, (start_lat, start_lng)).kilometers G.add_edge(new_node_id, i, weight=0.25 * distance_km) # Find the Hamiltonian cycle starting and ending at the specified start point optimal_route = list(nx.approximation.traveling_salesman_problem(G, cycle=True)) # Remove the added node representing the start point optimal_route.remove(new_node_id) # Rearrange the route to start from the specified start point start_index = optimal_route.index(city_data['city'].index(start_point)) optimal_route = optimal_route[start_index+1:] + optimal_route[:start_index+1] # Remove the added node representing the start point G.remove_node(new_node_id) # Rearrange the route to start from the specified start point start_index = optimal_route.index(new_node_id) optimal_route = optimal_route[start_index+1:] + optimal_route[:start_index+1] # Create a GeoDataFrame for the optimal route route_points = [(city_data['lng'][i], city_data['lat'][i]) for i in optimal_route] gdf_route = gpd.GeoDataFrame({'geometry': [LineString(route_points + [route_points[0]])]}) # Set the positions of nodes using spring_layout node_positions = nx.spring_layout(G, seed=42, iterations=100, scale=30) # Plot the optimal route in red color with arrows optimal_route_edges = [(optimal_route[i], optimal_route[i + 1]) for i in range(len(optimal_route) - 1)] + [(optimal_route[-1], optimal_route[0])] optimal_route_positions = {node: node_positions[node-1] if node != 0 else (0, 0) for node in optimal_route} # Create a plot for the graph data with assigned weights fig, ax = plt.subplots(figsize=(10, 8)) # Create a GeoDataFrame for the cities with Point geometries gdf_cities = gpd.GeoDataFrame(city_data, geometry=gpd.points_from_xy(city_data['lng'], city_data['lat'])) # Draw the routes as edges with weights as labels edges = nx.draw_networkx_edges(G, pos, edge_color='blue', alpha=0.6, width=1) edge_labels = nx.get_edge_attributes(G, 'weight') nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8, ax=ax) # Add labels for the cities for x, y, label in zip(gdf_cities.geometry.x, gdf_cities.geometry.y, gdf_cities['city']): ax.annotate(label, xy=(x, y), xytext=(3, 3), textcoords="offset points", color='black', fontsize=10) # Plot the optimal route in red color with arrows for i in range(len(optimal_route) - 1): city1 = city_data['city'][optimal_route[i]] city2 = city_data['city'][optimal_route[i + 1]] x_coords = [city_data['lng'][optimal_route[i]], city_data['lng'][optimal_route[i + 1]]] y_coords = [city_data['lat'][optimal_route[i]], city_data['lat'][optimal_route[i + 1]]] ax.plot(x_coords, y_coords, color='red', linewidth=2, alpha=0.6, zorder=1) # Add arrows to the line segments dx = x_coords[1] - x_coords[0] dy = y_coords[1] - y_coords[0] ax.arrow(x_coords[0], y_coords[0], dx, dy, head_width=0.1, head_length=0.1, fc='red', ec='red') # Connect the last city to the first city in the optimal route city1 = city_data['city'][optimal_route[-1]] city2 = city_data['city'][optimal_route[0]] x_coords = [city_data['lng'][optimal_route[-1]], city_data['lng'][optimal_route[0]]] y_coords = [city_data['lat'][optimal_route[-1]], city_data['lat'][optimal_route[0]]] ax.plot(x_coords, y_coords, color='red', linewidth=2, alpha=0.6, zorder=1) # Draw the cities as nodes on top of the edges #nx.draw_networkx_nodes(G, pos, node_size=200, node_color='red', alpha=0.7, ax=ax) # Set the axis limits to focus on the cities ax.set_xlim(min(city_data['lng']) - 1, max(city_data['lng']) + 1) ax.set_ylim(min(city_data['lat']) - 1, max(city_data['lat']) + 1) # Show the plot plt.title('Graph Data with Assigned Weights (Distances in Euros) and Optimal Route') plt.axis('equal') plt.show()
Итак, вот решение: начните с Париж -> Нант -> Бордо -> Тулуза -> Монпелье -> Ницца -> Марсель -> Лион -> Страсбург -> Лилль и, наконец, вернитесь в Париж. .
3- Добавление большей сложности: минимизация стоимости расстояния и стоимости транспортировки
Хорошо, теперь предположим, что у каждой дороги есть другая конкретная стоимость, такая как дорожные сборы и налоги, стоимость дороги больше не зависит от расстояния в км. Давайте попробуем смоделировать это, добавив некоторую случайную положительную стоимость к каждому ребру.
import random # Add random positive cost to each edge for edge in G.edges(): G[edge[0]][edge[1]]['cost'] = random.randint(25, 200) # Define a custom weight function that considers both distance and cost def custom_weight(u, v, data): distance_weight = data['weight'] # Distance weight (previously calculated) cost_weight = data['cost'] # Custom cost weight for the edge return distance_weight + cost_weight # Compute the shortest path with the custom weight function optimal_route = nx.approximation.traveling_salesman_problem(G, cycle=True, weight=custom_weight)
Теперь посмотрим решение
Итак, теперь решение: Начните с Парижа – › Лилля – › Парижа – › Нанта – › Ниццы – › Монпелье – › Тулузы – › Парижа – › Страсбурга – › Лиона – › Бордо – › Марселя – › Парижа, Обратите внимание, что мы не добавили условие не возвращаться в город, а хотя бы посетить каждый город по одному разу.
4 – Решение с помощью агента RL.
Чтобы решить с помощью агента RL, нам нужно определить нашу среду, то есть состояния: в каких городах мы сейчас находимся (посещенные города и еще не посещенные города), набор возможных действий (какой город посетить следующим) и вознаграждение ( отрицательное вознаграждение, представляющее стоимость) за каждое действие.
Для этого мы будем использовать следующие библиотеки на питоне: stable_baselines3 и gym.
from stable_baselines3 import DQN from stable_baselines3.common.env_util import DummyVecEnv import gym from gym import spaces # Define the RL environment as a wrapper for the TSPEnvironment class TSPGymEnv(gym.Env): def __init__(self, G): self.G = G self.num_nodes = G.number_of_nodes() self.reset() # Action space self.action_space = spaces.Discrete(self.num_nodes) # Observation space (current city) self.observation_space = spaces.Discrete(self.num_nodes) # Initialize the set of visited cities self.visited_cities = set() # Initialize the starting city as the current city self.current_city = 0 def reset(self): self.visited_cities = set() self.current_city = random.choice(list(self.G.nodes())) self.visited_cities.add(self.current_city) return self.current_city def step(self, action): if self.current_city not in self.visited_cities: # Add the current city to the set of visited cities self.visited_cities.add(self.current_city) next_city = list(self.G[self.current_city])[action - 1 if action != 0 else 0] reward = -self.G[self.current_city][next_city]['cost'] # Negative cost as the reward done = len(self.visited_cities) == self.num_nodes if not done: self.current_city = next_city return next_city, reward, done, {} # Create the RL environment env = DummyVecEnv([lambda: TSPGymEnv(G)]) # Create the DQN model model = DQN("MlpPolicy", env, verbose=1) # Train the model model.learn(total_timesteps=30000) # Create a list to store the optimal route optimal_route = [0] # Continue predicting the next cities until all cities have been visited while len(optimal_route) < num_nodes: # Convert the current_city to a numpy array current_city_array = np.array([current_city]) # Predict the next action using the RL model action, _ = model.predict(current_city_array) # Get the next city based on the predicted action next_city = list(G[current_city])[action[0] - 1 if action[0] != 0 else 0] # If the next city has not been visited yet, add it to the optimal route if next_city not in optimal_route: optimal_route.append(next_city) # Update the current_city for the next iteration current_city = next_city
Вот решение, найденное Агентом: Начните с Парижа – › Лиона – › Тулузы – › Бордо – › Лилля – › Ниццы – › Страсбурга – › Нанта – › Марселя – › Монпелье
5 – Проблема потока с минимальной стоимостью
Наконец, давайте предположим, что у вас есть общий запас d товаров для отправки, у каждого города есть требуемый спрос f и возможные излишки, которые он может предоставить, и для каждого маршрута существует максимальное количество товаров, которые вы можете отправить по этому конкретному маршруту, и мы хотят найти оптимальный маршрут, чтобы мы могли минимизировать общие затраты, а также максимально удовлетворить спрос. теперь проблема действительно стала довольно сложной задачей.
Математически это вариант задачи потока с минимальной стоимостью или сети потоков, мы можем сформулировать это как задачу оптимизации следующим образом:
Пусть G = (V , E ) — ориентированный граф с исходной вершиной s ∈ V и стоковой вершиной t ∈ V. Каждое ребро (u, v) ∈ E имеет положительную пропускную способность c(u, v), значение потока f(u, v) и стоимость a(u, v). Стоимость отправки потока по ребру (u, v) определяется выражением f(u, v) * a(u, v). Цель состоит в том, чтобы найти поток заданного количества d с минимальной стоимостью из источника s в сток t с учетом ограничений пропускной способности на ребрах. Цель состоит в том, чтобы минимизировать общую стоимость потока по всем ребрам:
Чтобы решить эту проблему, мы будем использовать симплексный алгоритм в networkx.
Симплексный алгоритм — это популярный алгоритм для решения задач линейного программирования, то есть задач оптимизации, в которых целевая функция и ограничения являются линейными. Алгоритм работает, двигаясь от одной вершины допустимой области (множество точек, удовлетворяющих ограничениям) к другой, вдоль ребер многогранника (геометрической формы, образованной ограничениями), пока не достигнет оптимального решения или не найдет что задача неограничена12
Алгоритм включает в себя добавление резервных переменных для преобразования неравенств в равенства, а затем выполнение ряда операций, называемых поворотом, которые обменивают базовую переменную (та, которая появляется в левой части равенства) на небазовую переменную ( тот, который появляется в правой части или целевой функции), сохраняя при этом осуществимость и улучшая целевое значение34
1: Симплексный алгоритм — Википедия 2: Симплексный алгоритм — Открытый учебник по вычислительной оптимизации Корнельского университета — Optimization Wiki 3: 1 Симплексный метод — Факультет компьютерных наук 4: Симплексный метод решения LPP (с примерами ) | Оперативное исследование
Итак, сначала нам нужно переопределить наш график и создать новые данные.
Симплексный решатель в networkx принимает только направленные графы, поэтому мы создадим ориентированный граф, представляющий наши города и ограничения, мы предположим, что у нас есть общий начальный запас 1000, и для упрощения генерации данных мы назначим случайный спрос для каждого города. , то мы будем использовать метод networkx nx.network_simplex(G), чтобы найти решение.
Также обратите внимание, что при таком подходе мы больше не предполагаем, что у нас есть только одно депо в Париже и один грузовик доставки, курсирующий по всем городам, а вместо этого каждый город с избытком можно рассматривать как склад и может отправить небольшой грузовик (или несколько грузовиков). ) для доставки товаров и удовлетворения некоторых потребностей других городов. В будущей статье мы рассмотрим, как еще больше ужесточить эти ограничения, например, заставить один грузовик разъезжать и иметь своего рода последовательный маршрут, то есть, например, мы не можем иметь несколько исходящих маршрутов из одного узла в то же самое время. время грузовик должен покинуть узел i до узла j, прежде чем вернуться в узел i, например, для пополнения запасов, или мы можем добавить еще одно ограничение на грузоподъемность самого грузовика, но мы также можем предположить, что в этом сценарии пропускная способность дорог фактически представляют собой грузоподъемность работающих на них грузовиков доставки.
import random import networkx as nx import matplotlib.pyplot as plt # Set the number of nodes (cities) and maximum possible routes between two nodes num_nodes = 10 max_routes = 3 required_flow = 1000 # Total flow across the graph # Define the city data city_data = { 'city': ['Paris', 'Marseille', 'Lyon', 'Toulouse', 'Nice', 'Nantes', 'Montpellier', 'Strasbourg', 'Bordeaux', 'Lille'], 'lat': [48.8567, 43.2964, 45.7600, 43.6045, 43.7034, 47.2181, 43.6119, 48.5833, 44.8400, 50.6278], 'lng': [2.3522, 5.3700, 4.8400, 1.4440, 7.2663, -1.5528, 3.8772, 7.7458, -0.5800, 3.0583] } # Create a random graph with num_nodes nodes and random edges G = nx.DiGraph() # Convert to directed graph # Add nodes to the graph with demand attributes total_demand = 0 # Set the source node (Paris) demand to a negative value for city in city_data['city']: if city_data['city'] != 'Paris': demand = random.randint(-required_flow, required_flow) G.add_node(city, demand=demand) total_demand += demand else: demand = -required_flow G.add_node(city, demand=demand) total_demand += demand # Balance the total demand to sum to 0 G.nodes[city_data['city'][-1]]['demand'] -= total_demand # Generate random edges between nodes for i in range(num_nodes): for j in range(i + 1, num_nodes): if random.random() < 0.5: num_routes = random.randint(1, max_routes) for _ in range(num_routes): distance = random.randint(50, 500) # Random distance between 50 to 500 km cost = random.randint(10, 100) # Random cost between 10 to 100 Euros capacity = random.randint(100, 500) # Random capacity between 100 to 500 units G.add_edge(city_data['city'][i], city_data['city'][j], distance=distance, cost=cost, capacity=capacity) G.add_edge(city_data['city'][j], city_data['city'][i], distance=distance, cost=cost, capacity=capacity) # Reverse direction # Solve the problem using Minimum Cost Flow algorithm with required flow constraint flow_cost, flow_dict = nx.network_simplex(G)
Теперь давайте найденное решение:
Оптимальный маршрут: [('Париж', 'Нант'), ('Париж', 'Страсбург'), ('Париж', 'Лилль'), ('Марсель', 'Страсбург'), ('Марсель', ' Лилль'), ('Лион', 'Париж'), ('Лион', 'Марсель'), ('Лион', 'Лилль'), ('Тулуза', 'Марсель'), ('Тулуза', ' Бордо'), ('Ницца', 'Марсель'), ('Ницца', 'Бордо'), ('Нант', 'Тулуза'), ('Нант', 'Бордо'), ('Монпелье', ' Лион'), ('Монпелье', 'Лилль'), ('Бордо', 'Марсель'), ('Бордо', 'Лилль')]
Общее расстояние: 4895 км
Общая стоимость: 5763 евро
Общая мощность: 5697 единиц
Общий поток достигнут: 711 единиц
На данный момент это все. В следующий раз мы увидим, как мы можем создать агент RL, чтобы попытаться решить эту сложную проблему, поскольку среда теперь намного сложнее, например, набор действий больше не просто куда двигаться дальше, но также сколько вы можете взять с собой, соблюдая все ограничения, а также пытаясь минимизировать общую стоимость.
Надеюсь, эта статья окажется для вас полезной :)