Перейти к основному содержимому

Задача маршрутизации с временными окнами

В соседних статьях Задача маршрутизации транспорта, Задача маршрутизации с грузоподъёмностью и Задача маршрутизации с погрузкой и доставкой рассмотрены базовый VRP и два его расширения. Эта статья — третье расширение того же базового кейса: к 17 точкам и 4 ТС добавляются временные окна на каждом узле. У каждого клиента задан целочисленный интервал [начало, конец], в пределах которого ТС обязано прибыть в эту точку. Если ТС приезжает раньше открытия окна — ждёт; если позже закрытия — решение становится недопустимым.

Задача встречается в курьерской доставке с расписанием «принимаем с 10:00 до 13:00», в развозке товаров по магазинам с открытием/закрытием, при обслуживании клиентов с прибытием бригад в согласованные интервалы — везде, где конечный потребитель определяет окно приёма.

Условия задачи

Задан набор из 17 точек. Узел 0 — общее депо: 4 транспортных средства выезжают из него, обслуживают остальные узлы и возвращаются обратно.

Время в пути между узлами задано квадратной матрицей 17 × 17 (в минутах) — это входная характеристика дорожной сети, которая в реальных задачах берётся из картографического сервиса или измерений; в примере она подана как литерал. Для каждого узла задано временное окно — пара целых чисел [начало, конец], в пределах которого ТС обязано прибыть в точку; конкретные значения окон приведены в шаге 3 «Ввод данных».

Дополнительно: каждое ТС может ожидать перед началом окна не более 30 минут, и общее время маршрута каждого ТС ограничено 30 минутами. Требуется минимизировать суммарное время маршрутов всех ТС при выполнении всех временных окон.

Решение задачи

1. Выбор модели

Применяется та же модель маршрутизации, что и в задачах коммивояжера и VRP. Накопление времени по маршруту описывается обычным ресурсом маршрутизации, как в vrp-capacity, только накапливается время в пути. Временные окна выражаются ограничениями вида «значение ресурса в точке маршрута не меньше A и не больше B» — менеджер ограничений модели поддерживает их напрямую.

2. Создание модели

Сборка модели — как в VRP-статье: построитель параметров, 17 узлов, 4 ТС с общим депо в узле 0, фиксация состава.

// Создаём построитель параметров модели маршрутизации
Построитель = О2.Модели()
.МодельМаршрутизации()
.СоздатьПостроительПараметровМодели();

// Добавляем 17 узлов
КоличествоУзлов = 17;
Для К = 0 По КоличествоУзлов - 1 Цикл
Построитель.ДобавитьУзел("Узел" + К);
КонецЦикла;

// Добавляем 4 транспортных средства с общим депо в узле 0
КоличествоТС = 4;
Для К = 1 По КоличествоТС Цикл
Построитель.ДобавитьТранспортноеСредство(0, 0, "ТС" + К);
КонецЦикла;

// Фиксируем состав модели
Модель = Построитель.СоздатьМодель();

3. Ввод данных

Матрица времени в пути 17 × 17 собирается из 17 строк целых чисел через утилиту МассивЧиселИзСтроки. Результат — массив из 17 массивов, готовый к регистрации как транзит-матрица.

// Матрица времени в пути между узлами (минуты), 17 × 17
Времена = Новый Массив();
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0"));

// Временные окна узлов: индекс — номер узла, элементы — [начало, конец]
Окна = Новый Массив();
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 0 — депо
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 12")); // 1
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 2
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("16, 18")); // 3
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 13")); // 4
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 5
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 6
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 4")); // 7
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 8
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 3")); // 9
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 16")); // 10
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 11
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 12
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 13
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 8")); // 14
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 15
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("11, 15")); // 16

4. Регистрация матрицы транзитов

Матрица времён регистрируется в модели как транзит. Ссылка на созданный транзит сохраняется в переменную МатрицаТранзита и используется и при описании ресурса времени, и при описании целевой функции.

// Регистрируем матрицу времён как транзит
МатрицаТранзита = Модель
.Транзиты()
.ДобавитьМатрицу(Времена, "Время");

5. Ресурс времени маршрута

Время в пути описывается как ресурс маршрутизации, накапливающий значения матрицы времени по ходу маршрута. Параметры метода Модель.Ресурсы().Добавить(...):

  • первый аргумент — МатрицаТранзита (времена между узлами); ресурс растёт на величину пройденной дуги;
  • максимальный резерв на узле — 30: разрешённое ожидание перед открытием окна. Если ТС прибыло раньше начала окна, оно «ожидает» — это ожидание добавляется к накопленному времени и ограничено 30 минутами на одной остановке;
  • ёмкость — 30: верхний предел накопленного значения для каждого ТС, общий на весь автопарк (общая длительность маршрута не превышает 30 минут);
  • признак накопления с нуля — Ложь: стартовое значение ресурса на старте маршрута может быть больше нуля. Это нужно, потому что окно депо [0, 5] допускает старт ТС в любой момент в этих границах, а не строго в момент 0;
  • имя ресурса — "Время": метка для доступа к накопленному значению из решения.

