В прошлом уроке мы познакомились с классами сложности задач и выяснили, что строгие алгоритмы практически неприменимы для задач класса NP.
Однако эти задачи все равно приходится решать: составлять расписания, распределять грузы и строить маршруты. Как именно программисты справляются со сложными задачами? Они могут применить один из трех подходов:
- Перейти от переборного алгоритма к полиномиальному — например, использовать мемоизацию и снизить сложность алгоритма с O(3^n) до O(n^2) при решении задачи о редакционном расстоянии. Этот подход называется динамическим программированием, он помогает оценить алгоритмическую сложность нового решения
- Ускорить алгоритм перебора, отбрасывая заведомо неподходящие варианты. Так работает метод ветвей и границ. В худшем случае, он работает медленно — со скоростью полного перебора. Здесь скорость зависит от того, с какими весами мы работаем и насколько точно оцениваем минимальное расстояние
- Обнаружить способ решения сложной задачи, опираясь на правдоподобные рассуждения. Так работает жадный алгоритм, который на каждом шаге выбирает наилучшее локальное решение. Он не дает оптимального результата, но его все равно применяют, потому что на реальных данных жадное решение может быть оптимальным
Иногда жадный алгоритм дает наилучшее решение, и это можно доказать математически. Например, он оптимально решает задачу о размене монет, если речь идет о российской, американской, европейской или другой валюте.
Но так бывает не всегда. Чаще программисты ищут приближенные решения с помощью эвристик — приемов или методов, полезность которого не имеет теоретического обоснования, но подтверждается практикой. Иногда эвристика точно не дает лучшего решения, но ее применяют, если результат достаточно хорош.
Жадный подход — типичный пример эвристического алгоритма. Метод ветвей и границ также считают эвристическим, поскольку его производительность сильно зависит от функции оценки, а ее подбирают эвристически — исходя из природы данных. В то же время, алгоритмы динамического программирования эвристическими не считают, поскольку они находят оптимальное решение с предсказуемой полиномиальной сложностью.
В этом уроке мы познакомимся с эвристическим алгоритмом А* (читается как «А-звездочка»), который находит кратчайший путь в графе.
Кратчайший путь
Мы уже решали эту задачу, когда разбирали алгоритм поиска в ширину. Он работает для невзвешенных графов, поскольку строит путь из минимального количества ребер.
Но во взвешенном графе длина пути определяется не количеством ребер, а суммой весов. Поиск в ширину перестает справляться, поэтому во взвешенном графе кратчайший путь строят с помощью алгоритма Дейкстры.
Вершины, которые алгоритм Дейкстры проверяет на каждом шаге, похожи на расходящуюся волну. Как и при поиске в ширину, волна расходится не равномерно, а в сторону ближайших вершин:
На этом рисунке мы видим волну, которая расходится из точки A при построении пути в точку B. Это классический поиск в ширину. Например, он применяется в играх, где на карте только дороги и препятствия.
Представим, что мы разрабатываем игру, в которой дороги бывают трех типов — трудная, обычная и легкая. Трудность дороги обозначим числами 1, 2 или 3 — это вес ребра, которое связывает соседние клетки.
Когда мы проходим клетку трудности 3, мы затрачиваем столько же усилий, сколько требуется на три клетки трудности 1. Поэтому речь идет о минимальной сумме значений в клетках, когда мы говорим о кратчайшем пути.
Во взвешенном графе волна будет выглядеть так:
Дальше всего волна продвигается по легким дорогам, и это логично — иногда быстрее дать круг по легкой дороге, чем срезать по трудной.
У поиска в ширину и алгоритма Дейкстры один и тот же недостаток — они тратят ресурсы на плохие варианты путей.
В произвольном графе мы не можем судить, какой путь хороший, а какой — плохой. Но если речь идет о географической карте, то из вершины A очевидно надо строить путь в сторону вершины B:
Так работает алгоритм А*. Он подходит для игровых и географических карт, поскольку опирается не геометрическое расстояние между точками. Также он подходит для любых графов, где можно подобрать хорошую эвристическую функцию нижней оценки расстояния.
Алгоритм А* умеет обходить препятствия. Он пробует напрямую пробиться к цели и находит кратчайший обходной путь, если прямой дороги нет.
Алгоритм А*
Вспомним, как работает поиск в ширину. Он находит всех соседей начальной вершины и помещает их в очередь. Затем в цикле алгоритм извлекает вершину из очереди, находит всех ее соседей и снова помещает в очередь. Поскольку очередь работает в соответствии с принципом «первый пришел — первый ушел», поиск в ширину перебирает сначала соседей, затем соседей соседей, затем соседей соседей соседей, и так далее.
Алгоритм А* действует похожим образом, однако вместо очереди использует очередь с приоритетом. Эту структуру данных мы не проходили, но столкнулись с ней, когда изучали метод ветвей и границ.
Элементы в такой очереди хранятся не сами по себе, а вместе с численной метрикой, которая называется приоритетом. Они извлекаются по возрастанию или убыванию приоритета — в зависимости от реализации.
Алгоритму А* нужны вершины, отсортированные по возрастанию приоритета. Приоритет рассчитывается как сумма двух значений:
- Фактического расстояния от начальной до текущей вершины
- Эвристической оценки расстояния от текущей вершины до конечной
Так это выглядит на схеме:
Здесь мы видим ситуацию при обработке вершины 4, когда мы ищем путь от вершины 1 к вершине 6.
Минимальное расстояние между вершинами 1 и и 4 равно пяти. Существует обходной путь через вершины 2 и 3 — его длина равна семи, так что он длиннее.
Мы не знаем минимального расстояния между вершинами 4 и 6. Это расстояние можно оценить геометрически как длину отрезка, который их соединяет (показан пунктирной красной линией). Из рисунка видно, что эта оценка также равна пяти.
Следовательно, приоритет вершины 4 — это 5 + 5 = 10.
Алгоритм А* работает не только с картами, но и с произвольными графами. Для них можно придумать эвристику, чтобы оценить нижнюю границу расстояния.
А*— это развитие алгоритма Дейкстры, который тоже использует очередь с приоритетами, но учитывает только фактическое расстояние от начальной вершины. Именно поэтому алгоритм Дейкстры осторожно двигается во все стороны, в то время как А* из всех доступных вершин выбирает те, которые ближе к концу пути.
Реализация
Посмотрим на реализацию алгоритма в коде:
Нажмите, чтобы увидеть код
Реализация функции astar() — это расширенный поиск в ширину, которую мы разбирали в четвертом уроке. Мы используем очередь с приоритетами вместо обычной. В стандартной библиотеке JavaScript нет соответствующей структуры данных, но можно использовать подходящий класс сторонних разработчиков — например, SortedMap.
Мы сделали очередь приоритетов на базе массива, добавив в него метод shiftMin(). Этот метод извлекает из массива элемент с минимальным приоритетом:
Обычно очередь с приоритетом реализуют поверх двоичного дерева, потому что это не самая производительная реализация. Зато одна из самых простых.
Чтобы оценить минимальное расстояние, считаем его как длину гипотенузы по теореме Пифагора:
Функция isValidNeighbour() проверяет, можно ли продолжить путь в ячейку с указанными координатами. Идти дальше нельзя, если ячейка выходит за границы карты или если мы ее уже посещали — она есть в множестве visited:
Функция tryAddCell() добавляет ячейку в очередь с приоритетами, если isValidNeighbour() считает ее валидной:
Каждый элемент очереди с приоритетами хранит четыре значения:
- priority — приоритет, оценка длины пути (считается как сумма пройденного пути и оценки оставшегося пути)
- elapsed — пройденный путь (считается как сумма значений во всех посещенных ячейках)
- cell — ячейка, до которой добрался алгоритм
- path — путь до этой ячейки
Исходные данные для функции astar() — двумерная карта или массив, где каждый элемент содержит трудность прохождения этой клетки. Минимальное значение 1 соответствует обычной клетке. Значение 3 означает клетку, которая соответствует трем обычным клеткам. Очень большие значения (в нашем случае 999) означает непреодолимое препятствие:
На схеме ниже показано, как выглядит эта карта. Области с более насыщенным цветом труднее для прохождения. Черным цветом мы обозначили непроходимые участки карты — стены или заборы:
На схеме мы видим маршрут, построенный алгоритмом. Большей частью он проходит по простым клеткам. Такой же маршрут построил бы и алгоритм Дейкстры, затратив больше времени.
Алгоритм А* работает быстрее благодаря эвристике. Мы знаем, что граф на самом деле представляет собой карту, и мы можем геометрически оценить расстояние между клетками:
Выводы
Повторим ключевые выводы этого урока:
- Эвристические алгоритмы широко применяются для решения задач класса NP
- Эвристическим называется алгоритм, который позволяет найти не оптимальное, но достаточно хорошее решение — например, жадный алгоритм
- Также эвристическим называется алгоритм, который может сократить полный перебор — например, метод ветвей и границ
- Ко второму варианту относится и алгоритм А*, который позволяет быстро построить кратчайший путь на географической карте
- Алгоритм А* — это расширенная версия алгоритма Дейкстры, которая пытается минимизировать длину пути от начальной до конечной клетки
- Полная длина для каждой клетки рассчитывается как сумма двух слагаемых
- Пройденного пути — это точное значение
- Оставшегося пути — это предполагаемая оценка
<!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 18:41:18 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="kJ67LT-FgkIItrU1ZpJL8cOsnapIf7bhyndgREhiHi9_T3AazfsvIr71ka1qnbuGA6WwAEBISEN3l_oQGmX5QQ";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/heuristic-algorithms/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/heuristic-algorithms/theory_unit">
<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="voQB7Nd1am_tCdGmJmhXZHjuVK4T3qscTvjWfp91oLtRVcrbJQvHD1tK9T4qZ6cTuOd5BBvpVb7zGEwqzXJH1Q" />
<script src="/vite/assets/inertia-DfXos102.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-BrRXra1y.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-cb8xch9l.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-26T18:41:18.825Z","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":"2UBExZKAFW4lGTFYipH-KCIbc2PFZlL0F-iZCirGrbg2kY_yYP64DpNaFcCGng5f4hJeyc1RrFaqCANeeMFK1g","topics":[],"lesson":{"exercise":{"id":3133,"slug":"algorithms_graphs_dijkstra_exercise","name":null,"state":"active","kind":"exercise","language":"javascript","locale":"ru","has_web_view":false,"has_test_view":false,"reviewable":true,"readme":"Реализуйте алгоритм Дейкстры для обхода графа. Он работает почти как алгоритм А*. Разница только в том, что алгоритм Дейсктры учитывает только расстояние до очередной клетки, не прибавляя к нему оценку оставшегося расстояния.\n\n## solution.js\n\nРеализуйте функцию `dijkstra(map, fromRow, fromColumn, toRow, toColumn)`. Она должна строить маршрут из точки `(fromRow, fromColumn)` в точку `(toRow, toColumn)` на карте `map`.\n\nФункция возвращает объект с полями \n* `path` - массив, представляющий кратчайший путь из начальной точки в конечную точку. Каждый элемент массива представляет вершину пути.\n* `distance` - число, представляющее общее расстояние (сумму весов ребер) кратчайшего пути из начальной точки в конечную точку\n* `visitedNodes` - число, представляющее количество просмотренных узлов (вершин) в процессе поиска кратчайшего пути.\n\n## Пример\n\n```javascript\nconst map = [\n [ 1, 1, 2, 2, 2 ],\n [ 1, 1, 2, 999, 999 ],\n [ 1, 1, 1, 1, 3 ],\n [ 1, 1, 1, 1, 3 ],\n [ 1, 1, 1, 1, 1 ]\n];\n\ndijkstra(map, 3, 3, 0, 3); // =>\n// {\n// path: [ '3:3', '2:3', '2:2', '1:2', '0:2', '0:3' ],\n// distance: 8,\n// visitedNodes: 21\n// }\n```\n","prepared_readme":"Реализуйте алгоритм Дейкстры для обхода графа. Он работает почти как алгоритм А*. Разница только в том, что алгоритм Дейсктры учитывает только расстояние до очередной клетки, не прибавляя к нему оценку оставшегося расстояния.\n\n## solution.js\n\nРеализуйте функцию `dijkstra(map, fromRow, fromColumn, toRow, toColumn)`. Она должна строить маршрут из точки `(fromRow, fromColumn)` в точку `(toRow, toColumn)` на карте `map`.\n\nФункция возвращает объект с полями \n* `path` - массив, представляющий кратчайший путь из начальной точки в конечную точку. Каждый элемент массива представляет вершину пути.\n* `distance` - число, представляющее общее расстояние (сумму весов ребер) кратчайшего пути из начальной точки в конечную точку\n* `visitedNodes` - число, представляющее количество просмотренных узлов (вершин) в процессе поиска кратчайшего пути.\n\n## Пример\n\n```javascript\nconst map = [\n [ 1, 1, 2, 2, 2 ],\n [ 1, 1, 2, 999, 999 ],\n [ 1, 1, 1, 1, 3 ],\n [ 1, 1, 1, 1, 3 ],\n [ 1, 1, 1, 1, 1 ]\n];\n\ndijkstra(map, 3, 3, 0, 3); // =>\n// {\n// path: [ '3:3', '2:3', '2:2', '1:2', '0:2', '0:3' ],\n// distance: 8,\n// visitedNodes: 21\n// }\n```\n","has_solution":true,"entity_name":"Эвристические алгоритмы"},"units":[{"id":9404,"name":"theory","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/theory_unit"},{"id":11278,"name":"quiz","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/quiz_unit"},{"id":11280,"name":"exercise","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/exercise_unit"}],"links":[],"ordered_units":[{"id":9404,"name":"theory","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/theory_unit"},{"id":11278,"name":"quiz","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/quiz_unit"},{"id":11280,"name":"exercise","url":"/courses/algorithms-graphs/lessons/heuristic-algorithms/exercise_unit"}],"id":4194,"slug":"heuristic-algorithms","state":"approved","name":"Эвристические алгоритмы","course_order":920,"goal":"Разбираемся, зачем применяются эвристические алгоритмы и как они работают на практике","self_study":null,"theory_video_provider":null,"theory_video_uid":null,"theory":"В прошлом уроке мы познакомились с классами сложности задач и выяснили, что строгие алгоритмы практически неприменимы для задач класса NP.\n\nОднако эти задачи все равно приходится решать: составлять расписания, распределять грузы и строить маршруты. Как именно программисты справляются со сложными задачами? Они могут применить один из трех подходов:\n\n* Перейти от переборного алгоритма к полиномиальному — например, использовать мемоизацию и снизить сложность алгоритма с `O(3^n)` до `O(n^2)` при решении задачи о редакционном расстоянии. Этот подход называется **динамическим программированием**, он помогает оценить алгоритмическую сложность нового решения\n* Ускорить алгоритм перебора, отбрасывая заведомо неподходящие варианты. Так работает **метод ветвей и границ**. В худшем случае, он работает медленно — со скоростью полного перебора. Здесь скорость зависит от того, с какими весами мы работаем и насколько точно оцениваем минимальное расстояние\n* Обнаружить способ решения сложной задачи, опираясь на правдоподобные рассуждения. Так работает жадный алгоритм, который на каждом шаге выбирает **наилучшее локальное решение**. Он не дает оптимального результата, но его все равно применяют, потому что на реальных данных жадное решение может быть оптимальным\n\nИногда жадный алгоритм дает наилучшее решение, и это можно доказать математически. Например, он [оптимально решает](https://arxiv.org/abs/0809.0400) задачу о размене монет, если речь идет о российской, американской, европейской или другой валюте.\n\nНо так бывает не всегда. Чаще программисты ищут приближенные решения с помощью **эвристик** — приемов или методов, полезность которого не имеет теоретического обоснования, но подтверждается практикой. Иногда эвристика точно не дает лучшего решения, но ее применяют, если результат достаточно хорош.\n\nЖадный подход — типичный пример эвристического алгоритма. Метод ветвей и границ также считают эвристическим, поскольку его производительность сильно зависит от функции оценки, а ее подбирают эвристически — исходя из природы данных. В то же время, алгоритмы динамического программирования эвристическими не считают, поскольку они находят оптимальное решение с предсказуемой полиномиальной сложностью.\n\nВ этом уроке мы познакомимся с эвристическим алгоритмом **А\\*** (читается как «А-звездочка»), который находит кратчайший путь в графе.\n\n## Кратчайший путь\n\nМы уже решали эту задачу, когда разбирали **алгоритм поиска в ширину**. Он работает для невзвешенных графов, поскольку строит путь из минимального количества ребер.\n\nНо во взвешенном графе длина пути определяется не количеством ребер, а суммой весов. Поиск в ширину перестает справляться, поэтому во взвешенном графе кратчайший путь строят с помощью алгоритма Дейкстры.\n\nВершины, которые алгоритм Дейкстры проверяет на каждом шаге, похожи на расходящуюся волну. Как и при поиске в ширину, волна расходится не равномерно, а в сторону ближайших вершин:\n\n\n\nНа этом рисунке мы видим волну, которая расходится из точки A при построении пути в точку B. Это классический поиск в ширину. Например, он применяется в играх, где на карте только дороги и препятствия.\n\nПредставим, что мы разрабатываем игру, в которой дороги бывают трех типов — трудная, обычная и легкая. Трудность дороги обозначим числами `1`, `2` или `3` — это вес ребра, которое связывает соседние клетки.\n\nКогда мы проходим клетку трудности `3`, мы затрачиваем столько же усилий, сколько требуется на три клетки трудности `1`. Поэтому речь идет о минимальной сумме значений в клетках, когда мы говорим о кратчайшем пути.\n\nВо взвешенном графе волна будет выглядеть так:\n\n\n\nДальше всего волна продвигается по легким дорогам, и это логично — иногда быстрее дать круг по легкой дороге, чем срезать по трудной.\n\nУ поиска в ширину и алгоритма Дейкстры один и тот же недостаток — они тратят ресурсы на плохие варианты путей.\n\nВ произвольном графе мы не можем судить, какой путь хороший, а какой — плохой. Но если речь идет о географической карте, то из вершины A очевидно надо строить путь в сторону вершины B:\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Здесь мы видим ситуацию при обработке вершины `4`, когда мы ищем путь от вершины `1` к вершине `6`.\n\nМинимальное расстояние между вершинами `1` и и `4` равно пяти. Существует обходной путь через вершины `2` и `3` — его длина равна семи, так что он длиннее.\n\nМы не знаем минимального расстояния между вершинами `4` и `6`. Это расстояние можно оценить геометрически как длину отрезка, который их соединяет (показан пунктирной красной линией). Из рисунка видно, что эта оценка также равна пяти.\n\nСледовательно, приоритет вершины `4` — это `5 + 5 = 10`.\n\nАлгоритм А* работает не только с картами, но и с произвольными графами. Для них можно придумать эвристику, чтобы оценить нижнюю границу расстояния.\n\nА*— это развитие алгоритма Дейкстры, который тоже использует очередь с приоритетами, но учитывает только фактическое расстояние от начальной вершины. Именно поэтому алгоритм Дейкстры осторожно двигается во все стороны, в то время как А* из всех доступных вершин выбирает те, которые ближе к концу пути.\n\n## Реализация\n\nПосмотрим на реализацию алгоритма в коде:\n\n<details>\n <summary>Нажмите, чтобы увидеть код</summary>\n\n```javascript\nconst astar = (map, fromRow, fromColumn, toRow, toColumn) => {\n const pack = (row, column) => `${row}:${column}`\n const unpack = cell => cell.split(':').map(x => parseInt(x, 10))\n\n const visited = new Set()\n const isValidNeighbour = (row, column) => {\n if (row < 0 || row >= map.length) {\n return false\n }\n\n if (column < 0 || column >= map[row].length) {\n return false\n }\n\n const cell = pack(row, column)\n if (visited.has(cell)) {\n return false\n }\n\n return true\n }\n\n const minDistance = (fromRow, fromColumn, toRow, toColumn) => Math\n .hypot(toRow - fromRow, toColumn - fromColumn)\n\n const priorityQueue = []\n priorityQueue.shiftMin = function () {\n let minIndex = 0\n let minPriority = this[minIndex].priority\n\n for (let i = 1; i < this.length; i += 1) {\n if (minPriority > this[i].priority) {\n minIndex = i\n minPriority = this[i].priority\n }\n }\n\n const result = this[minIndex]\n this.splice(minIndex, 1)\n\n return result\n }\n\n priorityQueue.push({\n priority: minDistance(fromRow, fromColumn, toRow, toColumn),\n elapsed: 0,\n cell: pack(fromRow, fromColumn),\n path: [],\n })\n\n while (priorityQueue.length > 0) {\n const top = priorityQueue.shiftMin()\n const [row, column] = unpack(top.cell)\n const path = [...top.path, top.cell]\n if (row === toRow && column === toColumn) {\n return path\n }\n\n visited.add(top.cell)\n\n const tryAddCell = (row, column) => {\n if (isValidNeighbour(row, column)) {\n const nextCell = pack(row, column)\n const elapsed = top.elapsed + map[row][column]\n const remaining = minDistance(fromRow, fromColumn, toRow, toColumn)\n priorityQueue.push({\n priority: elapsed + remaining,\n elapsed,\n cell: nextCell,\n path,\n })\n }\n }\n\n tryAddCell(row - 1, column)\n tryAddCell(row + 1, column)\n tryAddCell(row, column - 1)\n tryAddCell(row, column + 1)\n }\n\n return null\n}\n```\n\n</details>\n\nРеализация функции `astar()` — это расширенный поиск в ширину, которую мы разбирали [в четвертом уроке](https://ru.hexlet.io/courses/algorithms-graphs/lessons/in-breadth-search/theory_unit). Мы используем очередь с приоритетами вместо обычной. В стандартной библиотеке JavaScript нет соответствующей структуры данных, но можно использовать подходящий класс сторонних разработчиков — например, [SortedMap](https://www.collectionsjs.com/sorted-map).\n\nМы сделали очередь приоритетов на базе массива, добавив в него метод `shiftMin()`. Этот метод извлекает из массива элемент с минимальным приоритетом:\n\n```javascript\n const priorityQueue = [];\n priorityQueue.shiftMin = function () {\n let minIndex = 0;\n let minPriority = this[minIndex].priority;\n\n for (let i = 1; i < this.length; i += 1) {\n if (minPriority > this[i].priority) {\n minIndex = i;\n minPriority = this[i].priority;\n }\n }\n\n const result = this[minIndex];\n this.splice(minIndex, 1);\n\n return result;\n\n```\n\nОбычно очередь с приоритетом реализуют поверх двоичного дерева, потому что это не самая производительная реализация. Зато одна из самых простых.\n\nЧтобы оценить минимальное расстояние, считаем его как длину гипотенузы по теореме Пифагора:\n\n\n\n```javascript\nconst minDistance = (fromRow, fromColumn, toRow, toColumn) =>\n Math.hypot(toRow - fromRow, toColumn - fromColumn)\n```\n\nФункция `isValidNeighbour()` проверяет, можно ли продолжить путь в ячейку с указанными координатами. Идти дальше нельзя, если ячейка выходит за границы карты или если мы ее уже посещали — она есть в множестве `visited`:\n\n```javascript\n const isValidNeighbour = (row, column) => {\n if (row < 0 || row >= map.length) {\n return false;\n }\n\n if (column < 0 || column >= map[row].length) {\n return false;\n }\n\n const cell = pack(row, column);\n if (visited.has(cell)) {\n return false;\n }\n\n return true;\n```\n\nФункция `tryAddCell()` добавляет ячейку в очередь с приоритетами, если `isValidNeighbour()` считает ее валидной:\n\n```javascript\nconst tryAddCell = (row, column) => {\n if (isValidNeighbour(row, column)) {\n const nextCell = pack(row, column)\n const elapsed = top.elapsed + map[row][column]\n const remaining = minDistance(fromRow, fromColumn, toRow, toColumn)\n priorityQueue.push({\n priority: elapsed + remaining,\n elapsed: elapsed,\n cell: nextCell,\n path: path,\n })\n }\n}\n```\n\nКаждый элемент очереди с приоритетами хранит четыре значения:\n\n* `priority` — приоритет, оценка длины пути (считается как сумма пройденного пути и оценки оставшегося пути)\n* `elapsed` — пройденный путь (считается как сумма значений во всех посещенных ячейках)\n* `cell` — ячейка, до которой добрался алгоритм\n* `path` — путь до этой ячейки\n\nИсходные данные для функции `astar()` — двумерная карта или массив, где каждый элемент содержит трудность прохождения этой клетки. Минимальное значение `1` соответствует обычной клетке. Значение 3 означает клетку, которая соответствует трем обычным клеткам. Очень большие значения (в нашем случае `999`) означает непреодолимое препятствие:\n\n```javascript\nconst map = [\n [1, 1, 2, 2, 2, 2, 2, 2, 2, 1],\n [1, 1, 2, 2, 2, 2, 2, 2, 2, 1],\n [1, 1, 1, 999, 999, 999, 999, 2, 2, 1],\n [1, 1, 1, 1, 1, 3, 999, 2, 1, 1],\n [1, 1, 1, 1, 1, 3, 999, 1, 1, 3],\n [1, 1, 1, 1, 2, 3, 999, 1, 2, 3],\n [1, 1, 1, 1, 2, 3, 999, 1, 3, 4],\n [1, 1, 1, 1, 1, 3, 3, 1, 3, 4],\n [1, 1, 1, 1, 1, 3, 3, 1, 3, 4],\n [1, 1, 1, 1, 1, 1, 1, 1, 1, 4],\n]\n```\n\nНа схеме ниже показано, как выглядит эта карта. Области с более насыщенным цветом труднее для прохождения. Черным цветом мы обозначили непроходимые участки карты — стены или заборы:\n\n\n\n```javascript\nastar(map, 5, 4, 0, 9)\n// [\n// '5:4', '6:4', '7:4',\n// '7:5', '7:6', '7:7',\n// '6:7', '5:7', '4:7',\n// '4:8', '3:8', '3:9',\n// '2:9', '1:9', '0:9'\n// ]\n```\n\nНа схеме мы видим маршрут, построенный алгоритмом. Большей частью он проходит по простым клеткам. Такой же маршрут построил бы и алгоритм Дейкстры, затратив больше времени.\n\nАлгоритм А* работает быстрее благодаря эвристике. Мы знаем, что граф на самом деле представляет собой карту, и мы можем геометрически оценить расстояние между клетками:\n\n\n\n## Выводы\n\nПовторим ключевые выводы этого урока:\n\n* Эвристические алгоритмы широко применяются для решения задач класса NP\n* Эвристическим называется алгоритм, который позволяет найти не оптимальное, но достаточно хорошее решение — например, жадный алгоритм\n* Также эвристическим называется алгоритм, который может сократить полный перебор — например, метод ветвей и границ\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/heuristic-algorithms/theory_unit","version":"8f286f6358a90a7bef2263b3a6edf5a90a94fa42","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>В прошлом уроке мы познакомились с классами сложности задач и выяснили, что строгие алгоритмы практически неприменимы для задач класса NP.</p>
<p>Однако эти задачи все равно приходится решать: составлять расписания, распределять грузы и строить маршруты. Как именно программисты справляются со сложными задачами? Они могут применить один из трех подходов:</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">O(3^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(n^2)</code> при решении задачи о редакционном расстоянии. Этот подход называется <strong>динамическим программированием</strong>, он помогает оценить алгоритмическую сложность нового решения</li>
<li>Ускорить алгоритм перебора, отбрасывая заведомо неподходящие варианты. Так работает <strong>метод ветвей и границ</strong>. В худшем случае, он работает медленно — со скоростью полного перебора. Здесь скорость зависит от того, с какими весами мы работаем и насколько точно оцениваем минимальное расстояние</li>
<li>Обнаружить способ решения сложной задачи, опираясь на правдоподобные рассуждения. Так работает жадный алгоритм, который на каждом шаге выбирает <strong>наилучшее локальное решение</strong>. Он не дает оптимального результата, но его все равно применяют, потому что на реальных данных жадное решение может быть оптимальным</li>
</ul>
<p>Иногда жадный алгоритм дает наилучшее решение, и это можно доказать математически. Например, он <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://arxiv.org/abs/0809.0400" rel="noopener noreferrer" target="_blank">оптимально решает</a> задачу о размене монет, если речь идет о российской, американской, европейской или другой валюте.</p>
<p>Но так бывает не всегда. Чаще программисты ищут приближенные решения с помощью <strong>эвристик</strong> — приемов или методов, полезность которого не имеет теоретического обоснования, но подтверждается практикой. Иногда эвристика точно не дает лучшего решения, но ее применяют, если результат достаточно хорош.</p>
<p>Жадный подход — типичный пример эвристического алгоритма. Метод ветвей и границ также считают эвристическим, поскольку его производительность сильно зависит от функции оценки, а ее подбирают эвристически — исходя из природы данных. В то же время, алгоритмы динамического программирования эвристическими не считают, поскольку они находят оптимальное решение с предсказуемой полиномиальной сложностью.</p>
<p>В этом уроке мы познакомимся с эвристическим алгоритмом <strong>А*</strong> (читается как «А-звездочка»), который находит кратчайший путь в графе.</p>
<h2 id="heading-2-1">Кратчайший путь</h2>
<p>Мы уже решали эту задачу, когда разбирали <strong>алгоритм поиска в ширину</strong>. Он работает для невзвешенных графов, поскольку строит путь из минимального количества ребер.</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/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDAsInB1ciI6ImJsb2JfaWQifX0=--640cd9abc0043bd780cf42bacd20361b5629cd1c/01.png" alt="300" loading="lazy"/></p>
<p>На этом рисунке мы видим волну, которая расходится из точки A при построении пути в точку B. Это классический поиск в ширину. Например, он применяется в играх, где на карте только дороги и препятствия.</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</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> — это вес ребра, которое связывает соседние клетки.</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</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">1</code>. Поэтому речь идет о минимальной сумме значений в клетках, когда мы говорим о кратчайшем пути.</p>
<p>Во взвешенном графе волна будет выглядеть так:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDEsInB1ciI6ImJsb2JfaWQifX0=--b2d5e1eb1ab88b7052df40a3e4b56a5c4e934d08/02.png" alt="300" loading="lazy"/></p>
<p>Дальше всего волна продвигается по легким дорогам, и это логично — иногда быстрее дать круг по легкой дороге, чем срезать по трудной.</p>
<p>У поиска в ширину и алгоритма Дейкстры один и тот же недостаток — они тратят ресурсы на плохие варианты путей.</p>
<p>В произвольном графе мы не можем судить, какой путь хороший, а какой — плохой. Но если речь идет о географической карте, то из вершины A очевидно надо строить путь в сторону вершины B:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDIsInB1ciI6ImJsb2JfaWQifX0=--284bf3c5162db009609c407a0d70aa242fb2660f/03.png" alt="300" loading="lazy"/></p>
<p>Так работает алгоритм А*. Он подходит для игровых и географических карт, поскольку опирается не геометрическое расстояние между точками. Также он подходит для любых графов, где можно подобрать хорошую эвристическую функцию нижней оценки расстояния.</p>
<p>Алгоритм А* умеет обходить препятствия. Он пробует напрямую пробиться к цели и находит кратчайший обходной путь, если прямой дороги нет.</p>
<h2 id="heading-2-2">Алгоритм А*</h2>
<p>Вспомним, как работает поиск в ширину. Он находит всех соседей начальной вершины и помещает их в очередь. Затем в цикле алгоритм извлекает вершину из очереди, находит всех ее соседей и снова помещает в очередь. Поскольку очередь работает в соответствии с принципом «первый пришел — первый ушел», поиск в ширину перебирает сначала соседей, затем соседей соседей, затем соседей соседей соседей, и так далее.</p>
<p>Алгоритм А* действует похожим образом, однако вместо очереди использует очередь с приоритетом. Эту структуру данных мы не проходили, но столкнулись с ней, когда изучали метод ветвей и границ.</p>
<p>Элементы в такой очереди хранятся не сами по себе, а вместе с численной метрикой, которая называется <strong>приоритетом</strong>. Они извлекаются по возрастанию или убыванию приоритета — в зависимости от реализации.</p>
<p>Алгоритму А* нужны вершины, отсортированные по возрастанию приоритета. Приоритет рассчитывается как сумма двух значений:</p>
<ul>
<li>Фактического расстояния от начальной до текущей вершины</li>
<li>Эвристической оценки расстояния от текущей вершины до конечной</li>
</ul>
<p>Так это выглядит на схеме:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDMsInB1ciI6ImJsb2JfaWQifX0=--0046d79a1dae7f3fc2cee52b6521002a5c433fc9/04.png" alt="300" 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">4</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">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">6</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">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">4</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> — его длина равна семи, так что он длиннее.</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">4</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">6</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">4</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">5 + 5 = 10</code>.</p>
<p>Алгоритм А* работает не только с картами, но и с произвольными графами. Для них можно придумать эвристику, чтобы оценить нижнюю границу расстояния.</p>
<p>А*— это развитие алгоритма Дейкстры, который тоже использует очередь с приоритетами, но учитывает только фактическое расстояние от начальной вершины. Именно поэтому алгоритм Дейкстры осторожно двигается во все стороны, в то время как А* из всех доступных вершин выбирает те, которые ближе к концу пути.</p>
<h2 id="heading-2-3">Реализация</h2>
<p>Посмотрим на реализацию алгоритма в коде:</p>
<details>
<summary>Нажмите, чтобы увидеть код</summary>
<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 astar = (map, fromRow, fromColumn, toRow, toColumn) => {
const pack = (row, column) => `${row}:${column}`
const unpack = cell => cell.split(':').map(x => parseInt(x, 10))
const visited = new Set()
const isValidNeighbour = (row, column) => {
if (row < 0 || row >= map.length) {
return false
}
if (column < 0 || column >= map[row].length) {
return false
}
const cell = pack(row, column)
if (visited.has(cell)) {
return false
}
return true
}
const minDistance = (fromRow, fromColumn, toRow, toColumn) => Math
.hypot(toRow - fromRow, toColumn - fromColumn)
const priorityQueue = []
priorityQueue.shiftMin = function () {
let minIndex = 0
let minPriority = this[minIndex].priority
for (let i = 1; i < this.length; i += 1) {
if (minPriority > this[i].priority) {
minIndex = i
minPriority = this[i].priority
}
}
const result = this[minIndex]
this.splice(minIndex, 1)
return result
}
priorityQueue.push({
priority: minDistance(fromRow, fromColumn, toRow, toColumn),
elapsed: 0,
cell: pack(fromRow, fromColumn),
path: [],
})
while (priorityQueue.length > 0) {
const top = priorityQueue.shiftMin()
const [row, column] = unpack(top.cell)
const path = [...top.path, top.cell]
if (row === toRow && column === toColumn) {
return path
}
visited.add(top.cell)
const tryAddCell = (row, column) => {
if (isValidNeighbour(row, column)) {
const nextCell = pack(row, column)
const elapsed = top.elapsed + map[row][column]
const remaining = minDistance(fromRow, fromColumn, toRow, toColumn)
priorityQueue.push({
priority: elapsed + remaining,
elapsed,
cell: nextCell,
path,
})
}
}
tryAddCell(row - 1, column)
tryAddCell(row + 1, column)
tryAddCell(row, column - 1)
tryAddCell(row, column + 1)
}
return null
}</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>
</details>
<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">astar()</code> — это расширенный поиск в ширину, которую мы разбирали <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://ru.hexlet.io/courses/algorithms-graphs/lessons/in-breadth-search/theory_unit" rel="noopener noreferrer" target="_blank">в четвертом уроке</a>. Мы используем очередь с приоритетами вместо обычной. В стандартной библиотеке JavaScript нет соответствующей структуры данных, но можно использовать подходящий класс сторонних разработчиков — например, <a style="text-decoration:underline" class="mantine-focus-auto m_849cf0da m_b6d8b162 mantine-Text-root mantine-Anchor-root" data-underline="hover" href="https://www.collectionsjs.com/sorted-map" rel="noopener noreferrer" target="_blank">SortedMap</a>.</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">shiftMin()</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">const priorityQueue = [];
priorityQueue.shiftMin = function () {
let minIndex = 0;
let minPriority = this[minIndex].priority;
for (let i = 1; i < this.length; i += 1) {
if (minPriority > this[i].priority) {
minIndex = i;
minPriority = this[i].priority;
}
}
const result = this[minIndex];
this.splice(minIndex, 1);
return result;</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>Чтобы оценить минимальное расстояние, считаем его как длину гипотенузы по теореме Пифагора:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDQsInB1ciI6ImJsb2JfaWQifX0=--a697b9f03a1425d0d72b10eaadf9914b3dcd6651/05.png" alt="300" loading="lazy"/></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 minDistance = (fromRow, fromColumn, toRow, toColumn) =>
Math.hypot(toRow - fromRow, toColumn - fromColumn)</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">isValidNeighbour()</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">visited</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">const isValidNeighbour = (row, column) => {
if (row < 0 || row >= map.length) {
return false;
}
if (column < 0 || column >= map[row].length) {
return false;
}
const cell = pack(row, column);
if (visited.has(cell)) {
return false;
}
return true;</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">tryAddCell()</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">isValidNeighbour()</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">const tryAddCell = (row, column) => {
if (isValidNeighbour(row, column)) {
const nextCell = pack(row, column)
const elapsed = top.elapsed + map[row][column]
const remaining = minDistance(fromRow, fromColumn, toRow, toColumn)
priorityQueue.push({
priority: elapsed + remaining,
elapsed: elapsed,
cell: nextCell,
path: path,
})
}
}</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>
<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">priority</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">elapsed</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">cell</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">path</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">astar()</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">1</code> соответствует обычной клетке. Значение 3 означает клетку, которая соответствует трем обычным клеткам. Очень большие значения (в нашем случае <code style="margin-bottom:var(--mantine-spacing-lg)" class="m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight m_e597c321 mantine-CodeHighlight-codeHighlight m_dfe9c588 mantine-InlineCodeHighlight-inlineCodeHighlight">999</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">const map = [
[1, 1, 2, 2, 2, 2, 2, 2, 2, 1],
[1, 1, 2, 2, 2, 2, 2, 2, 2, 1],
[1, 1, 1, 999, 999, 999, 999, 2, 2, 1],
[1, 1, 1, 1, 1, 3, 999, 2, 1, 1],
[1, 1, 1, 1, 1, 3, 999, 1, 1, 3],
[1, 1, 1, 1, 2, 3, 999, 1, 2, 3],
[1, 1, 1, 1, 2, 3, 999, 1, 3, 4],
[1, 1, 1, 1, 1, 3, 3, 1, 3, 4],
[1, 1, 1, 1, 1, 3, 3, 1, 3, 4],
[1, 1, 1, 1, 1, 1, 1, 1, 1, 4],
]</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/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDUsInB1ciI6ImJsb2JfaWQifX0=--72c64a415dcf7320161201a47a3f9c9f909257b2/06.png" alt="300" loading="lazy"/></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">astar(map, 5, 4, 0, 9)
// [
// '5:4', '6:4', '7:4',
// '7:5', '7:6', '7:7',
// '6:7', '5:7', '4:7',
// '4:8', '3:8', '3:9',
// '2:9', '1:9', '0:9'
// ]</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>Алгоритм А* работает быстрее благодаря эвристике. Мы знаем, что граф на самом деле представляет собой карту, и мы можем геометрически оценить расстояние между клетками:</p>
<p><img style="--image-object-fit:contain;width:auto" class="m_9e117634 mantine-Image-root" src="/rails/active_storage/blobs/proxy/eyJfcmFpbHMiOnsiZGF0YSI6MTQ0NDYsInB1ciI6ImJsb2JfaWQifX0=--14b6be519530454b8b3fcf9407751f658745e70f/07.png" alt="300" loading="lazy"/></p>
<h2 id="heading-2-4">Выводы</h2>
<p>Повторим ключевые выводы этого урока:</p>
<ul>
<li>Эвристические алгоритмы широко применяются для решения задач класса NP</li>
<li>Эвристическим называется алгоритм, который позволяет найти не оптимальное, но достаточно хорошее решение — например, жадный алгоритм</li>
<li>Также эвристическим называется алгоритм, который может сократить полный перебор — например, метод ветвей и границ</li>
<li>Ко второму варианту относится и алгоритм А*, который позволяет быстро построить кратчайший путь на географической карте</li>
<li>Алгоритм А* — это расширенная версия алгоритма Дейкстры, которая пытается минимизировать длину пути от начальной до конечной клетки</li>
<li>Полная длина для каждой клетки рассчитывается как сумма двух слагаемых
<ul>
<li>Пройденного пути — это точное значение</li>
<li>Оставшегося пути — это предполагаемая оценка</li>
</ul>
</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/heuristic-algorithms/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/heuristic-algorithms/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-Bukl1lYy.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-BrRXra1y.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>