Задача коммивояжера — это одна из самых известных задач на графах. По-английски ее называют TSP (Traveling Salesman Problem — задача странствующего торговца).
Представьте, что вы торговый представитель, который хочет объехать несколько ближайших городов.
Схематично маршруты между городами можно обозначить так:
На этой схеме города — это вершины графа, а дороги — его ребрами. У каждого ребра есть число, которое обозначает длину маршрута. При работе с графами это число называют весом ребра*, а сам граф — взвешенным.
Вы хотите сэкономить, поэтому ищете кратчайший маршрут и хотите посетить каждый город только один раз.
У этой задачи есть несколько разных решений — изучим первый способ.
Метод перебора
Для начала рассмотрим решение «в лоб» — перебором всех возможных вариантов. Чтобы решить задачу перебором, нужно:
- Построить все возможные маршруты
- Сложить вес всех ребер в каждом маршруте
- Найти путь с минимальной суммой
Посмотрим, как это работает на практике. Для примера возьмем маршрут по городам ABCFED:
- Из города A едем в город B — между ними 44 километра
- Едем из B в C — 45 километров
- Едем из C в F — 15 километров
- Едем из F в E — 35 километров
- Едем из E в D — 17 километров
- Чтобы замкнуть маршрут, едем из D в A — 32 километра
В итоге весь маршрут составит 188 километров.
Затем мы строим другой маршрут, попутно вычисляя его суммарный вес. Если новый маршрут короче текущего, запоминаем его — теперь мы считаем его лучшим найденным решением.
Когда алгоритм переберет все варианты, у нас останется лучшее решение из всех маршрутов.
Обратите внимание, что решения может и не быть. Для примера посмотрим на такую схему:
Граф на этом рисунке невозможно обойти, так чтобы мы начали из точки A и побывали в каждой вершине всего по одному разу.
Для такого графа метод перебора должен вернуть что-то, обозначающее отсутствие решения — значение null или пустой массив.
Реализация метода перебора
Граф для задачи коммивояжера можно хранить любым удобным способом. Остановимся на матрице смежности, которую изучали ранее в курсе.
Если вершины связаны ребром, то мы поместим в матрицу его вес, если нет — добавим значение null:
Создадим класс Route, который упрощает построение маршрута. Он хранит суммарную длину пути и список вершин, который алгоритм успел обойти:
Метод add() добавляет к маршруту новую вершину и вес ребра, которое ведет к этой вершине.
Используем класс Route при реализации метода tsp(). Напоминаем, что TSP — это Travelling Salesman Problem:
Внутренняя рекурсивная функция bruteforce() получает в качестве параметров:
- Индекс следующей вершины
- Уже построенный маршрут
Эта функция помечает новую вершину как посещенную и проверяет, сколько всего помеченных вершин. Если их количество равно общему количеству вершин, мы построили весь маршрут — то есть обошли все вершины. Это конец рекурсии.
Далее нам надо выбрать минимальный маршрут, его мы храним в переменной min. Она обновляется, как только мы находим новый маршрут с меньшей длительностью:
Если мы не посмотрели все вершины, то находимся в середине построения маршрута. Отбираем из матрицы все вершины, до которых можно добраться (значение в матрице this.edges не равно null).
Рекурсивно вызываем bruteforce() для каждой подходящей вершины:
Алгоритм всегда начинает построение маршрута с первой вершины. Проверим его на графе-пятиугольнике, где все вершины соединены со всеми:
Самым коротким маршрутом будет обход этого графа по сторонам. Если длина каждой стороны равна 100, полная длина маршрута составит 500. Длина лучей звезды на такой картинке равна 162. Если алгоритм свернет на один из лучей, длина маршрута окажется больше:
Здесь мы видим, что длина маршрута равна 500 — значит, мы нашли кратчайший маршрут через все вершины.
Оценка сложности
Представим, что у нас есть четыре попарно связанные вершины, как показано на рисунке:
Сколько существует способов обойти весь граф, если мы всегда начинаем с первой вершины? Посмотрим на этот рисунок:
Здесь мы видим, что таких способов всего шесть. Они образуют три пары — в каждой паре один и тот же маршрут. Маршруты в парах отличаются только направлением обхода.
Выпишем все маршруты в таблицу:
Как видите, первая и последняя точка никогда не меняется — это всегда 1. Середина маршрута — различные перестановки вершин 2, 3 и 4.
В этом случае у нас три вершины, поэтому количество перестановок вычисляется так:
3!=1*2*3=6
Именно поэтому у нас шесть маршрутов.
Первая вершина всегда фиксирована и не участвует в перестановках, поэтому общее количество маршрутов для n вершин равно:
(n-1)!
Если из каждой симметричной пары брать только один маршрут, то маршрутов станет вполовину меньше:
((n-1)!)/2
В обоих случаях в нотации O-большое алгоритмическая сложность при решении задачи коммивояжера методом перебора оценивается как O(n!).
Метод ветвей и границ
Алгоритмы с факториальной сложностью O(n!) работают даже медленнее алгоритмов с экспоненциальной сложностью O(2^n). Для пяти вершин алгоритм перебрал всего 4!=24 маршрута. Но чем больше вершин, тем сложнее:
- Для 10 городов потребуется сравнить больше 300 000 маршрутов
- Для 21 города количество вариантов возрастает до миллиарда миллиардов. Если тысяча мощных компьютеров будет перебирать миллион вариантов в секунду, то для решения задачи потребуется чуть больше тридцати лет
Ранее в курсе мы узнали, что есть способ найти более быстрое решение — жадный алгоритм. К сожалению, он часто оказывается неоптимальным. Обсудим его подробнее.
Вместо перебора, мы можем воспользоваться другим способом — методом ветвей и границ. Перебор выбирает одно единственное решение, которое кажется лучшим. В отличие от него, метод ветвей и границ концентрируется на том, чтобы отбрасывать заведомо плохие варианты.
Как и перебор, метод ветвей и границ гарантирует нахождение лучшего решения, но при этом он может найти его за приемлемое время. Но есть потенциальная проблема — в худшем случае он вырождается в метод перебора и может работать слишком долго.
Как работает метод ветвей и границ
Метод ветвей и границ довольно сложный. Он позволяет решать разные задачи на графах — задачу о рюкзаке, о коммивояжере и другие. Именно поэтому его описание достаточно абстрактное.
Кроме того, сам метод состоит из нескольких шагов. ИХ детальное описание может отличаться от задачи к задаче.
Чтобы упростить изучение метода, мы освоим его в два этапа. Сейчас рассмотрим метод в целом и применительно к задаче коммивояжера, а уже в следующем уроке — познакомимся с алгоритмом Литтла.
Отличие от метода перебора
И перебор, и метод ветвей и границ обходят деревья решений, но строят эти деревья по-разному. Для примера сравним деревья, которые получаются для графа из четырех вершин.
В методе перебора мы начинаем с пустого графа — графа без построенного маршрута. На первом шаге у нас есть три возможных варианта начать маршрут:
- Ребро 1-2
- Ребро 1-3
- Ребро 1-4
Из корня мы начинаем строить три поддерева, каждое из которых соответствует одному из ребер.
Будем работать с каждым поддеревом по очереди. Сначала обработаем узел, который соответствует ребру 1-2. На этом этапе алгоритм построил маршрут до вершины 2. Далее он может добавить:
- Либо ребро 2-3
- Либо ребро 2-4
Значит, из узла 1-2 можно построить еще два поддерева. Вершина дерева решений выглядит так:
В методе ветвей и границ построение дерева также начинается с пустого графа, но алгоритм всегда строит бинарное дерево.
Он берет первое доступное ребро 1-2 и строит два поддерева:
- В первое попадают те маршруты, где ребро 1-2 есть
- Во второе — те, где его нет
Посмотрим на узел, соответствующий узлу 1-2. Как мы помним, из него выходят два ребра, чтобы продолжить маршрут:
Выбираем первый вариант и снова строим два поддерева — с ребром 2-3 и без него. Вершина дерева решений будет такой:
Нижняя граница
Построив очередное поддерево, попытаемся оценить нижнюю границу длины всех маршрутов в этом поддереве.
Это не так просто. Если бы поддерево было построено, мы могли мы найти самый короткий маршрут, сравнив длины всех маршрутов. Но сейчас у нас есть только корень поддерева.
Нижняя граница должна быть совершенно корректна — в поддереве не должно быть маршрутов короче ее. С другой стороны, мы не можем просто использовать ноль, мы хотим оценить нижнюю границу как можно точнее. Таким образом метод ветвей и границ не будет тратить время на заведомо плохие поддеревья.
Мы вернемся к вопросу о том, как оценивать нижнюю границу, разбирая алгоритм Литтла в следующем уроке. Пока же будем считать, что способ оценки нам известен и он достаточно точен.
Отсечение ветвей
Встретив поддерево с большой нижней границей, мы можем отказаться от его обработки, потому что все решения в нем будут слишком длинными. Иными словами, мы можем отсечь эту ветвь дерева решений.
На этом шаге можно случайно отбросить ветвь, которая только выглядит плохой, а на самом деле содержит кратчайший маршрут.
Чтобы этого не допустить, нужно все тщательно проверить. Есть несколько способов убедиться, что в поддереве нет кратчайшего маршрута:
-
Сравнить верхние и нижние границы. Представим, что в первом поддереве верхняя граница меньше, чем нижняя граница во втором. В таком случае все маршруты второго поддерева длиннее, чем маршруты первого. Это значит, что второе поддерево можно отсечь, потому что самого короткого маршрута в нем точно нет
-
Построить дерево решений вглубь и добраться до первого завершенного маршрута*. Этот маршрут — лучший из тех, что нам известны. В оригинальном описании алгоритма его называют рекордным. Скорее всего, это не самый короткий маршрут. По мере работы алгоритм может найти более короткий путь — тогда он будет рекордным. Если нижняя граница поддерева больше, чем длина рекордного маршрута, это поддерево также можно отсекать
В алгоритме Литтла применяется второй подход. В следующем уроке мы разберемся с тем, как строить рекордные маршруты.
Рекурсия
После отсечения у нас остаются несколько ветвей, где может находиться оптимальное решение. Мы рекурсивно продолжаем работу с самой перспективной ветвью — это ветвь с наименьшей нижней границей. Для хранения не отсеченных ветвей удобно использовать структуру данных, которая называется очередь с приоритетом. Значения в такую очередь добавляются в произвольном порядке, а извлекаются в порядке возрастания или убывания приоритета.
Рекурсия завершается, когда на очередном шаге мы добираемся до листа в дереве решений.
На рисунке ниже вы увидите пример готового дерева. Мы обрезаем две ветви и добираемся до оптимального решения, в данном случае — до маршрута 1-2-4-3:
В следующем уроке мы продолжим изучение метода ветвей и границ и познакомимся с алгоритмом Литтла — адаптацией метода для решения задачи коммивояжера.
Выводы
В этом уроке мы познакомились с задачей коммивояжера и двумя методами ее решения. Повторим основные выводы:
- Одна из самых частых задач, которая встречается в логистике — задача коммивояжера
- Задача коммивояжера легко решается методом перебора, но алгоритмическая сложность такого решения слишком высокая — O(n!)
- Чтобы снизить алгоритмическую сложность, можно использовать метод ветвей и границ
- Метод ветвей и границ — общий метод, который применяется для решения разных задач на графах
- В этом уроке мы изучали его версию, адаптированную для задачи коммивояжера. В этой версии есть способ вычисления нижней оценки для всех решений в поддереве
- Одна из известных адаптация метода ветвей и границ — алгоритм Литтла. Детально с этим алгоритмом мы познакомимся в следующем уроке
<!DOCTYPE html>
<html class="h-100" data-bs-theme="light" data-mantine-color-scheme="light" lang="ru" prefix="og: https://ogp.me/ns#">
<head>
<meta content="width=device-width, initial-scale=1.0" name="viewport">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<link crossorigin="true" href="https://cdn.hexlet.io" rel="preconnect">
<link href="https://mc.yandex.ru" rel="preconnect">
<meta content="aa2vrdtq64dub8knuf83lwywit311w" name="facebook-domain-verification">
<link href="/favicon.ico" rel="icon" sizes="any">
<link href="/favicon.svg" rel="icon" type="image/svg+xml">
<link href="/apple-touch-icon.png" rel="apple-touch-icon">
<link href="/manifest.webmanifest" rel="manifest">
<script>
//<![CDATA[
window.gon={};gon.ym_counter="25559621";gon.is_bot=true;gon.applications={};gon.current_user={"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26 15:13:27 UTC","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false};gon.token="ntHUEg97_5Kmqv2hvlFCgeis2Rp8bun9vyQNVKmOTZVxAB8l_QVS8hDp2TmyXrL2KKX0sHRZF18CxJcA-4mq-w";gon.locale="ru";gon.language="ru";gon.theme="light";gon.rails_env="production";gon.mobile=false;gon.google={"analytics_key":"UA-1360700-51","optimize_key":"GTM-5QDVFPF"};gon.captcha={"google_v3_site_key":"6LenGbgZAAAAAM7HbrDbn5JlizCSzPcS767c9vaY","yandex_site_key":"ysc1_Vyob5ZPPUdPBsu0ykt8bVFdzsfpoVjQChLGl2b4g19647a89","verification_failed":null};gon.social_signin=false;gon.typoreporter_google_form_id="1FAIpQLSeibfGq-KvWQ2Fyru-zkFFRVTLBuzXAHAoEyN1p49FtDmNoNA";
//]]>
</script>
<meta charset="utf-8">
<title>Задача коммивояжера | Алгоритмы на графах</title>
<meta name="description" content="Задача коммивояжера / Алгоритмы на графах: Учимся опознавать задачу о коммивояжере и решать ее двумя способами: с помощью перебора и с помощью метода ветвей и границ">
<link rel="canonical" href="https://ru.hexlet.io/courses/algorithms-graphs/lessons/traveling-salesman-problem/theory_unit">
<meta name="robots" content="noarchive">
<meta property="og:title" content="Задача коммивояжера">
<meta property="og:title" content="Алгоритмы на графах">
<meta property="og:description" content="Задача коммивояжера / Алгоритмы на графах: Учимся опознавать задачу о коммивояжере и решать ее двумя способами: с помощью перебора и с помощью метода ветвей и границ">
<meta property="og:url" content="https://ru.hexlet.io/courses/algorithms-graphs/lessons/traveling-salesman-problem/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="8Lm1MccOJXeDfC899-q6hBW5GMJa1w6BgUi9tgjFig8faH4GNXCIFzU_C6X75Urz1bA1aFLg8CM8qCfiWsJtYQ" />
<script src="/vite/assets/inertia-INZxX8jp.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/preload-helper-BJ4cLWpC.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ahoy-DrlRQ-1D.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/analytics-6pOtQ3OW.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Surface-DL2bpZA-.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/extends-C-EagtpE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/inheritsLoose-BBd-DCVI.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/objectWithoutPropertiesLoose-DRHXDhjp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/index.esm-DAqKOkZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Button-CGPUux8l.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/CloseButton-D1euiPao.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Group-BX48WcuU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Loader-BQEY8g6v.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Modal-Cy3HByv7.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/OptionalPortal-1Hza5P2w.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Stack-CtjJzfw4.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Textarea-Ck64llAy.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/DirectionProvider-Dc9zdUke.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/events-DJQOhap0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-reduced-motion-D2owz4wa.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-disclosure-zKtK5W1r.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/use-hotkeys-Cnc_Rwkb.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/random-id-DOQyszCZ.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/exports-C_MrNx_T.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<link rel="stylesheet" href="/vite/assets/application-BqhCP46M.js" />
<script src="/vite/assets/application-Df9RExpe.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/autocomplete-VMNbxKGl.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/createPopper-C3aM9r1M.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/js.cookie-D1-O8zkX.js" as="script" crossorigin="anonymous"><link rel="stylesheet" href="/vite/assets/application-C8HjmMaq.css" media="screen" />
<script>
window.ym = function(){(ym.a=ym.a||[]).push(arguments)};
window.addEventListener('load', function() {
setTimeout(function() {
ym.l = 1*new Date();
ym(window.gon.ym_counter, "init", {
clickmap: true,
trackLinks: true,
accurateTrackBounce: true,
webvisor: true
});
// Загружаем скрипт
var k = document.createElement('script');
k.async = 1;
k.src = 'https://mc.yandex.ru/metrika/tag.js';
document.head.appendChild(k);
ym(window.gon.ym_counter, 'getClientID', function(clientID) {
window.ymClientId = clientID;
});
}, 1500);
});
</script>
<!-- Google Tag Manager - deferred -->
<script>
// dataLayer stub сразу — пуши работают до загрузки скрипта
window.dataLayer = window.dataLayer || [];
// Сам скрипт — отложенно после load
window.addEventListener('load', function() {
setTimeout(function() {
dataLayer.push({'gtm.start': new Date().getTime(), event: 'gtm.js'});
var j = document.createElement('script');
j.async = true;
j.src = 'https://www.googletagmanager.com/gtm.js?id=GTM-WK88TH';
document.head.appendChild(j);
}, 1500);
});
</script>
<!-- End Google Tag Manager -->
</head>
<body>
<noscript>
<div>
<img alt="" src="https://mc.yandex.ru/watch/25559621" style="position:absolute; left:-9999px;">
</div>
</noscript>
<header class="sticky-top bg-body">
<nav class="navbar navbar-expand-lg">
<div class="container-xxl">
<a class="navbar-brand" href="/"><img alt="Логотип Хекслета" height="24" src="https://ru.hexlet.io/vite/assets/logo_ru_light-BpiEA1LT.svg" width="96">
</a><button aria-controls="collapsable" aria-expanded="false" aria-label="Меню" class="navbar-toggler border-0 mb-0 mt-1" data-bs-target="#collapsable" data-bs-toggle="collapse">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="collapsable">
<ul class="navbar-nav mb-lg-0 mt-lg-1">
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
Все курсы
<span class="bi bi-chevron-down align-middle ms-1"></span>
</button>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item d-flex py-2" href="/courses"><div class="fw-bold me-auto">Все что есть</div>
<div class="text-muted">117</div>
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные категории</b>
</li>
<li>
<a class="dropdown-item py-2" href="/courses_devops">Курсы по DevOps
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_data_analytics">Курсы по аналитике данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_programming">Курсы по программированию
</a></li>
<li>
<a class="dropdown-item py-2" href="/courses_testing">Курсы по тестированию
</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li class="dropdown-item">
<b>Популярные курсы</b>
</li>
<li>
<a class="dropdown-item py-2" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/go">Go-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/java">Java-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/python">Python-разработчик
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/qa-auto-engineer-java">Автоматизатор тестирования на Java
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/data-analytics">Аналитик данных
</a></li>
<li>
<a class="dropdown-item py-2" href="/programs/frontend">Фронтенд-разработчик
</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<button aria-haspopup class="btn nav-link" data-bs-toggle="dropdown" type="button">
О Хекслете
<span class="bi bi-chevron-down align-middle"></span>
</button>
<ul class="dropdown-menu bg-body">
<li>
<a class="dropdown-item py-2" href="/pages/about">О нас
</a></li>
<li>
<a class="dropdown-item py-2" href="/blog">Блог
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/hse-research" role="button">Результаты (Исследование)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://career.hexlet.io" role="button">Хекслет Карьера
</span></li>
<li>
<a class="dropdown-item py-2" href="/testimonials">Отзывы студентов
</a></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://t.me/hexlet_help_bot" role="button">Поддержка (В ТГ)
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/referal-program/?promo_creative=priglasite-druzei&promo_name=referal-program&promo_position=promo_position&promo_start=010724&promo_type=link" role="button">Реферальная программа
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://special.hexlet.io/certificate" role="button">Подарочные сертификаты
</span></li>
<li>
<span class="dropdown-item py-2 external-link" data-href="https://hh.ru/employer/4307094" role="button">Вакансии
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://b2b.hexlet.io" data-target="_blank" role="button">Компаниям
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexly.ru/" data-target="_blank" role="button">Колледж
</span></li>
<li>
<span class="dropdown-item d-flex external-link" rel="noopener noreferrer nofollow" data-href="https://hexlyschool.ru/" data-target="_blank" role="button">Частная школа
</span></li>
</ul>
</li>
<li><a class="nav-link" href="/subscription/new">Подписка</a></li>
</ul>
<ul class="navbar-nav flex-lg-row align-items-lg-center gap-2 ms-auto">
<li>
<a class="nav-link" aria-label="Переключить тему" href="/theme/switch?new_theme=dark"><span aria-hidden="true" class="bi bi-moon"></span>
</a></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="/u/new" role="button"><span>Регистрация</span>
</span></li>
<li>
<span data-target="_self" class="nav-link external-link" data-href="https://ru.hexlet.io/session/new" role="button"><span>Вход</span>
</span></li>
</ul>
</div>
</div>
</nav>
</header>
<div class="x-container-xxxl">
</div>
<main class="mb-6 min-vh-100 h-100">
<link rel="preload" as="image" href="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAyOCwicHVyIjoiYmxvYl9pZCJ9fQ==--ae9eed98663dd1201759d042a5ba7ca790866156/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programming-bro.png"/><link rel="preload" as="image" href="/vite/assets/development-BVihs_d5.png"/><div id="app" data-page="{"component":"web/courses/lessons/theory_unit","props":{"errors":{},"locale":"ru","language":"ru","httpsHost":"https://ru.hexlet.io","host":"ru.hexlet.io","colorScheme":"light","auth":{"user":{"id":null,"last_viewed_notification_id":null,"email":null,"state":null,"first_name":"","last_name":"","created_at":"2026-02-26T15:13:27.349Z","current_program":null,"current_team":null,"full_name":"","guest":true,"can_use_paid_features":false,"is_hexlet_employee":false,"sanitized_phone_number":"","can_subscribe":true,"can_renew_education":false}},"cloudflareTurnstileSiteKey":"0x4AAAAAAA15KmeFXzd2H0Xo","vkIdClientId":"51586979","yandexIdClientId":"88d071f1d3384eb4bd1deb37910235c7","formAuthToken":"w45kp2XwC-6fItFjS-ITJNbr3bkTHLsx4n4pxoK5qLcsX6-Ql46mjilh9ftH7eNTFuLwExsrRZNfnrOS0L5P2Q","topics":[],"lesson":{"exercise":null,"units":[{"id":8852,"name":"theory","url":"/courses/algorithms-graphs/lessons/traveling-salesman-problem/theory_unit"},{"id":11260,"name":"quiz","url":"/courses/algorithms-graphs/lessons/traveling-salesman-problem/quiz_unit"}],"links":[],"ordered_units":[{"id":8852,"name":"theory","url":"/courses/algorithms-graphs/lessons/traveling-salesman-problem/theory_unit"},{"id":11260,"name":"quiz","url":"/courses/algorithms-graphs/lessons/traveling-salesman-problem/quiz_unit"}],"id":3907,"slug":"traveling-salesman-problem","state":"approved","name":"Задача коммивояжера","course_order":600,"goal":"Учимся опознавать задачу о коммивояжере и решать ее двумя способами: с помощью перебора и с помощью метода ветвей и границ","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Задача коммивояжера — это одна из самых известных задач на графах. По-английски ее называют TSP (_Traveling Salesman Problem_ — задача странствующего торговца).\n\nПредставьте, что вы торговый представитель, который хочет объехать несколько ближайших городов.\n\nСхематично маршруты между городами можно обозначить так:\n\n\n\nНа этой схеме города — это вершины графа, а дороги — его ребрами. У каждого ребра есть число, которое обозначает длину маршрута. При работе с графами это число называют **весом ребра***, а сам граф — **взвешенным**.\n\nВы хотите сэкономить, поэтому ищете кратчайший маршрут и хотите посетить каждый город только один раз.\n\nУ этой задачи есть несколько разных решений — изучим первый способ.\n\n## Метод перебора\n\nДля начала рассмотрим решение «в лоб» — перебором всех возможных вариантов. Чтобы решить задачу перебором, нужно:\n\n* Построить все возможные маршруты\n* Сложить вес всех ребер в каждом маршруте\n* Найти путь с минимальной суммой\n\nПосмотрим, как это работает на практике. Для примера возьмем маршрут по городам `ABCFED`:\n\n* Из города `A` едем в город `B` — между ними 44 километра\n* Едем из `B` в `C` — 45 километров\n* Едем из `C` в `F` — 15 километров\n* Едем из `F` в `E` — 35 километров\n* Едем из `E` в `D` — 17 километров\n* Чтобы замкнуть маршрут, едем из `D` в `A` — 32 километра\n\nВ итоге весь маршрут составит 188 километров.\n\nЗатем мы строим другой маршрут, попутно вычисляя его суммарный вес. Если новый маршрут короче текущего, запоминаем его — теперь мы считаем его лучшим найденным решением.\n\nКогда алгоритм переберет все варианты, у нас останется лучшее решение из всех маршрутов.\n\nОбратите внимание, что решения может и не быть. Для примера посмотрим на такую схему:\n\n\n\nГраф на этом рисунке невозможно обойти, так чтобы мы начали из точки `A` и побывали в каждой вершине всего по одному разу.\n\nДля такого графа метод перебора должен вернуть что-то, обозначающее отсутствие решения — значение `null` или пустой массив.\n\n### Реализация метода перебора\n\nГраф для задачи коммивояжера можно хранить любым удобным способом. Остановимся на матрице смежности, которую изучали ранее в курсе.\n\nЕсли вершины связаны ребром, то мы поместим в матрицу его вес, если нет — добавим значение `null`:\n\n```javascript\nclass Graph {\n vertices\n size\n edges\n\n constructor(vertices) {\n this.vertices = vertices\n this.size = vertices.length\n this.edges = Array.from({ length: this.size }, () => {\n return Array(this.size).fill(null)\n })\n }\n\n addEdge(value1, value2, weight) {\n const row = this.vertices.indexOf(value1)\n const column = this.vertices.indexOf(value2)\n\n this.edges[row][column] = weight\n this.edges[column][row] = weight\n }\n}\n```\n\nСоздадим класс `Route`, который упрощает построение маршрута. Он хранит суммарную длину пути и список вершин, который алгоритм успел обойти:\n\n```javascript\nclass Route {\n vertices\n weight\n\n constructor(vertices, weight) {\n this.vertices = vertices\n this.weight = weight\n }\n\n add(vertex, incWeight) {\n let nextVertices = [...this.vertices]\n nextVertices.push(vertex)\n const nextWeight = this.weight + incWeight\n\n return new Route(nextVertices, nextWeight)\n }\n}\n```\n\nМетод `add()` добавляет к маршруту новую вершину и вес ребра, которое ведет к этой вершине.\n\nИспользуем класс `Route` при реализации метода `tsp()`. Напоминаем, что TSP — это _Travelling Salesman Problem_:\n\n```js\n tsp() {\n let seen = new Set();\n let min = null;\n\n const bruteforce = (i, current) => {\n seen.add(i);\n\n if (seen.size === this.size) {\n const weight = this.edges[i][0];\n if (weight !== null) {\n const route = current.add(this.vertices[0], weight);\n if (min === null || min.weight > route.weight) {\n min = route;\n }\n }\n } else {\n for (let j = 0; j < this.edges[i].length; j += 1) {\n const weight = this.edges[i][j];\n if (weight !== null && !seen.has(j)) {\n const route = current.add(this.vertices[j], weight);\n bruteforce(j, route);\n }\n }\n }\n seen.delete(i);\n };\n bruteforce(0, new Route([this.vertices[0]], 0));\n\n return min;\n }\n```\n\nВнутренняя рекурсивная функция `bruteforce()` получает в качестве параметров:\n\n* Индекс следующей вершины\n* Уже построенный маршрут\n\nЭта функция помечает новую вершину как посещенную и проверяет, сколько всего помеченных вершин. Если их количество равно общему количеству вершин, мы построили весь маршрут — то есть обошли все вершины. Это конец рекурсии.\n\nДалее нам надо выбрать минимальный маршрут, его мы храним в переменной `min`. Она обновляется, как только мы находим новый маршрут с меньшей длительностью:\n\n```js\nif (seen.size === this.size) {\n const weight = this.edges[i][0];\n if (weight !== null) {\n const route = current.add(this.vertices[0], weight);\n if (min === null || min.weight > route.weight) {\n min = route;\n }\n```\n\nЕсли мы не посмотрели все вершины, то находимся в середине построения маршрута. Отбираем из матрицы все вершины, до которых можно добраться (значение в матрице `this.edges` не равно `null`).\n\nРекурсивно вызываем `bruteforce()` для каждой подходящей вершины:\n\n```js\nfor (let j = 0; j < this.edges[i].length; j++) {\n const weight = this.edges[i][j];\n if (weight !== null && !seen.has(j)) {\n const route = current.add(this.vertices[j], weight);\n bruteforce(j, route);\n```\n\nАлгоритм всегда начинает построение маршрута с первой вершины. Проверим его на графе-пятиугольнике, где все вершины соединены со всеми:\n\n\n\nСамым коротким маршрутом будет обход этого графа по сторонам. Если длина каждой стороны равна 100, полная длина маршрута составит 500. Длина лучей звезды на такой картинке равна 162. Если алгоритм свернет на один из лучей, длина маршрута окажется больше:\n\n```js\nconst graph = new Graph(['A', 'B', 'C', 'D', 'E'])\ngraph.addEdge('A', 'B', 100)\ngraph.addEdge('A', 'C', 162)\ngraph.addEdge('A', 'D', 162)\ngraph.addEdge('A', 'E', 100)\ngraph.addEdge('B', 'C', 100)\ngraph.addEdge('B', 'D', 162)\ngraph.addEdge('B', 'E', 162)\ngraph.addEdge('C', 'D', 100)\ngraph.addEdge('C', 'E', 162)\ngraph.addEdge('D', 'E', 100)\n\ngraph.tsp()\n// Route {\n// vertices: [ 'A', 'B', 'C', 'D', 'E', 'A' ],\n// weight: 500\n// }\n```\n\nЗдесь мы видим, что длина маршрута равна 500 — значит, мы нашли кратчайший маршрут через все вершины.\n\n## Оценка сложности\n\nПредставим, что у нас есть четыре попарно связанные вершины, как показано на рисунке:\n\n\n\nСколько существует способов обойти весь граф, если мы всегда начинаем с первой вершины? Посмотрим на этот рисунок:\n\n\n\nЗдесь мы видим, что таких способов всего шесть. Они образуют три пары — в каждой паре один и тот же маршрут. Маршруты в парах отличаются только направлением обхода.\n\nВыпишем все маршруты в таблицу:\n\n| В одну сторону | В другую сторону|\n|----------------|-----------------|\n| `1, 2, 3, 4, 1`| `1, 4, 3, 2, 1` |\n| `1, 2, 4, 3, 1`| `1, 3, 4, 2, 1` |\n| `1, 3, 2, 4, 1`| `1, 4, 2, 3, 1` |\n\nКак видите, первая и последняя точка никогда не меняется — это всегда `1`. Середина маршрута — различные перестановки вершин `2`, `3` и `4`.\n\nВ этом случае у нас три вершины, поэтому количество перестановок вычисляется так:\n\n`3!=1*2*3=6`\n\nИменно поэтому у нас шесть маршрутов.\n\nПервая вершина всегда фиксирована и не участвует в перестановках, поэтому общее количество маршрутов для `n` вершин равно:\n\n`(n-1)!`\n\nЕсли из каждой симметричной пары брать только один маршрут, то маршрутов станет вполовину меньше:\n\n`((n-1)!)/2`\n\nВ обоих случаях в нотации `O`-большое алгоритмическая сложность при решении задачи коммивояжера методом перебора оценивается как `O(n!)`.\n\n## Метод ветвей и границ\n\nАлгоритмы с факториальной сложностью `O(n!)` работают даже медленнее алгоритмов с экспоненциальной сложностью `O(2^n)`. Для пяти вершин алгоритм перебрал всего `4!=24` маршрута. Но чем больше вершин, тем сложнее:\n\n* Для 10 городов потребуется сравнить больше `300 000` маршрутов\n* Для 21 города количество вариантов возрастает до миллиарда миллиардов. Если тысяча мощных компьютеров будет перебирать миллион вариантов в секунду, то для решения задачи потребуется чуть больше тридцати лет\n\nРанее в курсе мы узнали, что есть способ найти более быстрое решение — жадный алгоритм. К сожалению, он часто оказывается неоптимальным. Обсудим его подробнее.\n\nВместо перебора, мы можем воспользоваться другим способом — **методом ветвей и границ**. Перебор выбирает одно единственное решение, которое кажется лучшим. В отличие от него, метод ветвей и границ концентрируется на том, чтобы отбрасывать заведомо плохие варианты.\n\nКак и перебор, метод ветвей и границ гарантирует нахождение лучшего решения, но при этом он может найти его за приемлемое время. Но есть потенциальная проблема — в худшем случае он вырождается в метод перебора и может работать слишком долго.\n\n### Как работает метод ветвей и границ\n\nМетод ветвей и границ довольно сложный. Он позволяет решать разные задачи на графах — задачу о рюкзаке, о коммивояжере и другие. Именно поэтому его описание достаточно абстрактное.\n\nКроме того, сам метод состоит из нескольких шагов. ИХ детальное описание может отличаться от задачи к задаче.\n\nЧтобы упростить изучение метода, мы освоим его в два этапа. Сейчас рассмотрим метод в целом и применительно к задаче коммивояжера, а уже в следующем уроке — познакомимся с алгоритмом Литтла.\n\n### Отличие от метода перебора\n\nИ перебор, и метод ветвей и границ обходят деревья решений, но строят эти деревья по-разному. Для примера сравним деревья, которые получаются для графа из четырех вершин.\n\n**В методе перебора** мы начинаем с пустого графа — графа без построенного маршрута. На первом шаге у нас есть три возможных варианта начать маршрут:\n\n* Ребро `1-2`\n* Ребро `1-3`\n* Ребро `1-4`\n\nИз корня мы начинаем строить три поддерева, каждое из которых соответствует одному из ребер.\n\nБудем работать с каждым поддеревом по очереди. Сначала обработаем узел, который соответствует ребру `1-2`. На этом этапе алгоритм построил маршрут до вершины `2`. Далее он может добавить:\n\n* Либо ребро `2-3`\n* Либо ребро `2-4`\n\nЗначит, из узла `1-2` можно построить еще два поддерева. Вершина дерева решений выглядит так:\n\n\n\n**В методе ветвей и границ** построение дерева также начинается с пустого графа, но алгоритм всегда строит бинарное дерево.\n\nОн берет первое доступное ребро `1-2` и строит два поддерева:\n\n* В первое попадают те маршруты, где ребро `1-2` есть\n* Во второе — те, где его нет\n\nПосмотрим на узел, соответствующий узлу `1-2`. Как мы помним, из него выходят два ребра, чтобы продолжить маршрут:\n\n* Ребро `2-3`\n* Ребро `2-4`\n\nВыбираем первый вариант и снова строим два поддерева — с ребром `2-3` и без него. Вершина дерева решений будет такой:\n\n\n\n### Нижняя граница\n\nПостроив очередное поддерево, попытаемся оценить нижнюю границу длины всех маршрутов в этом поддереве.\n\nЭто не так просто. Если бы поддерево было построено, мы могли мы найти самый короткий маршрут, сравнив длины всех маршрутов. Но сейчас у нас есть только корень поддерева.\n\nНижняя граница должна быть совершенно корректна — в поддереве не должно быть маршрутов короче ее. С другой стороны, мы не можем просто использовать ноль, мы хотим оценить нижнюю границу как можно точнее. Таким образом метод ветвей и границ не будет тратить время на заведомо плохие поддеревья.\n\nМы вернемся к вопросу о том, как оценивать нижнюю границу, разбирая алгоритм Литтла в следующем уроке. Пока же будем считать, что способ оценки нам известен и он достаточно точен.\n\n### Отсечение ветвей\n\nВстретив поддерево с большой нижней границей, мы можем отказаться от его обработки, потому что все решения в нем будут слишком длинными. Иными словами, мы можем **отсечь эту ветвь дерева решений**.\n\nНа этом шаге можно случайно отбросить ветвь, которая только выглядит плохой, а на самом деле содержит кратчайший маршрут.\n\nЧтобы этого не допустить, нужно все тщательно проверить. Есть несколько способов убедиться, что в поддереве нет кратчайшего маршрута:\n\n* **Сравнить верхние и нижние границы**. Представим, что в первом поддереве верхняя граница меньше, чем нижняя граница во втором. В таком случае все маршруты второго поддерева длиннее, чем маршруты первого. Это значит, что второе поддерево можно отсечь, потому что самого короткого маршрута в нем точно нет\n* **Построить дерево решений вглубь и добраться до первого завершенного маршрута***. Этот маршрут — лучший из тех, что нам известны. В оригинальном описании алгоритма его называют **рекордным**. Скорее всего, это не самый короткий маршрут. По мере работы алгоритм может найти более короткий путь — тогда он будет рекордным. Если нижняя граница поддерева больше, чем длина рекордного маршрута, это поддерево также можно отсекать\n\nВ алгоритме Литтла применяется второй подход. В следующем уроке мы разберемся с тем, как строить рекордные маршруты.\n\n### Рекурсия\n\nПосле отсечения у нас остаются несколько ветвей, где может находиться оптимальное решение. Мы рекурсивно продолжаем работу с самой перспективной ветвью — это ветвь с наименьшей нижней границей. Для хранения не отсеченных ветвей удобно использовать структуру данных, которая называется **очередь с приоритетом**. Значения в такую очередь добавляются в произвольном порядке, а извлекаются в порядке возрастания или убывания приоритета.\n\nРекурсия завершается, когда на очередном шаге мы добираемся до листа в дереве решений.\n\nНа рисунке ниже вы увидите пример готового дерева. Мы обрезаем две ветви и добираемся до оптимального решения, в данном случае — до маршрута `1-2-4-3`:\n\n\n\nВ следующем уроке мы продолжим изучение метода ветвей и границ и познакомимся с алгоритмом Литтла — адаптацией метода для решения задачи коммивояжера.\n\n## Выводы\n\nВ этом уроке мы познакомились с задачей коммивояжера и двумя методами ее решения. Повторим основные выводы:\n\n* Одна из самых частых задач, которая встречается в логистике — задача коммивояжера\n* Задача коммивояжера легко решается методом перебора, но алгоритмическая сложность такого решения слишком высокая — `O(n!)`\n* Чтобы снизить алгоритмическую сложность, можно использовать метод ветвей и границ\n* Метод ветвей и границ — общий метод, который применяется для решения разных задач на графах\n* В этом уроке мы изучали его версию, адаптированную для задачи коммивояжера. В этой версии есть способ вычисления нижней оценки для всех решений в поддереве\n* Одна из известных адаптация метода ветвей и границ — алгоритм Литтла. Детально с этим алгоритмом мы познакомимся в следующем уроке\n"},"lessonMember":null,"courseMember":null,"course":{"start_lesson":{"exercise":null,"units":[{"id":8213,"name":"theory","url":"/courses/algorithms-graphs/lessons/intro/theory_unit"}],"links":[],"ordered_units":[{"id":8213,"name":"theory","url":"/courses/algorithms-graphs/lessons/intro/theory_unit"}],"id":3653,"slug":"intro","state":"approved","name":"Введение","course_order":100,"goal":"Знакомимся с темой курса","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"Сначала графы использовались только в математике — с их помощью решались задачи, связанные с картами. Со временем графы пришли и в программирование, потому что они подходят для решения широкого круга задач:\n\n* Составление расписаний\n* Заполнение грузовых контейнеров\n* Работа с компьютерной графикой\n* Поиск дешевых авиабилетов\n* Построение маршрутов на реальных картах и в компьютерных играх\n\nВсе эти непохожие задачи решаются одним и тем же способом — с помощью алгоритмов на графах.\n\nВ этом курсе мы изучим с самыми важными алгоритмами на графах, а пока познакомимся с самым главным термином этого курса.\n\n## Что такое графы\n\nГрафы — это важное, но случайное изобретение. Математик Леонард Эйлер придумал их, когда решал задачу о кенигсбергских мостах.\n\nПо условию этой задачи в городе Кенигсберге было семь мостов. Его жители пытались составить маршрут, который позволил бы обойти все мосты, не проходя ни по одному мосту дважды. Схематически карта города выглядит так:\n\n\n\nЭйлер поставил на карте четыре точки и соединил их дугами:\n\n\n\nКарту города можно убрать совсем и увидеть рисунок, с которым работал Эйлер:\n\n\n\nЗначительно упростив представление задачи, Эйлер доказал, что нужного маршрута не существует. Более того, он вывел общее правило, которое помогает определить, можно ли построить подобный маршрут на произвольной карте.\n\nТак Эйлер изобрел метод, который позволяет сводить сложные задачи к наглядным рисункам. Фигуры, подобные рисунку Эйлера, стали называть **графами***. Точки называются **вершинами графа***, а соединяющие их дуги — **ребрами**.\n\nНазвание «граф» происходит от греческого слова _графо_ — «писать» или «рисовать». Слова «картография» и «фотография» произошли из этого же корня.\n\nРазглядывая рисунок, Эйлер понял такую закономерность:\n\n* Если из вершины выходит четное количество ребер, то ее можно «пройти», побывав на каждом ребре ровно один раз\n* Если же ребер нечетное число, то между собой можно связать только две нечетных вершины\n\nНа графе эта закономерность выглядит так:\n\n\n\nТак Эйлер сформулировал правило:\n\n> Можно посетить все вершины графа, пройдя по каждому ребру ровно один раз, но только в том случае, если у графа только две нечетные вершины или вообще нет нечетных вершин\n\nПо этому правилу получается, что кенигсбергские мосты обойти нельзя, потому что они образуют граф с четырьмя нечетными вершинами. Эйлер не просто решил конкретную головоломку, но и сформулировал общую теорему, которую можно приложить к самым разным задачам.\n\n## Другие задачи на графах\n\nОдна из распространенных задач с графами — это выдача купюр.\n\nОбычно банкоматы стараются выдать сумму наименьшим числом банкнот. Если вы попросили 5000 рублей, банкомат выдаст 5000 рублей одной купюрой. Если такой купюры нет, то банкомат выдаст 2000, 2000 и 1000 рублей. Существует быстрый алгоритм, который всегда оптимально решает эту задачу.\n\nТо же самое работает и в логистике. Например, при перевозке контейнеров их заполняют ящиками стандартных размеров. Ящики нужно разместить как можно плотнее, чтобы обойтись минимальным числом контейнеров. Графы позволяют решить и эту задачу.\n\nЕще один пример — текстовые редакторы. Они не только опознают слова с ошибками, но и предлагают варианты замены. Очень непросто найти в словаре несколько слов, похожих на ошибочное, но графы могут разобраться и с этим.\n\nВ целом, задачи на графах встречаются в самых разных областях. Но есть одна проблема — их не всегда легко опознать.\n\nПоэтому в этом курсе мы затронем задачи, которые на первый взгляд вообще не имеют отношения к графам.\n\nНапример, к таким задачам относятся:\n\n* Задача коммивояжера\n* Задача о рюкзаке\n\nОни заметно отличаются по формулировке, но при этом решаются с помощью одних и тех же алгоритмов.\n\n## Точность и производительность\n\nКстати, графы — та область программирования, где часто применяются приближенные решения вместо точных. Иногда эти алгоритмы с приближенными решениями называются **эвристическими**.\n\nДело в том, что точные алгоритмы на графах работают настолько медленно, что их нельзя применять на практике даже для очень небольших наборов данных.\n\nВ теории алгоритмов есть **классы сложности**, с которым мы познакомимся в конце курса.\n\nМы узнаем, что такое **задачи класса NP** и почему они считаются сложным. Также мы познакомимся с несколькими приближенными алгоритмами и решим с их помощью несколько разных задач.\n\n## Выводы\n\nПовторим ключевые выводы курса:\n\n* Задачи на графах часто решаются графически — в таких случаях очевидно, что задачу надо решать через графы\n* Иногда с первого взгляда не понятно, что задача решается с помощью графов. В этом курсе мы научимся распознавать такие задачи\n* Точные алгоритмы на графах работают очень медленно\n* Чтобы быстро решать задачи на графах, программисты прибегают к эвристическим алгоритмам\n* В курсе мы разберем несколько эвристических алгоритмов\n"},"id":289,"slug":"algorithms-graphs","challenges_count":0,"name":"Алгоритмы на графах","allow_indexing":true,"state":"approved","course_state":"finished","pricing_type":"paid","description":"В этом курсе мы познакомимся с базовыми понятиями из теории графов: NP-полные задачи, поиск пути, жадные алгоритмы. Вы узнаете, решается ли задача коммивояжера, научитесь строить расписание, находить кратчайший путь.","kind":"additional","updated_at":"2026-02-12T14:03:36.118Z","language":"javascript","duration_cache":38220,"skills":["Определять NP-полные задачи и находить приближенное решение","Строить эффективные алгоритмы при работе с графами","Поиску кратчайшего пути","Различию циклических и ациклических графов","Строить граф зависимостей"],"keywords":[],"lessons_count":12,"cover":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQyODMsInB1ciI6ImJsb2JfaWQifX0=--f0d34a91cff91cafe6f82811a94fb39b4d710dfb/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJwbmciLCJyZXNpemVfdG9fZmlsbCI6WzYwMCw0MDBdfSwicHVyIjoidmFyaWF0aW9uIn19--6067466c2912ca31a17eddee04b8cf2a38c6ad17/image.png"},"recommendedLandings":[{"stack":{"id":34,"slug":"algorithms","title":"Алгоритмы и структуры данных","audience":"for_programmers","start_type":"anytime","pricing_model":"subscription","priority":"medium","kind":"track","state":"published","stack_state":"finished","order":4000,"duration_in_months":2},"id":56,"slug":"algorithms","title":"Алгоритмы и структуры данных","subtitle":"Навык, который увеличит ваши шансы пройти алгоритмическое интервью в международные компании на 80%","subtitle_for_lists":"Алгоритмы для собеседований","locale":"ru","current":true,"duration_in_months_text":"2 месяца","stack_slug":"algorithms","price_text":"от 3 900 ₽","duration_text":"2 месяца","cover_list_variant":"https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAyOCwicHVyIjoiYmxvYl9pZCJ9fQ==--ae9eed98663dd1201759d042a5ba7ca790866156/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programming-bro.png"}],"lessonMemberUnit":null,"accessToLearnUnitExists":false,"accessToCourseExists":false},"url":"/courses/algorithms-graphs/lessons/traveling-salesman-problem/theory_unit","version":"0b0c6d4ebbd40fd58630a0dd89cc25544ccdf24e","encryptHistory":false,"clearHistory":false}"><style data-mantine-styles="true">:root, :host{--mantine-font-family: Arial, sans-serif;--mantine-font-family-headings: Arial, sans-serif;--mantine-heading-font-weight: normal;--mantine-radius-default: 0rem;--mantine-primary-color-filled: var(--mantine-color-indigo-filled);--mantine-primary-color-filled-hover: var(--mantine-color-indigo-filled-hover);--mantine-primary-color-light: var(--mantine-color-indigo-light);--mantine-primary-color-light-hover: var(--mantine-color-indigo-light-hover);--mantine-primary-color-light-color: var(--mantine-color-indigo-light-color);--mantine-spacing-xxl: calc(4rem * var(--mantine-scale));--mantine-font-size-xs: 12px;--mantine-font-size-sm: 14px;--mantine-font-size-md: 16px;--mantine-font-size-lg: clamp(16.0000px, calc(15.2727px + 0.2273vw), 18.0000px);--mantine-font-size-xl: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-display-3: clamp(32.0000px, calc(26.1818px + 1.8182vw), 48.0000px);--mantine-font-size-display-2: clamp(36.0000px, calc(25.8182px + 3.1818vw), 64.0000px);--mantine-font-size-display-1: clamp(40.0000px, calc(25.4545px + 4.5455vw), 80.0000px);--mantine-font-size-h1: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-font-size-h2: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-font-size-h3: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-font-size-h4: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-font-size-h5: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-font-size-h6: 1rem;--mantine-primary-color-0: var(--mantine-color-indigo-0);--mantine-primary-color-1: var(--mantine-color-indigo-1);--mantine-primary-color-2: var(--mantine-color-indigo-2);--mantine-primary-color-3: var(--mantine-color-indigo-3);--mantine-primary-color-4: var(--mantine-color-indigo-4);--mantine-primary-color-5: var(--mantine-color-indigo-5);--mantine-primary-color-6: var(--mantine-color-indigo-6);--mantine-primary-color-7: var(--mantine-color-indigo-7);--mantine-primary-color-8: var(--mantine-color-indigo-8);--mantine-primary-color-9: var(--mantine-color-indigo-9);--mantine-color-red-0: #ffeaea;--mantine-color-red-1: #fed4d4;--mantine-color-red-2: #f4a7a8;--mantine-color-red-3: #ec7878;--mantine-color-red-4: #e55050;--mantine-color-red-5: #e03131;--mantine-color-red-6: #e02829;--mantine-color-red-7: #c71a1c;--mantine-color-red-8: #b21218;--mantine-color-red-9: #9c0411;--mantine-color-violet-0: #fce9ff;--mantine-color-violet-1: #f1cfff;--mantine-color-violet-2: #e09bff;--mantine-color-violet-3: #d16fff;--mantine-color-violet-4: #be37fe;--mantine-color-violet-5: #b51afe;--mantine-color-violet-6: #b009ff;--mantine-color-violet-7: #9b00e4;--mantine-color-violet-8: #8a00cc;--mantine-color-violet-9: #7800b3;--mantine-color-indigo-0: #edecff;--mantine-color-indigo-1: #d6d5fe;--mantine-color-indigo-2: #aaa9f4;--mantine-color-indigo-3: #7b79eb;--mantine-color-indigo-4: #5451e4;--mantine-color-indigo-5: #3b37e0;--mantine-color-indigo-6: #2d2adf;--mantine-color-indigo-7: #1f1ec7;--mantine-color-indigo-8: #1819b2;--mantine-color-indigo-9: #0c149e;--mantine-color-cyan-0: #dffdff;--mantine-color-cyan-1: #caf5ff;--mantine-color-cyan-2: #99e8ff;--mantine-color-cyan-3: #64daff;--mantine-color-cyan-4: #3ccffe;--mantine-color-cyan-5: #24c8fe;--mantine-color-cyan-6: #00c2ff;--mantine-color-cyan-7: #00ade4;--mantine-color-cyan-8: #009acd;--mantine-color-cyan-9: #0085b5;--mantine-color-green-0: #e9fdec;--mantine-color-green-1: #d7f6dc;--mantine-color-green-2: #b0eab9;--mantine-color-green-3: #86df94;--mantine-color-green-4: #62d574;--mantine-color-green-5: #4ccf5f;--mantine-color-green-6: #3fcc54;--mantine-color-green-7: #2fb344;--mantine-color-green-8: #25a03b;--mantine-color-green-9: #138a2e;--mantine-color-yellow-0: #fff7e2;--mantine-color-yellow-1: #ffeecd;--mantine-color-yellow-2: #ffdc9c;--mantine-color-yellow-3: #ffc966;--mantine-color-yellow-4: #feb93a;--mantine-color-yellow-5: #feae1e;--mantine-color-yellow-6: #ffa90f;--mantine-color-yellow-8: #ca8200;--mantine-color-yellow-9: #af7000;--mantine-h1-font-size: clamp(28.0000px, calc(23.6364px + 1.3636vw), 40.0000px);--mantine-h1-font-weight: normal;--mantine-h2-font-size: clamp(24.0000px, calc(21.0909px + 0.9091vw), 32.0000px);--mantine-h2-font-weight: normal;--mantine-h3-font-size: clamp(20.0000px, calc(17.0909px + 0.9091vw), 28.0000px);--mantine-h3-font-weight: normal;--mantine-h4-font-size: clamp(16.0000px, calc(13.0909px + 0.9091vw), 24.0000px);--mantine-h4-font-weight: normal;--mantine-h5-font-size: clamp(16.0000px, calc(14.5455px + 0.4545vw), 20.0000px);--mantine-h5-font-weight: normal;--mantine-h6-font-size: 1rem;--mantine-h6-font-weight: normal;}
:root[data-mantine-color-scheme="dark"], :host([data-mantine-color-scheme="dark"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-dark-filled: var(--mantine-color-dark-5);--mantine-color-dark-filled-hover: var(--mantine-color-dark-6);--mantine-color-dark-light: rgba(105, 105, 105, 0.15);--mantine-color-dark-light-hover: rgba(105, 105, 105, 0.2);--mantine-color-dark-light-color: var(--mantine-color-dark-0);--mantine-color-dark-outline: var(--mantine-color-dark-1);--mantine-color-dark-outline-hover: rgba(184, 184, 184, 0.05);--mantine-color-gray-filled: var(--mantine-color-gray-5);--mantine-color-gray-filled-hover: var(--mantine-color-gray-6);--mantine-color-gray-light: rgba(222, 226, 230, 0.15);--mantine-color-gray-light-hover: rgba(222, 226, 230, 0.2);--mantine-color-gray-light-color: var(--mantine-color-gray-0);--mantine-color-gray-outline: var(--mantine-color-gray-1);--mantine-color-gray-outline-hover: rgba(241, 243, 245, 0.05);--mantine-color-red-filled: var(--mantine-color-red-5);--mantine-color-red-filled-hover: var(--mantine-color-red-6);--mantine-color-red-light: rgba(236, 120, 120, 0.15);--mantine-color-red-light-hover: rgba(236, 120, 120, 0.2);--mantine-color-red-light-color: var(--mantine-color-red-0);--mantine-color-red-outline: var(--mantine-color-red-1);--mantine-color-red-outline-hover: rgba(254, 212, 212, 0.05);--mantine-color-pink-filled: var(--mantine-color-pink-5);--mantine-color-pink-filled-hover: var(--mantine-color-pink-6);--mantine-color-pink-light: rgba(250, 162, 193, 0.15);--mantine-color-pink-light-hover: rgba(250, 162, 193, 0.2);--mantine-color-pink-light-color: var(--mantine-color-pink-0);--mantine-color-pink-outline: var(--mantine-color-pink-1);--mantine-color-pink-outline-hover: rgba(255, 222, 235, 0.05);--mantine-color-grape-filled: var(--mantine-color-grape-5);--mantine-color-grape-filled-hover: var(--mantine-color-grape-6);--mantine-color-grape-light: rgba(229, 153, 247, 0.15);--mantine-color-grape-light-hover: rgba(229, 153, 247, 0.2);--mantine-color-grape-light-color: var(--mantine-color-grape-0);--mantine-color-grape-outline: var(--mantine-color-grape-1);--mantine-color-grape-outline-hover: rgba(243, 217, 250, 0.05);--mantine-color-violet-filled: var(--mantine-color-violet-5);--mantine-color-violet-filled-hover: var(--mantine-color-violet-6);--mantine-color-violet-light: rgba(209, 111, 255, 0.15);--mantine-color-violet-light-hover: rgba(209, 111, 255, 0.2);--mantine-color-violet-light-color: var(--mantine-color-violet-0);--mantine-color-violet-outline: var(--mantine-color-violet-1);--mantine-color-violet-outline-hover: rgba(241, 207, 255, 0.05);--mantine-color-indigo-filled: var(--mantine-color-indigo-5);--mantine-color-indigo-filled-hover: var(--mantine-color-indigo-6);--mantine-color-indigo-light: rgba(123, 121, 235, 0.15);--mantine-color-indigo-light-hover: rgba(123, 121, 235, 0.2);--mantine-color-indigo-light-color: var(--mantine-color-indigo-0);--mantine-color-indigo-outline: var(--mantine-color-indigo-1);--mantine-color-indigo-outline-hover: rgba(214, 213, 254, 0.05);--mantine-color-blue-filled: var(--mantine-color-blue-5);--mantine-color-blue-filled-hover: var(--mantine-color-blue-6);--mantine-color-blue-light: rgba(116, 192, 252, 0.15);--mantine-color-blue-light-hover: rgba(116, 192, 252, 0.2);--mantine-color-blue-light-color: var(--mantine-color-blue-0);--mantine-color-blue-outline: var(--mantine-color-blue-1);--mantine-color-blue-outline-hover: rgba(208, 235, 255, 0.05);--mantine-color-cyan-filled: var(--mantine-color-cyan-5);--mantine-color-cyan-filled-hover: var(--mantine-color-cyan-6);--mantine-color-cyan-light: rgba(100, 218, 255, 0.15);--mantine-color-cyan-light-hover: rgba(100, 218, 255, 0.2);--mantine-color-cyan-light-color: var(--mantine-color-cyan-0);--mantine-color-cyan-outline: var(--mantine-color-cyan-1);--mantine-color-cyan-outline-hover: rgba(202, 245, 255, 0.05);--mantine-color-teal-filled: var(--mantine-color-teal-5);--mantine-color-teal-filled-hover: var(--mantine-color-teal-6);--mantine-color-teal-light: rgba(99, 230, 190, 0.15);--mantine-color-teal-light-hover: rgba(99, 230, 190, 0.2);--mantine-color-teal-light-color: var(--mantine-color-teal-0);--mantine-color-teal-outline: var(--mantine-color-teal-1);--mantine-color-teal-outline-hover: rgba(195, 250, 232, 0.05);--mantine-color-green-filled: var(--mantine-color-green-5);--mantine-color-green-filled-hover: var(--mantine-color-green-6);--mantine-color-green-light: rgba(134, 223, 148, 0.15);--mantine-color-green-light-hover: rgba(134, 223, 148, 0.2);--mantine-color-green-light-color: var(--mantine-color-green-0);--mantine-color-green-outline: var(--mantine-color-green-1);--mantine-color-green-outline-hover: rgba(215, 246, 220, 0.05);--mantine-color-lime-filled: var(--mantine-color-lime-5);--mantine-color-lime-filled-hover: var(--mantine-color-lime-6);--mantine-color-lime-light: rgba(192, 235, 117, 0.15);--mantine-color-lime-light-hover: rgba(192, 235, 117, 0.2);--mantine-color-lime-light-color: var(--mantine-color-lime-0);--mantine-color-lime-outline: var(--mantine-color-lime-1);--mantine-color-lime-outline-hover: rgba(233, 250, 200, 0.05);--mantine-color-yellow-filled: var(--mantine-color-yellow-5);--mantine-color-yellow-filled-hover: var(--mantine-color-yellow-6);--mantine-color-yellow-light: rgba(255, 201, 102, 0.15);--mantine-color-yellow-light-hover: rgba(255, 201, 102, 0.2);--mantine-color-yellow-light-color: var(--mantine-color-yellow-0);--mantine-color-yellow-outline: var(--mantine-color-yellow-1);--mantine-color-yellow-outline-hover: rgba(255, 238, 205, 0.05);--mantine-color-orange-filled: var(--mantine-color-orange-5);--mantine-color-orange-filled-hover: var(--mantine-color-orange-6);--mantine-color-orange-light: rgba(255, 192, 120, 0.15);--mantine-color-orange-light-hover: rgba(255, 192, 120, 0.2);--mantine-color-orange-light-color: var(--mantine-color-orange-0);--mantine-color-orange-outline: var(--mantine-color-orange-1);--mantine-color-orange-outline-hover: rgba(255, 232, 204, 0.05);--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-9) 0%, var(--mantine-color-cyan-7) 100%);--app-color-surface: #2e2e2e;}
:root[data-mantine-color-scheme="light"], :host([data-mantine-color-scheme="light"]){--mantine-color-anchor: var(--mantine-color-text);--mantine-color-dimmed: #495057;--mantine-color-red-light: rgba(224, 40, 41, 0.1);--mantine-color-red-light-hover: rgba(224, 40, 41, 0.12);--mantine-color-red-outline-hover: rgba(224, 40, 41, 0.05);--mantine-color-violet-light: rgba(176, 9, 255, 0.1);--mantine-color-violet-light-hover: rgba(176, 9, 255, 0.12);--mantine-color-violet-outline-hover: rgba(176, 9, 255, 0.05);--mantine-color-indigo-light: rgba(45, 42, 223, 0.1);--mantine-color-indigo-light-hover: rgba(45, 42, 223, 0.12);--mantine-color-indigo-outline-hover: rgba(45, 42, 223, 0.05);--mantine-color-cyan-light: rgba(0, 194, 255, 0.1);--mantine-color-cyan-light-hover: rgba(0, 194, 255, 0.12);--mantine-color-cyan-outline-hover: rgba(0, 194, 255, 0.05);--mantine-color-green-light: rgba(63, 204, 84, 0.1);--mantine-color-green-light-hover: rgba(63, 204, 84, 0.12);--mantine-color-green-outline-hover: rgba(63, 204, 84, 0.05);--mantine-color-yellow-light: rgba(255, 169, 15, 0.1);--mantine-color-yellow-light-hover: rgba(255, 169, 15, 0.12);--mantine-color-yellow-outline-hover: rgba(255, 169, 15, 0.05);--app-color-surface: #f1f3f5;--app-cta-gradient: linear-gradient(90deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-5) 100%);}</style><style data-mantine-styles="classes">@media (max-width: 35.99375em) {.mantine-visible-from-xs {display: none !important;}}@media (min-width: 36em) {.mantine-hidden-from-xs {display: none !important;}}@media (max-width: 47.99375em) {.mantine-visible-from-sm {display: none !important;}}@media (min-width: 48em) {.mantine-hidden-from-sm {display: none !important;}}@media (max-width: 61.99375em) {.mantine-visible-from-md {display: none !important;}}@media (min-width: 62em) {.mantine-hidden-from-md {display: none !important;}}@media (max-width: 74.99375em) {.mantine-visible-from-lg {display: none !important;}}@media (min-width: 75em) {.mantine-hidden-from-lg {display: none !important;}}@media (max-width: 87.99375em) {.mantine-visible-from-xl {display: none !important;}}@media (min-width: 88em) {.mantine-hidden-from-xl {display: none !important;}}</style><div style="position:absolute;top:0rem" class=""></div><div style="max-width:var(--container-size-xl);height:100%;min-height:0rem" class=""><style data-mantine-styles="inline">.__m__-_R_5ub_{--grid-gutter:0rem;}</style><div style="height:100%;min-height:0rem" class="m_410352e9 mantine-Grid-root __m__-_R_5ub_"><div class="m_dee7bd2f mantine-Grid-inner" style="height:100%"><style data-mantine-styles="inline">.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:91.66666666666667%;--col-max-width:91.66666666666667%;}@media(min-width: 48em){.__m__-_R_rdub_{--col-flex-grow:auto;--col-flex-basis:83.33333333333334%;--col-max-width:83.33333333333334%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem;display:flex" class="m_96bdd299 mantine-Grid-col __m__-_R_rdub_"><style data-mantine-styles="inline">.__m__-_R_6qrdub_{margin-top:0rem;padding-inline:var(--mantine-spacing-xs);width:100%;}@media(min-width: 48em){.__m__-_R_6qrdub_{margin-top:var(--mantine-spacing-xl);width:80%;}}@media(min-width: 62em){.__m__-_R_6qrdub_{padding-inline:var(--mantine-spacing-xl);}}</style><div style="margin-inline:auto;max-width:var(--mantine-breakpoint-xl)" class="__m__-_R_6qrdub_"><div style="color:var(--mantine-color-dimmed)" class="m_4451eb3a mantine-Center-root" data-inline="true"><div style="--ti-size:var(--ti-size-xs);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;margin-inline-end:calc(0.125rem * var(--mantine-scale));color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="xs"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-lock "><path d="M5 13a2 2 0 0 1 2 -2h10a2 2 0 0 1 2 2v6a2 2 0 0 1 -2 2h-10a2 2 0 0 1 -2 -2v-6"></path><path d="M11 16a1 1 0 1 0 2 0a1 1 0 0 0 -2 0"></path><path d="M8 11v-4a4 4 0 1 1 8 0v4"></path></svg></div><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Алгоритмы на графах</p></div><h1 style="--title-fw:var(--mantine-h1-font-weight);--title-lh:var(--mantine-h1-line-height);--title-fz:var(--mantine-h1-font-size);margin-bottom:var(--mantine-spacing-xl)" class="m_8a5d1357 mantine-Title-root" data-order="1">Теория: Задача коммивояжера</h1><script type="application/ld+json">{"@context":"https://schema.org","@type":"LearningResource","name":"Задача коммивояжера","inLanguage":"ru","isPartOf":{"@type":"LearningResource","name":"Алгоритмы на графах"},"isAccessibleForFree":"False","hasPart":{"@type":"WebPageElement","isAccessibleForFree":"False","cssSelector":".paywalled"}}</script><div class=""><div style="--alert-color:var(--mantine-color-indigo-light-color);margin-bottom:var(--mantine-spacing-lg);font-size:var(--mantine-font-size-lg)" class="m_66836ed3 mantine-Alert-root" id="mantine-_R_remqrdub_" role="alert" aria-describedby="mantine-_R_remqrdub_-body" aria-labelledby="mantine-_R_remqrdub_-title"><div class="m_a5d60502 mantine-Alert-wrapper"><div class="m_667f2a6a mantine-Alert-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-rocket "><path d="M4 13a8 8 0 0 1 7 7a6 6 0 0 0 3 -5a9 9 0 0 0 6 -8a3 3 0 0 0 -3 -3a9 9 0 0 0 -8 6a6 6 0 0 0 -5 3"></path><path d="M7 14a6 6 0 0 0 -3 6a6 6 0 0 0 6 -3"></path><path d="M14 9a1 1 0 1 0 2 0a1 1 0 1 0 -2 0"></path></svg></div><div class="m_667c2793 mantine-Alert-body"><div class="m_6a03f287 mantine-Alert-title"><span id="mantine-_R_remqrdub_-title" class="m_698f4f23 mantine-Alert-label">Полный доступ к материалам</span></div><div id="mantine-_R_remqrdub_-body" class="m_7fa78076 mantine-Alert-message"><div style="--group-gap:var(--mantine-spacing-md);--group-align:center;--group-justify:space-between;--group-wrap:wrap" class="m_4081bf90 mantine-Group-root"><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Зарегистрируйтесь и получите доступ к этому и десяткам других курсов</p><a style="--button-height:var(--button-height-xs);--button-padding-x:var(--button-padding-x-xs);--button-fz:var(--mantine-font-size-xs);--button-bg:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-hover:linear-gradient(45deg, var(--mantine-color-blue-filled) 0%, var(--mantine-color-cyan-filled) 100%);--button-color:var(--mantine-color-white);--button-bd:none" class="mantine-focus-auto mantine-active m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root" data-variant="gradient" data-size="xs" href="/u/new"><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">Зарегистрироваться</span></span></a></div></div></div></div></div><div class="paywalled m_d08caa0 mantine-Typography-root"><p>Задача коммивояжера — это одна из самых известных задач на графах. По-английски ее называют TSP (<em>Traveling Salesman Problem</em> — задача странствующего торговца).</p>
<p>Представьте, что вы торговый представитель, который хочет объехать несколько ближайших городов.</p>
<p>Схематично маршруты между городами можно обозначить так:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzODgsInB1ciI6ImJsb2JfaWQifX0=--0c951d87fb0792a70e06afe770f075eb9dc6f351/01.png" alt="01" loading="lazy"/></p>
<p>На этой схеме города — это вершины графа, а дороги — его ребрами. У каждого ребра есть число, которое обозначает длину маршрута. При работе с графами это число называют <strong>весом ребра</strong>*, а сам граф — <strong>взвешенным</strong>.</p>
<p>Вы хотите сэкономить, поэтому ищете кратчайший маршрут и хотите посетить каждый город только один раз.</p>
<p>У этой задачи есть несколько разных решений — изучим первый способ.</p>
<h2 id="heading-2-1">Метод перебора</h2>
<p>Для начала рассмотрим решение «в лоб» — перебором всех возможных вариантов. Чтобы решить задачу перебором, нужно:</p>
<ul>
<li>Построить все возможные маршруты</li>
<li>Сложить вес всех ребер в каждом маршруте</li>
<li>Найти путь с минимальной суммой</li>
</ul>
<p>Посмотрим, как это работает на практике. Для примера возьмем маршрут по городам <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">ABCFED</code>:</p>
<ul>
<li>Из города <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">A</code> едем в город <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">B</code> — между ними 44 километра</li>
<li>Едем из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">B</code> в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">C</code> — 45 километров</li>
<li>Едем из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">C</code> в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">F</code> — 15 километров</li>
<li>Едем из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">F</code> в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">E</code> — 35 километров</li>
<li>Едем из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">E</code> в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">D</code> — 17 километров</li>
<li>Чтобы замкнуть маршрут, едем из <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">D</code> в <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">A</code> — 32 километра</li>
</ul>
<p>В итоге весь маршрут составит 188 километров.</p>
<p>Затем мы строим другой маршрут, попутно вычисляя его суммарный вес. Если новый маршрут короче текущего, запоминаем его — теперь мы считаем его лучшим найденным решением.</p>
<p>Когда алгоритм переберет все варианты, у нас останется лучшее решение из всех маршрутов.</p>
<p>Обратите внимание, что решения может и не быть. Для примера посмотрим на такую схему:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzODksInB1ciI6ImJsb2JfaWQifX0=--13060ab2a506afbcbf1d5d89ed5460b91a455297/02.png" alt="02" loading="lazy"/></p>
<p>Граф на этом рисунке невозможно обойти, так чтобы мы начали из точки <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">A</code> и побывали в каждой вершине всего по одному разу.</p>
<p>Для такого графа метод перебора должен вернуть что-то, обозначающее отсутствие решения — значение <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">null</code> или пустой массив.</p>
<h3 id="heading-3-2">Реализация метода перебора</h3>
<p>Граф для задачи коммивояжера можно хранить любым удобным способом. Остановимся на матрице смежности, которую изучали ранее в курсе.</p>
<p>Если вершины связаны ребром, то мы поместим в матрицу его вес, если нет — добавим значение <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">null</code>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class Graph {
vertices
size
edges
constructor(vertices) {
this.vertices = vertices
this.size = vertices.length
this.edges = Array.from({ length: this.size }, () => {
return Array(this.size).fill(null)
})
}
addEdge(value1, value2, weight) {
const row = this.vertices.indexOf(value1)
const column = this.vertices.indexOf(value2)
this.edges[row][column] = weight
this.edges[column][row] = weight
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Создадим класс <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Route</code>, который упрощает построение маршрута. Он хранит суммарную длину пути и список вершин, который алгоритм успел обойти:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">class Route {
vertices
weight
constructor(vertices, weight) {
this.vertices = vertices
this.weight = weight
}
add(vertex, incWeight) {
let nextVertices = [...this.vertices]
nextVertices.push(vertex)
const nextWeight = this.weight + incWeight
return new Route(nextVertices, nextWeight)
}
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Метод <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">add()</code> добавляет к маршруту новую вершину и вес ребра, которое ведет к этой вершине.</p>
<p>Используем класс <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">Route</code> при реализации метода <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">tsp()</code>. Напоминаем, что TSP — это <em>Travelling Salesman Problem</em>:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">tsp() {
let seen = new Set();
let min = null;
const bruteforce = (i, current) => {
seen.add(i);
if (seen.size === this.size) {
const weight = this.edges[i][0];
if (weight !== null) {
const route = current.add(this.vertices[0], weight);
if (min === null || min.weight > route.weight) {
min = route;
}
}
} else {
for (let j = 0; j < this.edges[i].length; j += 1) {
const weight = this.edges[i][j];
if (weight !== null && !seen.has(j)) {
const route = current.add(this.vertices[j], weight);
bruteforce(j, route);
}
}
}
seen.delete(i);
};
bruteforce(0, new Route([this.vertices[0]], 0));
return min;
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Внутренняя рекурсивная функция <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">bruteforce()</code> получает в качестве параметров:</p>
<ul>
<li>Индекс следующей вершины</li>
<li>Уже построенный маршрут</li>
</ul>
<p>Эта функция помечает новую вершину как посещенную и проверяет, сколько всего помеченных вершин. Если их количество равно общему количеству вершин, мы построили весь маршрут — то есть обошли все вершины. Это конец рекурсии.</p>
<p>Далее нам надо выбрать минимальный маршрут, его мы храним в переменной <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">min</code>. Она обновляется, как только мы находим новый маршрут с меньшей длительностью:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">if (seen.size === this.size) {
const weight = this.edges[i][0];
if (weight !== null) {
const route = current.add(this.vertices[0], weight);
if (min === null || min.weight > route.weight) {
min = route;
}</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Если мы не посмотрели все вершины, то находимся в середине построения маршрута. Отбираем из матрицы все вершины, до которых можно добраться (значение в матрице <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">this.edges</code> не равно <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">null</code>).</p>
<p>Рекурсивно вызываем <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">bruteforce()</code> для каждой подходящей вершины:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">for (let j = 0; j < this.edges[i].length; j++) {
const weight = this.edges[i][j];
if (weight !== null && !seen.has(j)) {
const route = current.add(this.vertices[j], weight);
bruteforce(j, route);</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Алгоритм всегда начинает построение маршрута с первой вершины. Проверим его на графе-пятиугольнике, где все вершины соединены со всеми:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTAsInB1ciI6ImJsb2JfaWQifX0=--a6823a8c29b75627328a396f33acd1bed3aa69c0/03.png" alt="300" loading="lazy"/></p>
<p>Самым коротким маршрутом будет обход этого графа по сторонам. Если длина каждой стороны равна 100, полная длина маршрута составит 500. Длина лучей звезды на такой картинке равна 162. Если алгоритм свернет на один из лучей, длина маршрута окажется больше:</p>
<div style="margin-bottom:var(--mantine-spacing-lg)" class="m_e597c321 mantine-CodeHighlight-codeHighlight" dir="ltr"><div class="m_be7e9c9c mantine-CodeHighlight-controls"><button style="--ai-bg:transparent;--ai-hover:transparent;--ai-color:inherit;--ai-bd:none" class="mantine-focus-auto mantine-active m_d498bab7 mantine-CodeHighlight-control m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="none" type="button" aria-label="Copy code"><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"></path><path d="M8 8m0 2a2 2 0 0 1 2 -2h8a2 2 0 0 1 2 2v8a2 2 0 0 1 -2 2h-8a2 2 0 0 1 -2 -2z"></path><path d="M16 8v-2a2 2 0 0 0 -2 -2h-8a2 2 0 0 0 -2 2v8a2 2 0 0 0 2 2h2"></path></svg></span></button></div><div style="--scrollarea-scrollbar-size:calc(0.25rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_f744fd40 mantine-CodeHighlight-scrollarea m_d57069b5 mantine-ScrollArea-root" dir="ltr"><div style="overflow-x:hidden;overflow-y:hidden;overscroll-behavior-inline:none" class="m_c0783ff9 mantine-ScrollArea-viewport" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><pre class="m_2c47c4fd mantine-CodeHighlight-pre" style="padding:0"><code class="m_5caae6d3 mantine-CodeHighlight-code">const graph = new Graph(['A', 'B', 'C', 'D', 'E'])
graph.addEdge('A', 'B', 100)
graph.addEdge('A', 'C', 162)
graph.addEdge('A', 'D', 162)
graph.addEdge('A', 'E', 100)
graph.addEdge('B', 'C', 100)
graph.addEdge('B', 'D', 162)
graph.addEdge('B', 'E', 162)
graph.addEdge('C', 'D', 100)
graph.addEdge('C', 'E', 162)
graph.addEdge('D', 'E', 100)
graph.tsp()
// Route {
// vertices: [ 'A', 'B', 'C', 'D', 'E', 'A' ],
// weight: 500
// }</code></pre></div></div></div><button class="mantine-focus-auto m_c9378bc2 mantine-CodeHighlight-showCodeButton m_87cf2631 mantine-UnstyledButton-root" data-hidden="true" type="button">Expand code</button></div>
<p>Здесь мы видим, что длина маршрута равна 500 — значит, мы нашли кратчайший маршрут через все вершины.</p>
<h2 id="heading-2-3">Оценка сложности</h2>
<p>Представим, что у нас есть четыре попарно связанные вершины, как показано на рисунке:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTEsInB1ciI6ImJsb2JfaWQifX0=--46064c0f0a67c49a8460425a3ab8ba5c382a6715/04.png" alt="300" loading="lazy"/></p>
<p>Сколько существует способов обойти весь граф, если мы всегда начинаем с первой вершины? Посмотрим на этот рисунок:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTIsInB1ciI6ImJsb2JfaWQifX0=--97b6c28d609c4b5c9a825f2a5b746378a44aabaa/05.png" alt="05" loading="lazy"/></p>
<p>Здесь мы видим, что таких способов всего шесть. Они образуют три пары — в каждой паре один и тот же маршрут. Маршруты в парах отличаются только направлением обхода.</p>
<p>Выпишем все маршруты в таблицу:</p>
<div style="--table-min-width:calc(50rem * var(--mantine-scale));--sa-corner-width:0px;--sa-corner-height:0px" class="m_a100c15 mantine-TableScrollContainer-scrollContainer m_d57069b5 mantine-ScrollArea-root"><div style="overflow-x:hidden;overflow-y:hidden" class="m_c0783ff9 mantine-ScrollArea-viewport" data-offset-scrollbars="x" data-scrollbars="xy"><div class="m_b1336c6 mantine-ScrollArea-content"><div class="m_62259741 mantine-TableScrollContainer-scrollContainerInner"><table><thead><tr><th>В одну сторону</th><th>В другую сторону</th></tr></thead><tbody><tr><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 2, 3, 4, 1</code></td><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 4, 3, 2, 1</code></td></tr><tr><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 2, 4, 3, 1</code></td><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 3, 4, 2, 1</code></td></tr><tr><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 3, 2, 4, 1</code></td><td><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1, 4, 2, 3, 1</code></td></tr></tbody></table></div></div></div></div>
<p>Как видите, первая и последняя точка никогда не меняется — это всегда <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1</code>. Середина маршрута — различные перестановки вершин <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2</code>, <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">3</code> и <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">4</code>.</p>
<p>В этом случае у нас три вершины, поэтому количество перестановок вычисляется так:</p>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">3!=1*2*3=6</code></p>
<p>Именно поэтому у нас шесть маршрутов.</p>
<p>Первая вершина всегда фиксирована и не участвует в перестановках, поэтому общее количество маршрутов для <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">n</code> вершин равно:</p>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">(n-1)!</code></p>
<p>Если из каждой симметричной пары брать только один маршрут, то маршрутов станет вполовину меньше:</p>
<p><code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">((n-1)!)/2</code></p>
<p>В обоих случаях в нотации <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">O</code>-большое алгоритмическая сложность при решении задачи коммивояжера методом перебора оценивается как <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">O(n!)</code>.</p>
<h2 id="heading-2-4">Метод ветвей и границ</h2>
<p>Алгоритмы с факториальной сложностью <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">O(n!)</code> работают даже медленнее алгоритмов с экспоненциальной сложностью <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">O(2^n)</code>. Для пяти вершин алгоритм перебрал всего <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">4!=24</code> маршрута. Но чем больше вершин, тем сложнее:</p>
<ul>
<li>Для 10 городов потребуется сравнить больше <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">300 000</code> маршрутов</li>
<li>Для 21 города количество вариантов возрастает до миллиарда миллиардов. Если тысяча мощных компьютеров будет перебирать миллион вариантов в секунду, то для решения задачи потребуется чуть больше тридцати лет</li>
</ul>
<p>Ранее в курсе мы узнали, что есть способ найти более быстрое решение — жадный алгоритм. К сожалению, он часто оказывается неоптимальным. Обсудим его подробнее.</p>
<p>Вместо перебора, мы можем воспользоваться другим способом — <strong>методом ветвей и границ</strong>. Перебор выбирает одно единственное решение, которое кажется лучшим. В отличие от него, метод ветвей и границ концентрируется на том, чтобы отбрасывать заведомо плохие варианты.</p>
<p>Как и перебор, метод ветвей и границ гарантирует нахождение лучшего решения, но при этом он может найти его за приемлемое время. Но есть потенциальная проблема — в худшем случае он вырождается в метод перебора и может работать слишком долго.</p>
<h3 id="heading-3-5">Как работает метод ветвей и границ</h3>
<p>Метод ветвей и границ довольно сложный. Он позволяет решать разные задачи на графах — задачу о рюкзаке, о коммивояжере и другие. Именно поэтому его описание достаточно абстрактное.</p>
<p>Кроме того, сам метод состоит из нескольких шагов. ИХ детальное описание может отличаться от задачи к задаче.</p>
<p>Чтобы упростить изучение метода, мы освоим его в два этапа. Сейчас рассмотрим метод в целом и применительно к задаче коммивояжера, а уже в следующем уроке — познакомимся с алгоритмом Литтла.</p>
<h3 id="heading-3-6">Отличие от метода перебора</h3>
<p>И перебор, и метод ветвей и границ обходят деревья решений, но строят эти деревья по-разному. Для примера сравним деревья, которые получаются для графа из четырех вершин.</p>
<p><strong>В методе перебора</strong> мы начинаем с пустого графа — графа без построенного маршрута. На первом шаге у нас есть три возможных варианта начать маршрут:</p>
<ul>
<li>Ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code></li>
<li>Ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-3</code></li>
<li>Ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-4</code></li>
</ul>
<p>Из корня мы начинаем строить три поддерева, каждое из которых соответствует одному из ребер.</p>
<p>Будем работать с каждым поддеревом по очереди. Сначала обработаем узел, который соответствует ребру <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code>. На этом этапе алгоритм построил маршрут до вершины <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2</code>. Далее он может добавить:</p>
<ul>
<li>Либо ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2-3</code></li>
<li>Либо ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2-4</code></li>
</ul>
<p>Значит, из узла <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code> можно построить еще два поддерева. Вершина дерева решений выглядит так:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTMsInB1ciI6ImJsb2JfaWQifX0=--325ea0e925df515ffe0f8143259bd980e8cc300c/06.png" alt="06" loading="lazy"/></p>
<p><strong>В методе ветвей и границ</strong> построение дерева также начинается с пустого графа, но алгоритм всегда строит бинарное дерево.</p>
<p>Он берет первое доступное ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code> и строит два поддерева:</p>
<ul>
<li>В первое попадают те маршруты, где ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code> есть</li>
<li>Во второе — те, где его нет</li>
</ul>
<p>Посмотрим на узел, соответствующий узлу <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2</code>. Как мы помним, из него выходят два ребра, чтобы продолжить маршрут:</p>
<ul>
<li>Ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2-3</code></li>
<li>Ребро <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2-4</code></li>
</ul>
<p>Выбираем первый вариант и снова строим два поддерева — с ребром <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">2-3</code> и без него. Вершина дерева решений будет такой:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTQsInB1ciI6ImJsb2JfaWQifX0=--797f8c44c2639f8200866e026c843b419db83dd9/07.png" alt="07" loading="lazy"/></p>
<h3 id="heading-3-7">Нижняя граница</h3>
<p>Построив очередное поддерево, попытаемся оценить нижнюю границу длины всех маршрутов в этом поддереве.</p>
<p>Это не так просто. Если бы поддерево было построено, мы могли мы найти самый короткий маршрут, сравнив длины всех маршрутов. Но сейчас у нас есть только корень поддерева.</p>
<p>Нижняя граница должна быть совершенно корректна — в поддереве не должно быть маршрутов короче ее. С другой стороны, мы не можем просто использовать ноль, мы хотим оценить нижнюю границу как можно точнее. Таким образом метод ветвей и границ не будет тратить время на заведомо плохие поддеревья.</p>
<p>Мы вернемся к вопросу о том, как оценивать нижнюю границу, разбирая алгоритм Литтла в следующем уроке. Пока же будем считать, что способ оценки нам известен и он достаточно точен.</p>
<h3 id="heading-3-8">Отсечение ветвей</h3>
<p>Встретив поддерево с большой нижней границей, мы можем отказаться от его обработки, потому что все решения в нем будут слишком длинными. Иными словами, мы можем <strong>отсечь эту ветвь дерева решений</strong>.</p>
<p>На этом шаге можно случайно отбросить ветвь, которая только выглядит плохой, а на самом деле содержит кратчайший маршрут.</p>
<p>Чтобы этого не допустить, нужно все тщательно проверить. Есть несколько способов убедиться, что в поддереве нет кратчайшего маршрута:</p>
<ul>
<li><strong>Сравнить верхние и нижние границы</strong>. Представим, что в первом поддереве верхняя граница меньше, чем нижняя граница во втором. В таком случае все маршруты второго поддерева длиннее, чем маршруты первого. Это значит, что второе поддерево можно отсечь, потому что самого короткого маршрута в нем точно нет</li>
<li><strong>Построить дерево решений вглубь и добраться до первого завершенного маршрута</strong>*. Этот маршрут — лучший из тех, что нам известны. В оригинальном описании алгоритма его называют <strong>рекордным</strong>. Скорее всего, это не самый короткий маршрут. По мере работы алгоритм может найти более короткий путь — тогда он будет рекордным. Если нижняя граница поддерева больше, чем длина рекордного маршрута, это поддерево также можно отсекать</li>
</ul>
<p>В алгоритме Литтла применяется второй подход. В следующем уроке мы разберемся с тем, как строить рекордные маршруты.</p>
<h3 id="heading-3-9">Рекурсия</h3>
<p>После отсечения у нас остаются несколько ветвей, где может находиться оптимальное решение. Мы рекурсивно продолжаем работу с самой перспективной ветвью — это ветвь с наименьшей нижней границей. Для хранения не отсеченных ветвей удобно использовать структуру данных, которая называется <strong>очередь с приоритетом</strong>. Значения в такую очередь добавляются в произвольном порядке, а извлекаются в порядке возрастания или убывания приоритета.</p>
<p>Рекурсия завершается, когда на очередном шаге мы добираемся до листа в дереве решений.</p>
<p>На рисунке ниже вы увидите пример готового дерева. Мы обрезаем две ветви и добираемся до оптимального решения, в данном случае — до маршрута <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">1-2-4-3</code>:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQzOTUsInB1ciI6ImJsb2JfaWQifX0=--733e6a59332ba3e3b8bd6f3fb1295b764a70bef5/09.png" alt="09" loading="lazy"/></p>
<p>В следующем уроке мы продолжим изучение метода ветвей и границ и познакомимся с алгоритмом Литтла — адаптацией метода для решения задачи коммивояжера.</p>
<h2 id="heading-2-10">Выводы</h2>
<p>В этом уроке мы познакомились с задачей коммивояжера и двумя методами ее решения. Повторим основные выводы:</p>
<ul>
<li>Одна из самых частых задач, которая встречается в логистике — задача коммивояжера</li>
<li>Задача коммивояжера легко решается методом перебора, но алгоритмическая сложность такого решения слишком высокая — <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">O(n!)</code></li>
<li>Чтобы снизить алгоритмическую сложность, можно использовать метод ветвей и границ</li>
<li>Метод ветвей и границ — общий метод, который применяется для решения разных задач на графах</li>
<li>В этом уроке мы изучали его версию, адаптированную для задачи коммивояжера. В этой версии есть способ вычисления нижней оценки для всех решений в поддереве</li>
<li>Одна из известных адаптация метода ветвей и границ — алгоритм Литтла. Детально с этим алгоритмом мы познакомимся в следующем уроке</li>
</ul></div><div style="margin-block:var(--mantine-spacing-xl)" class=""><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md)" class="m_8a5d1357 mantine-Title-root" data-order="2">Рекомендуемые программы</h2><style data-mantine-styles="inline">.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xs);--carousel-slide-size:70%;}@media(min-width: 36em){.__m__-_R_2mremqrdub_{--carousel-slide-gap:var(--mantine-spacing-xl);--carousel-slide-size:50%;}}</style><div style="--carousel-control-size:calc(2.5rem * var(--mantine-scale));--carousel-controls-offset:var(--mantine-spacing-sm);margin-bottom:var(--mantine-spacing-lg);padding-block:var(--mantine-spacing-sm);background:var(--app-color-surface)" class="m_17884d0f mantine-Carousel-root responsiveClassName" data-orientation="horizontal" data-include-gap-in-size="true"><div class="m_39bc3463 mantine-Carousel-controls" data-orientation="horizontal"><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="previous" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button><button class="mantine-focus-auto m_64f58e10 mantine-Carousel-control m_87cf2631 mantine-UnstyledButton-root" type="button" data-inactive="true" data-type="next" tabindex="-1"><svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" style="transform:rotate(-90deg);width:calc(1rem * var(--mantine-scale));height:calc(1rem * var(--mantine-scale));display:block"><path d="M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z" fill="currentColor" fill-rule="evenodd" clip-rule="evenodd"></path></svg></button></div><div class="m_a2dae653 mantine-Carousel-viewport" data-type="media"><div class="m_fcd81474 mantine-Carousel-container __m__-_R_2mremqrdub_" data-orientation="horizontal"><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/programs/algorithms?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card" target="_blank"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><div style="--group-gap:calc(0.25rem * var(--mantine-scale));--group-align:center;--group-justify:flex-start;--group-wrap:nowrap" class="m_4081bf90 mantine-Group-root"><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">2 месяца</span><span class="mantine-focus-auto m_b6d8b162 mantine-Text-root">·</span><span style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Для продвинутых</span></div><p style="margin-bottom:var(--mantine-spacing-sm);font-size:var(--mantine-font-size-h5);font-weight:bold" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Алгоритмы и структуры данных</p><p class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Алгоритмы для собеседований</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="https://hexlet.io/rails/active_storage/representations/proxy/eyJfcmFpbHMiOnsiZGF0YSI6NDAyOCwicHVyIjoiYmxvYl9pZCJ9fQ==--ae9eed98663dd1201759d042a5ba7ca790866156/eyJfcmFpbHMiOnsiZGF0YSI6eyJmb3JtYXQiOiJ3ZWJwIiwicmVzaXplX3RvX2xpbWl0IjpbNDAwLDQwMF0sInNhdmVyIjp7InF1YWxpdHkiOjg1fX0sInB1ciI6InZhcmlhdGlvbiJ9fQ==--5b6f46dacd1af664f27558553a58076185091823/Programming-bro.png" alt="Алгоритмы и структуры данных" loading="eager"/></div><div style="--group-gap:var(--mantine-spacing-md);--group-align:end;--group-justify:space-between;--group-wrap:wrap;margin-top:var(--mantine-spacing-xs)" class="m_4081bf90 mantine-Group-root"><p style="font-size:var(--mantine-font-size-xl)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">от 3 900 ₽</p><p style="font-size:var(--mantine-font-size-sm)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Посмотреть →</p></div></div></div></a></div></div><div class="m_d98df724 mantine-Carousel-slide" data-orientation="horizontal"><div tabindex="0" style="cursor:pointer;height:100%"><a style="text-decoration:none" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses?promo_name=programs_list&promo_position=course&promo_creative=catalog_card&promo_type=card"><div style="height:100%" class="m_e615b15f mantine-Card-root m_1b7284a3 mantine-Paper-root" data-with-border="true"><h2 style="--title-fw:var(--mantine-h2-font-weight);--title-lh:var(--mantine-h2-line-height);--title-fz:var(--mantine-h2-font-size);margin-bottom:var(--mantine-spacing-md);font-size:var(--mantine-font-size-h3)" class="m_8a5d1357 mantine-Title-root" data-order="2" data-responsive="true">Каталог</h2><p style="margin-bottom:auto" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Полный список доступных курсов по разным направлениям</p><div style="margin-top:auto" class=""><div class="m_4451eb3a mantine-Center-root"><img style="opacity:0.8;width:70%" class="m_9e117634 mantine-Image-root mantine-visible-from-xs" src="/vite/assets/development-BVihs_d5.png" alt="Orientation"/></div></div></div></a></div></div></div></div></div></div></div></div></div><style data-mantine-styles="inline">.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:8.333333333333334%;--col-max-width:8.333333333333334%;}@media(min-width: 48em){.__m__-_R_1bdub_{--col-flex-grow:auto;--col-flex-basis:16.666666666666668%;--col-max-width:16.666666666666668%;}}</style><div style="min-width:0rem;height:100%;min-height:0rem" class="m_96bdd299 mantine-Grid-col __m__-_R_1bdub_"><div style="margin-inline:var(--mantine-spacing-xs)" class="mantine-visible-from-sm"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-lg);text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/algorithms-graphs/lessons/traveling-salesman-problem/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label"><span style="margin-inline-end:var(--mantine-spacing-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Дальше</span>→</span></span></a><a style="padding-inline:0rem" class="mantine-focus-auto m_f0824112 mantine-NavLink-root m_87cf2631 mantine-UnstyledButton-root"><span class="m_690090b5 mantine-NavLink-section" data-position="left"><div style="--ti-size:var(--ti-size-sm);--ti-bg:transparent;--ti-color:var(--mantine-color-indigo-light-color);--ti-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;color:inherit" class="m_7341320d mantine-ThemeIcon-root" data-variant="transparent" data-size="sm"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></div></span><div class="m_f07af9d2 mantine-NavLink-body"><span class="m_1f6ac4c4 mantine-NavLink-label">Навигация по теме</span><span class="m_57492dcc mantine-NavLink-description">Теория</span></div><span class="m_690090b5 mantine-NavLink-section" data-position="right"></span></a><div style="margin-block:var(--mantine-spacing-lg)" class="m_3eebeb36 mantine-Divider-root" data-orientation="horizontal" role="separator"></div><div style="margin-block:var(--mantine-spacing-lg)" class=""><div style="justify-content:space-between;margin-bottom:calc(0.1875rem * var(--mantine-scale));color:var(--mantine-color-dimmed);font-size:var(--mantine-font-size-xs)" class="m_8bffd616 mantine-Flex-root __m__-_R_qimrbdub_"><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">Завершено</p><p style="font-size:var(--mantine-font-size-xs)" class="mantine-focus-auto m_b6d8b162 mantine-Text-root">0 / 12</p></div><div style="--progress-size:var(--progress-size-sm)" class="m_db6d6462 mantine-Progress-root" data-size="sm"><div style="--progress-section-size:0%;--progress-section-color:var(--mantine-color-gray-filled)" class="m_2242eb65 mantine-Progress-section" role="progressbar" aria-valuemax="100" aria-valuemin="0" aria-valuenow="0" aria-valuetext="0%"></div></div></div><div style="--toc-bg:var(--mantine-color-blue-light);--toc-color:var(--mantine-color-blue-light-color);--toc-size:var(--mantine-font-size-sm);--toc-radius:var(--mantine-radius-sm);margin-top:var(--mantine-spacing-xl)" class="m_bcaa9990 mantine-TableOfContents-root" data-variant="light" data-size="sm"></div></div><div class="mantine-hidden-from-sm"><div style="--stack-gap:0rem;--stack-align:stretch;--stack-justify:flex-start" class="m_6d731127 mantine-Stack-root"><a style="--button-color:var(--mantine-color-white);margin-bottom:var(--mantine-spacing-xs);padding:0rem;text-decoration:none" class="mantine-focus-auto m_849cf0da mantine-focus-auto m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="/courses/algorithms-graphs/lessons/traveling-salesman-problem/finish_unit?unit=theory" data-disabled="true" data-block="true" disabled=""><span class="m_80f1301b mantine-Button-inner"><span class="m_811560b9 mantine-Button-label">→</span></span></a><button style="--ai-size:var(--ai-size-sm);--ai-bg:transparent;--ai-hover:var(--mantine-color-indigo-light-hover);--ai-color:var(--mantine-color-indigo-light-color);--ai-bd:calc(0.0625rem * var(--mantine-scale)) solid transparent;padding-block:var(--mantine-spacing-lg);color:inherit;width:100%" class="mantine-focus-auto m_8d3f4000 mantine-ActionIcon-root m_87cf2631 mantine-UnstyledButton-root" data-variant="subtle" data-size="sm" data-disabled="true" type="button" disabled=""><span class="m_8d3afb97 mantine-ActionIcon-icon"><svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-list-numbers "><path d="M11 6h9"></path><path d="M11 12h9"></path><path d="M12 18h8"></path><path d="M4 16a2 2 0 1 1 4 0c0 .591 -.5 1 -1 1.5l-3 2.5h4"></path><path d="M6 10v-6l-2 2"></path></svg></span></button></div></div></div></div></div></div></div>
</main>
<footer class="bg-dark fw-light text-light px-3 py-5">
<div class="row small">
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 mb-3">Хекслет</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/about">О нас</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/testimonials">Отзывы</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://b2b.hexlet.io" role="button">Корпоративное обучение</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/blog">Блог</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/qna">Вопросы и ответы</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/glossary">Глоссарий</a>
</li>
<li>
<span class="nav-link link-light py-1 ps-0 external-link" data-href="https://help.hexlet.io" data-target="_blank" role="button">Справка</span>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" target="_blank" rel="noopener noreferrer" href="/map">Карта сайта</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5 fw-normal mb-3">Направления</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_devops">DevOps
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_data_analytics">Аналитика
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_backend_development">Бэкенд
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_programming">Программирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_testing">Тестирование
</a></li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/courses_front_end_dev">Фронтенд
</a></li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Профессии</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/devops-engineer-from-scratch">DevOps-инженер с нуля</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/go">Go-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/java">Java-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python">Python-разработчик </a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/data-analytics">Аналитик данных</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/qa-engineer">Инженер по ручному тестированию</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php">РНР-разработчик</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/frontend">Фронтенд-разработчик</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-6 col-md-3">
<div class="h5">Навыки</div>
<ul class="list-unstyled">
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/python-django-developer">Django</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/docker">Docker</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/php-laravel-developer">Laravel</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/postman">Postman</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-react-developer">React</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/js-rest-api">REST API в Node.js</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/spring-boot">Spring Boot</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/programs/typescript">Typescript</a>
</li>
</ul>
</div>
</div>
<hr>
<div class="row">
<div class="col-12 col-sm-4 col-md-2">
<div class="fs-4">
<ul class="list-unstyled d-flex">
<li class="me-3">
<a aria-label="Telegram" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://t.me/hexlet_ru"><span class="bi bi-telegram"></span>
</a></li>
<li>
<a aria-label="Youtube" target="_blank" class="link-light" rel="noopener noreferrer nofollow" href="https://www.youtube.com/user/HexletUniversity"><span class="bi bi-youtube"></span>
</a></li>
</ul>
</div>
<div class="mb-2 d-flex flex-column">
<a class="link-light text-decoration-none" rel="nofollow" href="mailto:support@hexlet.io">support@hexlet.io</a>
<a class="link-light text-decoration-none py-2" target="_blank" href="https://t.me/hexlet_help_bot">t.me/hexlet_help_bot</a>
</div>
<ul class="list-unstyled d-flex">
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://hexlet.io/locale/switch?new_locale=en" data-target="_self" role="button"><span class="my-auto">EN</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 opacity-100 external-link" rel="nofollow" data-href="https://ru.hexlet.io/locale/switch?new_locale=ru" data-target="_self" role="button"><span class="my-auto">RU</span>
</span></li>
<li class="me-3">
<span class="link-light text-decoration-none opacity-50 x-font-size-18 external-link" rel="nofollow" data-href="https://kz.hexlet.io/locale/switch?new_locale=kz" data-target="_self" role="button"><span class="my-auto">KZ</span>
</span></li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<ul class="list-unstyled fs-4">
<li class="mb-3">
<a class="link-light text-decoration-none" href="tel:8%20800%20100%2022%2047">8 800 100 22 47</a>
<span class="d-block opacity-50 small">бесплатно по РФ</span>
</li>
<li>
<a class="link-light text-decoration-none" href="tel:%2B7%20495%20085%2021%2062">+7 495 085 21 62</a>
<span class="d-block opacity-50 small">бесплатно по Москве</span>
</li>
</ul>
</div>
<div class="col-12 col-sm-4 col-md-3">
<div class="small mb-3">Образовательные услуги оказываются на основании Л035-01298-77/01989008 от 14.03.2025</div>
<ul class="list-unstyled small">
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/legal">Правовая информация</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/offer">Оферта</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/license">Лицензия</a>
</li>
<li>
<a class="nav-link link-light py-1 ps-0" href="/pages/contacts">Контакты</a>
</li>
</ul>
</div>
<div class="col-12 col-sm-12 col-md-4 small">
<div class="mb-2">
<div>ООО «<a href="/" class="text-decoration-none link-light">Хекслет Рус</a>»</div>
<div>108813 г. Москва, вн.тер.г. поселение Московский,</div>
<div>г. Московский, ул. Солнечная, д. 3А, стр. 1, помещ. 20Б/3</div>
<div>ОГРН 1217300010476</div>
<div>ИНН 7325174845</div>
</div>
<hr>
<div>АНО ДПО «<a href="/" class="text-decoration-none link-light">Учебный центр «Хекслет</a>»</div>
<div>119331 г. Москва, вн. тер. г. муниципальный округ</div>
<div>Ломоносовский, пр-кт Вернадского, д. 29</div>
<div>ОГРН 1247700712390</div>
<div>ИНН 7736364948</div>
</div>
</div>
</footer>
<div id="root-assistant-offcanvas"></div>
<script src="/vite/assets/assistant-CdBlNCiQ.js" crossorigin="anonymous" type="module"></script><link rel="modulepreload" href="/vite/assets/chunk-DsPFFUou.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/init-nkZBEvfU.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/ErrorFallbackBlock-naDSYSy9.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/MarkdownBlock-DbyKWoR_.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/gon-D3e4yh1x.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/mantine-CGMYrt2Y.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/shiki-V011pkdv.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/utils-DRqSHbQE.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/routes-CCH8ilKF.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-XR8Qr8kR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dist-GCHh59xr.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/Box-B5-OOzBf.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/notifications.store-C-3AFSMn.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useIsomorphicEffect-HJ6VK0D3.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/lib-KSp6QbZ0.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/axios-BEvgo0ym.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/classnames-l6ipYlLR.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/dayjs.min-BkKovM-s.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/debounce-jMQ_Cf4f.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/i18next-BlSq9s7B.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/client-U9M77rxp.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-dom-DaLxUz_h.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/useTranslation-Bx1Cdrkz.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/compiler-runtime-6XxiPFnt.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/jsx-runtime-CwjcCKJi.js" as="script" crossorigin="anonymous">
<link rel="modulepreload" href="/vite/assets/react-CkL4ZRHB.js" as="script" crossorigin="anonymous">
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v67327c56f0bb4ef8b305cae61679db8f1769101564043" integrity="sha512-rdcWY47ByXd76cbCFzznIcEaCN71jqkWBBqlwhF1SY7KubdLKZiEGeP7AyieKZlGP9hbY/MhGrwXzJC/HulNyg==" data-cf-beacon='{"version":"2024.11.0","token":"d11015b65d11429ea6b4a2ef37dd7e0b","server_timing":{"name":{"cfCacheStatus":true,"cfEdge":true,"cfExtPri":true,"cfL4":true,"cfOrigin":true,"cfSpeedBrain":true},"location_startswith":null}}' crossorigin="anonymous"></script>
</body>
</html>