Возвращённый объект ресурса сохраняем в переменную РесурсВремени — она потребуется на следующих шагах для установки временных окон.

// Ресурс времени: накапливается по матрице, ожидание ≤ 30 мин, общее время маршрута ≤ 30 мин,
// старт маршрута не привязан к нулю
РесурсВремени = Модель
.Ресурсы()
.Добавить(МатрицаТранзита, 30, 30, Ложь, "Время");

6. Временные окна клиентов

Для каждого узла-клиента (узлы 1–16) накладывается пара ограничений на значение ресурса Время в точке маршрута этого узла: оно должно быть не меньше начала окна и не больше конца. Точка маршрута клиента получается через Модель.ТочкиМаршрута().ПолучитьПоУзлу(индекс), ограничения — Модель.Ограничения().РесурсВТочкеМаршрутаНеМенее(...) и РесурсВТочкеМаршрутаНеБолее(...).

// Временные окна клиентов (узлы 1..16)
Ограничения = Модель.Ограничения();
ТочкиМаршрута = Модель.ТочкиМаршрута();
Для УзелКлиента = 1 По КоличествоУзлов - 1 Цикл
ТочкаКлиента = ТочкиМаршрута.ПолучитьПоУзлу(УзелКлиента);
Ограничения.РесурсВТочкеМаршрутаНеМенее(РесурсВремени, ТочкаКлиента, Окна[УзелКлиента][0]);
Ограничения.РесурсВТочкеМаршрутаНеБолее(РесурсВремени, ТочкаКлиента, Окна[УзелКлиента][1]);
КонецЦикла;

7. Временное окно депо на старте маршрута

Окно депо [0, 5] применяется к стартовой точке каждого ТС — это другая сущность, чем «точка узла 0»: у каждого ТС своя стартовая точка, доступная через Модель.ТочкиМаршрута().ПолучитьСтарт(...). Ограничения те же — не меньше начала окна, не больше конца.

// Окно депо [0, 5] на старте каждого ТС
Для ИндексТС = 0 По КоличествоТС - 1 Цикл
СтартТС = ТочкиМаршрута.ПолучитьСтарт(ИндексТС);
Ограничения.РесурсВТочкеМаршрутаНеМенее(РесурсВремени, СтартТС, Окна[0][0]);
Ограничения.РесурсВТочкеМаршрутаНеБолее(РесурсВремени, СтартТС, Окна[0][1]);
КонецЦикла;

8. Описание целевой функции

Целевая функция привязывается к матрице времени — минимизируется суммарное время маршрутов всех ТС. Временные окна и ограничение длительности влияют на множество допустимых маршрутов, а не на саму целевую функцию.

// Минимизируем суммарное время маршрутов всех ТС
Модель
.ЦелеваяФункция()
.УстановитьКоэффициентТранзита(МатрицаТранзита);

9. Решение модели

Стандартный вызов Модель.Решить(). Модель сама учтёт временные окна и ограничения длительности — отдельной настройки решателя не требуется.

// Решение модели
Решение = Модель.Решить();

10. Вывод результатов

Помимо имени ТС и пути на каждой остановке выводится время прибытия в эту точку. Его даёт поле Остановка.Ресурсы["Время"] — фиксированное соответствие, ключом которого является имя ресурса (заданное на шаге 5), а значением — накопленное значение ресурса в точке. Формат строки маршрута — 0 (0) → 9 (2) → 14 (7) → 16 (11) → 0 (18); в скобках — время прибытия в точку. После пути выводится суммарное время маршрута ТС. В шапке — общее время по всему автопарку.

// Вывод результатов
Если Решение.РешениеНайдено() Тогда
ФорматВывода = "ЧДЦ=0; ЧН=0; ЧГ=0";

Сообщение = СтрШаблон("Задача маршрутизации с временными окнами (%1 ТС, %2 вершин)%3", КоличествоТС, КоличествоУзлов, Символы.ПС);
Сообщение = Сообщение + Символы.ПС;
Сообщение = Сообщение + СтрШаблон("Суммарное время маршрутов: %1%2", Формат(Решение.ЗначениеЦелевойФункции(), ФорматВывода), Символы.ПС);
Сообщение = Сообщение + Символы.ПС;

Для Каждого Маршрут Из Решение.Маршруты() Цикл
ПунктыПути = Новый Массив();
Для Каждого Остановка Из Маршрут.Остановки Цикл
ВремяПрибытия = Остановка.Ресурсы["Время"];
ПунктыПути.Добавить(СтрШаблон("%1 (%2)", Остановка.Узел, Формат(ВремяПрибытия, ФорматВывода)));
КонецЦикла;
Путь = СтрСоединить(ПунктыПути, " → ");

Сообщение = Сообщение + СтрШаблон(
"Маршрут %1 (время %2): %3%4",
Маршрут.ИмяТранспортногоСредства,
Формат(Маршрут.Стоимость, ФорматВывода),
Путь,
Символы.ПС
);
КонецЦикла;

