Коммивояжёр и NP-трудность

Коммивояжёр — символ вычислительно трудных задач: проверить решение легко, найти лучшее — мучительно дорого.

NP-трудная задача — задача, для которой не известно алгоритма, гарантированно находящего точный оптимум за полиномиальное время; время точного решения растёт экспоненциально.

Задача коммивояжёра

Дано $n$ городов и расстояния между ними. Нужно найти кратчайший замкнутый маршрут, проходящий через каждый город ровно раз. Решений — все перестановки городов, их $(n-1)!/2$. Для $n=10$ это $\sim181$ тысяча, для $n=20$ — больше $10^{16}$, для $n=60$ перестановок больше, чем атомов во Вселенной. Полный перебор обречён.

Лёгкая проверка, трудный поиск

Парадокс трудных задач: проверить данный маршрут (сложить расстояния) элементарно — $O(n)$. А найти лучший — невероятно трудно. Это и есть суть класса NP: решение проверяется быстро, но пространство решений экспоненциально. Коммивояжёр и рюкзак (в общей форме) — NP-трудные; для них не известно (и, по гипотезе $P\ne NP$, скорее всего не существует) быстрого точного алгоритма.

Рост перебора в числах

import math

print(f"{'городов':>8} | {'маршрутов (n-1)!/2':>22}")
for n in [5, 8, 10, 15, 20]:
    routes = math.factorial(n - 1) // 2
    print(f"{n:8d} | {routes:22d}")

Вывод:

 городов |     маршрутов (n-1)!/2
       5 |                     12
       8 |                   2520
      10 |                 181440
      15 |            43589145600
      20 |      60822550204416000

Каждые несколько добавленных городов умножают число маршрутов на порядки — наглядная экспонента. Перебор реально работает лишь до $n\approx12$.

Что делать с трудными задачами

ПодходЧто даётКогда
ПереборТочный оптимумКрошечные $n$ ($\le12$)
Ветви и границыТочный оптимум, отсекая бесперспективные ветвиМалые-средние $n$
Эвристики (ближайший сосед, 2-opt)Хорошее решение быстроБольшие $n$
Метаэвристики (отжиг, ГА)Очень хорошее решениеБольшие $n$, есть время
Приближённые алгоритмыРешение с гарантией «не хуже чем в $k$ раз»Когда нужна гарантия качества

Ветви и границы — идея

Метод ветвей и границ (branch and bound) даёт точный ответ, но умнее перебора. Он строит дерево частичных маршрутов и для каждого считает нижнюю границу (оптимистичную оценку лучшего возможного продолжения). Если граница хуже уже найденного полного маршрута, всю ветвь отсекают, не разворачивая. Так дерево обрезается, и часто удаётся решить точно куда большие задачи, чем позволяет голый перебор.

Как работает под капотом

Граница между «лёгкими» (полиномиальными, класс P) и «трудными» (NP-трудными) задачами — центральный вопрос информатики ($P$ против $NP$, одна из задач тысячелетия). Практический вывод для оптимизатора прост: распознав NP-трудность задачи, не тратьте силы на поиск быстрого точного алгоритма (его, вероятно, нет) — переходите к ветвям и границам для умеренных размеров или к (мета)эвристикам для больших. Именно поэтому метаэвристики из прошлого раздела так важны: они — рабочий инструмент против комбинаторного взрыва.

Частые ошибки

  • Перебор для больших $n$. Уже при $n>12$ он безнадёжен — сразу берите эвристики.
  • Ждать точного оптимума за разумное время. Для NP-трудных задач большого размера это нереалистично; цельтесь в «достаточно хорошо».
  • Путать «трудно проверить» и «трудно найти». Проверка маршрута тривиальна; трудность — в поиске лучшего среди экспоненты вариантов.

Итоги

  • Коммивояжёр — NP-трудная задача: $(n-1)!/2$ маршрутов, перебор взрывается.
  • В NP-задачах решение проверяется быстро, но пространство поиска экспоненциально.
  • Точно — ветви и границы (умеренные $n$); для больших — эвристики и метаэвристики.
Проверьте себя
1. Сколько различных маршрутов в задаче коммивояжёра с $n$ городами?
A$n$
B$n^2$
C$(n-1)!/2$ — факториальный рост
D$2^n$
2. Что характерно для NP-трудных задач?
AРешение и найти, и проверить легко
BРешение легко проверить, но трудно найти (нет быстрого точного алгоритма)
CОни не имеют решений
DОни всегда выпуклы
3. Как метод ветвей и границ ускоряет точный поиск?
AОкругляет решение
BОтсекает ветви, чья нижняя граница хуже уже найденного решения
CИспользует градиент
DПеребирает все варианты быстрее