Сообщить(Сообщение);
Иначе
Сообщить("Решение не найдено! Статус: " + Решение.Статус());
КонецЕсли;

Полный код решения задачи

Ниже представлен полный код решения. Вы также можете скачать пример в виде готовой обработки.

// Создаём построитель параметров модели маршрутизации
Построитель = О2.Модели()
.МодельМаршрутизации()
.СоздатьПостроительПараметровМодели();

// Добавляем 17 узлов
КоличествоУзлов = 17;
Для К = 0 По КоличествоУзлов - 1 Цикл
Построитель.ДобавитьУзел("Узел" + К);
КонецЦикла;

// Добавляем 4 транспортных средства с общим депо в узле 0
КоличествоТС = 4;
Для К = 1 По КоличествоТС Цикл
Построитель.ДобавитьТранспортноеСредство(0, 0, "ТС" + К);
КонецЦикла;

// Фиксируем состав модели
Модель = Построитель.СоздатьМодель();

// Матрица времени в пути между узлами (минуты), 17 × 17
Времена = Новый Массив();
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9"));
Времена.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0"));

// Временные окна узлов: индекс — номер узла, элементы — [начало, конец]
Окна = Новый Массив();
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 0 — депо
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 12")); // 1
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 2
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("16, 18")); // 3
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 13")); // 4
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 5
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 6
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 4")); // 7
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 8
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 3")); // 9
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 16")); // 10
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 11
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("0, 5")); // 12
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("5, 10")); // 13
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("7, 8")); // 14
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("10, 15")); // 15
Окна.Добавить(О2.Утилиты().МассивЧиселИзСтроки("11, 15")); // 16

// Регистрируем матрицу времён как транзит
МатрицаТранзита = Модель
.Транзиты()
.ДобавитьМатрицу(Времена, "Время");

// Ресурс времени: накапливается по матрице, ожидание ≤ 30 мин, общее время маршрута ≤ 30 мин,
// старт маршрута не привязан к нулю
РесурсВремени = Модель
.Ресурсы()
.Добавить(МатрицаТранзита, 30, 30, Ложь, "Время");

// Временные окна клиентов (узлы 1..16)
Ограничения = Модель.Ограничения();
ТочкиМаршрута = Модель.ТочкиМаршрута();
Для УзелКлиента = 1 По КоличествоУзлов - 1 Цикл
ТочкаКлиента = ТочкиМаршрута.ПолучитьПоУзлу(УзелКлиента);
Ограничения.РесурсВТочкеМаршрутаНеМенее(РесурсВремени, ТочкаКлиента, Окна[УзелКлиента][0]);
Ограничения.РесурсВТочкеМаршрутаНеБолее(РесурсВремени, ТочкаКлиента, Окна[УзелКлиента][1]);
КонецЦикла;

// Окно депо [0, 5] на старте каждого ТС
Для ИндексТС = 0 По КоличествоТС - 1 Цикл
СтартТС = ТочкиМаршрута.ПолучитьСтарт(ИндексТС);
Ограничения.РесурсВТочкеМаршрутаНеМенее(РесурсВремени, СтартТС, Окна[0][0]);
Ограничения.РесурсВТочкеМаршрутаНеБолее(РесурсВремени, СтартТС, Окна[0][1]);
КонецЦикла;

// Минимизируем суммарное время маршрутов всех ТС
Модель
.ЦелеваяФункция()
.УстановитьКоэффициентТранзита(МатрицаТранзита);

// Решение модели
Решение = Модель.Решить();

// Вывод результатов
Если Решение.РешениеНайдено() Тогда
ФорматВывода = "ЧДЦ=0; ЧН=0; ЧГ=0";

Сообщение = СтрШаблон("Задача маршрутизации с временными окнами (%1 ТС, %2 вершин)%3", КоличествоТС, КоличествоУзлов, Символы.ПС);
Сообщение = Сообщение + Символы.ПС;
Сообщение = Сообщение + СтрШаблон("Суммарное время маршрутов: %1%2", Формат(Решение.ЗначениеЦелевойФункции(), ФорматВывода), Символы.ПС);
Сообщение = Сообщение + Символы.ПС;

Для Каждого Маршрут Из Решение.Маршруты() Цикл
ПунктыПути = Новый Массив();
Для Каждого Остановка Из Маршрут.Остановки Цикл
ВремяПрибытия = Остановка.Ресурсы["Время"];
ПунктыПути.Добавить(СтрШаблон("%1 (%2)", Остановка.Узел, Формат(ВремяПрибытия, ФорматВывода)));
КонецЦикла;
Путь = СтрСоединить(ПунктыПути, " → ");

Сообщение = Сообщение + СтрШаблон(
"Маршрут %1 (время %2): %3%4",
Маршрут.ИмяТранспортногоСредства,
Формат(Маршрут.Стоимость, ФорматВывода),
Путь,
Символы.ПС
);
КонецЦикла;

Сообщить(Сообщение);
Иначе
Сообщить("Решение не найдено! Статус: " + Решение.Статус());
КонецЕсли;
  Скачать пример