HTML Diff
0 added 0 removed
Original 2026-01-01
Modified 2026-02-26
1 <p>В прошлом уроке мы познакомились с классами сложности задач и выяснили, что строгие алгоритмы практически неприменимы для задач класса NP.</p>
1 <p>В прошлом уроке мы познакомились с классами сложности задач и выяснили, что строгие алгоритмы практически неприменимы для задач класса NP.</p>
2 <p>Однако эти задачи все равно приходится решать: составлять расписания, распределять грузы и строить маршруты. Как именно программисты справляются со сложными задачами? Они могут применить один из трех подходов:</p>
2 <p>Однако эти задачи все равно приходится решать: составлять расписания, распределять грузы и строить маршруты. Как именно программисты справляются со сложными задачами? Они могут применить один из трех подходов:</p>
3 <ul><li>Перейти от переборного алгоритма к полиномиальному - например, использовать мемоизацию и снизить сложность алгоритма с O(3^n) до O(n^2) при решении задачи о редакционном расстоянии. Этот подход называется<strong>динамическим программированием</strong>, он помогает оценить алгоритмическую сложность нового решения</li>
3 <ul><li>Перейти от переборного алгоритма к полиномиальному - например, использовать мемоизацию и снизить сложность алгоритма с O(3^n) до O(n^2) при решении задачи о редакционном расстоянии. Этот подход называется<strong>динамическим программированием</strong>, он помогает оценить алгоритмическую сложность нового решения</li>
4 <li>Ускорить алгоритм перебора, отбрасывая заведомо неподходящие варианты. Так работает<strong>метод ветвей и границ</strong>. В худшем случае, он работает медленно - со скоростью полного перебора. Здесь скорость зависит от того, с какими весами мы работаем и насколько точно оцениваем минимальное расстояние</li>
4 <li>Ускорить алгоритм перебора, отбрасывая заведомо неподходящие варианты. Так работает<strong>метод ветвей и границ</strong>. В худшем случае, он работает медленно - со скоростью полного перебора. Здесь скорость зависит от того, с какими весами мы работаем и насколько точно оцениваем минимальное расстояние</li>
5 <li>Обнаружить способ решения сложной задачи, опираясь на правдоподобные рассуждения. Так работает жадный алгоритм, который на каждом шаге выбирает<strong>наилучшее локальное решение</strong>. Он не дает оптимального результата, но его все равно применяют, потому что на реальных данных жадное решение может быть оптимальным</li>
5 <li>Обнаружить способ решения сложной задачи, опираясь на правдоподобные рассуждения. Так работает жадный алгоритм, который на каждом шаге выбирает<strong>наилучшее локальное решение</strong>. Он не дает оптимального результата, но его все равно применяют, потому что на реальных данных жадное решение может быть оптимальным</li>
6 </ul><p>Иногда жадный алгоритм дает наилучшее решение, и это можно доказать математически. Например, он<a>оптимально решает</a>задачу о размене монет, если речь идет о российской, американской, европейской или другой валюте.</p>
6 </ul><p>Иногда жадный алгоритм дает наилучшее решение, и это можно доказать математически. Например, он<a>оптимально решает</a>задачу о размене монет, если речь идет о российской, американской, европейской или другой валюте.</p>
7 <p>Но так бывает не всегда. Чаще программисты ищут приближенные решения с помощью<strong>эвристик</strong>- приемов или методов, полезность которого не имеет теоретического обоснования, но подтверждается практикой. Иногда эвристика точно не дает лучшего решения, но ее применяют, если результат достаточно хорош.</p>
7 <p>Но так бывает не всегда. Чаще программисты ищут приближенные решения с помощью<strong>эвристик</strong>- приемов или методов, полезность которого не имеет теоретического обоснования, но подтверждается практикой. Иногда эвристика точно не дает лучшего решения, но ее применяют, если результат достаточно хорош.</p>
8 <p>Жадный подход - типичный пример эвристического алгоритма. Метод ветвей и границ также считают эвристическим, поскольку его производительность сильно зависит от функции оценки, а ее подбирают эвристически - исходя из природы данных. В то же время, алгоритмы динамического программирования эвристическими не считают, поскольку они находят оптимальное решение с предсказуемой полиномиальной сложностью.</p>
8 <p>Жадный подход - типичный пример эвристического алгоритма. Метод ветвей и границ также считают эвристическим, поскольку его производительность сильно зависит от функции оценки, а ее подбирают эвристически - исходя из природы данных. В то же время, алгоритмы динамического программирования эвристическими не считают, поскольку они находят оптимальное решение с предсказуемой полиномиальной сложностью.</p>
9 <p>В этом уроке мы познакомимся с эвристическим алгоритмом<strong>А*</strong>(читается как "А-звездочка"), который находит кратчайший путь в графе.</p>
9 <p>В этом уроке мы познакомимся с эвристическим алгоритмом<strong>А*</strong>(читается как "А-звездочка"), который находит кратчайший путь в графе.</p>
10 <h2>Кратчайший путь</h2>
10 <h2>Кратчайший путь</h2>
11 <p>Мы уже решали эту задачу, когда разбирали<strong>алгоритм поиска в ширину</strong>. Он работает для невзвешенных графов, поскольку строит путь из минимального количества ребер.</p>
11 <p>Мы уже решали эту задачу, когда разбирали<strong>алгоритм поиска в ширину</strong>. Он работает для невзвешенных графов, поскольку строит путь из минимального количества ребер.</p>
12 <p>Но во взвешенном графе длина пути определяется не количеством ребер, а суммой весов. Поиск в ширину перестает справляться, поэтому во взвешенном графе кратчайший путь строят с помощью алгоритма Дейкстры.</p>
12 <p>Но во взвешенном графе длина пути определяется не количеством ребер, а суммой весов. Поиск в ширину перестает справляться, поэтому во взвешенном графе кратчайший путь строят с помощью алгоритма Дейкстры.</p>
13 <p>Вершины, которые алгоритм Дейкстры проверяет на каждом шаге, похожи на расходящуюся волну. Как и при поиске в ширину, волна расходится не равномерно, а в сторону ближайших вершин:</p>
13 <p>Вершины, которые алгоритм Дейкстры проверяет на каждом шаге, похожи на расходящуюся волну. Как и при поиске в ширину, волна расходится не равномерно, а в сторону ближайших вершин:</p>
14 <p>На этом рисунке мы видим волну, которая расходится из точки A при построении пути в точку B. Это классический поиск в ширину. Например, он применяется в играх, где на карте только дороги и препятствия.</p>
14 <p>На этом рисунке мы видим волну, которая расходится из точки A при построении пути в точку B. Это классический поиск в ширину. Например, он применяется в играх, где на карте только дороги и препятствия.</p>
15 <p>Представим, что мы разрабатываем игру, в которой дороги бывают трех типов - трудная, обычная и легкая. Трудность дороги обозначим числами 1, 2 или 3 - это вес ребра, которое связывает соседние клетки.</p>
15 <p>Представим, что мы разрабатываем игру, в которой дороги бывают трех типов - трудная, обычная и легкая. Трудность дороги обозначим числами 1, 2 или 3 - это вес ребра, которое связывает соседние клетки.</p>
16 <p>Когда мы проходим клетку трудности 3, мы затрачиваем столько же усилий, сколько требуется на три клетки трудности 1. Поэтому речь идет о минимальной сумме значений в клетках, когда мы говорим о кратчайшем пути.</p>
16 <p>Когда мы проходим клетку трудности 3, мы затрачиваем столько же усилий, сколько требуется на три клетки трудности 1. Поэтому речь идет о минимальной сумме значений в клетках, когда мы говорим о кратчайшем пути.</p>
17 <p>Во взвешенном графе волна будет выглядеть так:</p>
17 <p>Во взвешенном графе волна будет выглядеть так:</p>
18 <p>Дальше всего волна продвигается по легким дорогам, и это логично - иногда быстрее дать круг по легкой дороге, чем срезать по трудной.</p>
18 <p>Дальше всего волна продвигается по легким дорогам, и это логично - иногда быстрее дать круг по легкой дороге, чем срезать по трудной.</p>
19 <p>У поиска в ширину и алгоритма Дейкстры один и тот же недостаток - они тратят ресурсы на плохие варианты путей.</p>
19 <p>У поиска в ширину и алгоритма Дейкстры один и тот же недостаток - они тратят ресурсы на плохие варианты путей.</p>
20 <p>В произвольном графе мы не можем судить, какой путь хороший, а какой - плохой. Но если речь идет о географической карте, то из вершины A очевидно надо строить путь в сторону вершины B:</p>
20 <p>В произвольном графе мы не можем судить, какой путь хороший, а какой - плохой. Но если речь идет о географической карте, то из вершины A очевидно надо строить путь в сторону вершины B:</p>
21 <p>Так работает алгоритм А*. Он подходит для игровых и географических карт, поскольку опирается не геометрическое расстояние между точками. Также он подходит для любых графов, где можно подобрать хорошую эвристическую функцию нижней оценки расстояния.</p>
21 <p>Так работает алгоритм А*. Он подходит для игровых и географических карт, поскольку опирается не геометрическое расстояние между точками. Также он подходит для любых графов, где можно подобрать хорошую эвристическую функцию нижней оценки расстояния.</p>
22 <p>Алгоритм А* умеет обходить препятствия. Он пробует напрямую пробиться к цели и находит кратчайший обходной путь, если прямой дороги нет.</p>
22 <p>Алгоритм А* умеет обходить препятствия. Он пробует напрямую пробиться к цели и находит кратчайший обходной путь, если прямой дороги нет.</p>
23 <h2>Алгоритм А*</h2>
23 <h2>Алгоритм А*</h2>
24 <p>Вспомним, как работает поиск в ширину. Он находит всех соседей начальной вершины и помещает их в очередь. Затем в цикле алгоритм извлекает вершину из очереди, находит всех ее соседей и снова помещает в очередь. Поскольку очередь работает в соответствии с принципом "первый пришел - первый ушел", поиск в ширину перебирает сначала соседей, затем соседей соседей, затем соседей соседей соседей, и так далее.</p>
24 <p>Вспомним, как работает поиск в ширину. Он находит всех соседей начальной вершины и помещает их в очередь. Затем в цикле алгоритм извлекает вершину из очереди, находит всех ее соседей и снова помещает в очередь. Поскольку очередь работает в соответствии с принципом "первый пришел - первый ушел", поиск в ширину перебирает сначала соседей, затем соседей соседей, затем соседей соседей соседей, и так далее.</p>
25 <p>Алгоритм А* действует похожим образом, однако вместо очереди использует очередь с приоритетом. Эту структуру данных мы не проходили, но столкнулись с ней, когда изучали метод ветвей и границ.</p>
25 <p>Алгоритм А* действует похожим образом, однако вместо очереди использует очередь с приоритетом. Эту структуру данных мы не проходили, но столкнулись с ней, когда изучали метод ветвей и границ.</p>
26 <p>Элементы в такой очереди хранятся не сами по себе, а вместе с численной метрикой, которая называется<strong>приоритетом</strong>. Они извлекаются по возрастанию или убыванию приоритета - в зависимости от реализации.</p>
26 <p>Элементы в такой очереди хранятся не сами по себе, а вместе с численной метрикой, которая называется<strong>приоритетом</strong>. Они извлекаются по возрастанию или убыванию приоритета - в зависимости от реализации.</p>
27 <p>Алгоритму А* нужны вершины, отсортированные по возрастанию приоритета. Приоритет рассчитывается как сумма двух значений:</p>
27 <p>Алгоритму А* нужны вершины, отсортированные по возрастанию приоритета. Приоритет рассчитывается как сумма двух значений:</p>
28 <ul><li>Фактического расстояния от начальной до текущей вершины</li>
28 <ul><li>Фактического расстояния от начальной до текущей вершины</li>
29 <li>Эвристической оценки расстояния от текущей вершины до конечной</li>
29 <li>Эвристической оценки расстояния от текущей вершины до конечной</li>
30 </ul><p>Так это выглядит на схеме:</p>
30 </ul><p>Так это выглядит на схеме:</p>
31 <p>Здесь мы видим ситуацию при обработке вершины 4, когда мы ищем путь от вершины 1 к вершине 6.</p>
31 <p>Здесь мы видим ситуацию при обработке вершины 4, когда мы ищем путь от вершины 1 к вершине 6.</p>
32 <p>Минимальное расстояние между вершинами 1 и и 4 равно пяти. Существует обходной путь через вершины 2 и 3 - его длина равна семи, так что он длиннее.</p>
32 <p>Минимальное расстояние между вершинами 1 и и 4 равно пяти. Существует обходной путь через вершины 2 и 3 - его длина равна семи, так что он длиннее.</p>
33 <p>Мы не знаем минимального расстояния между вершинами 4 и 6. Это расстояние можно оценить геометрически как длину отрезка, который их соединяет (показан пунктирной красной линией). Из рисунка видно, что эта оценка также равна пяти.</p>
33 <p>Мы не знаем минимального расстояния между вершинами 4 и 6. Это расстояние можно оценить геометрически как длину отрезка, который их соединяет (показан пунктирной красной линией). Из рисунка видно, что эта оценка также равна пяти.</p>
34 <p>Следовательно, приоритет вершины 4 - это 5 + 5 = 10.</p>
34 <p>Следовательно, приоритет вершины 4 - это 5 + 5 = 10.</p>
35 <p>Алгоритм А* работает не только с картами, но и с произвольными графами. Для них можно придумать эвристику, чтобы оценить нижнюю границу расстояния.</p>
35 <p>Алгоритм А* работает не только с картами, но и с произвольными графами. Для них можно придумать эвристику, чтобы оценить нижнюю границу расстояния.</p>
36 <p>А*- это развитие алгоритма Дейкстры, который тоже использует очередь с приоритетами, но учитывает только фактическое расстояние от начальной вершины. Именно поэтому алгоритм Дейкстры осторожно двигается во все стороны, в то время как А* из всех доступных вершин выбирает те, которые ближе к концу пути.</p>
36 <p>А*- это развитие алгоритма Дейкстры, который тоже использует очередь с приоритетами, но учитывает только фактическое расстояние от начальной вершины. Именно поэтому алгоритм Дейкстры осторожно двигается во все стороны, в то время как А* из всех доступных вершин выбирает те, которые ближе к концу пути.</p>
37 <h2>Реализация</h2>
37 <h2>Реализация</h2>
38 <p>Посмотрим на реализацию алгоритма в коде:</p>
38 <p>Посмотрим на реализацию алгоритма в коде:</p>
39 Нажмите, чтобы увидеть код<p>Реализация функции astar() - это расширенный поиск в ширину, которую мы разбирали<a>в четвертом уроке</a>. Мы используем очередь с приоритетами вместо обычной. В стандартной библиотеке JavaScript нет соответствующей структуры данных, но можно использовать подходящий класс сторонних разработчиков - например,<a>SortedMap</a>.</p>
39 Нажмите, чтобы увидеть код<p>Реализация функции astar() - это расширенный поиск в ширину, которую мы разбирали<a>в четвертом уроке</a>. Мы используем очередь с приоритетами вместо обычной. В стандартной библиотеке JavaScript нет соответствующей структуры данных, но можно использовать подходящий класс сторонних разработчиков - например,<a>SortedMap</a>.</p>
40 <p>Мы сделали очередь приоритетов на базе массива, добавив в него метод shiftMin(). Этот метод извлекает из массива элемент с минимальным приоритетом:</p>
40 <p>Мы сделали очередь приоритетов на базе массива, добавив в него метод shiftMin(). Этот метод извлекает из массива элемент с минимальным приоритетом:</p>
41 <p>Обычно очередь с приоритетом реализуют поверх двоичного дерева, потому что это не самая производительная реализация. Зато одна из самых простых.</p>
41 <p>Обычно очередь с приоритетом реализуют поверх двоичного дерева, потому что это не самая производительная реализация. Зато одна из самых простых.</p>
42 <p>Чтобы оценить минимальное расстояние, считаем его как длину гипотенузы по теореме Пифагора:</p>
42 <p>Чтобы оценить минимальное расстояние, считаем его как длину гипотенузы по теореме Пифагора:</p>
43 <p>Функция isValidNeighbour() проверяет, можно ли продолжить путь в ячейку с указанными координатами. Идти дальше нельзя, если ячейка выходит за границы карты или если мы ее уже посещали - она есть в множестве visited:</p>
43 <p>Функция isValidNeighbour() проверяет, можно ли продолжить путь в ячейку с указанными координатами. Идти дальше нельзя, если ячейка выходит за границы карты или если мы ее уже посещали - она есть в множестве visited:</p>
44 <p>Функция tryAddCell() добавляет ячейку в очередь с приоритетами, если isValidNeighbour() считает ее валидной:</p>
44 <p>Функция tryAddCell() добавляет ячейку в очередь с приоритетами, если isValidNeighbour() считает ее валидной:</p>
45 <p>Каждый элемент очереди с приоритетами хранит четыре значения:</p>
45 <p>Каждый элемент очереди с приоритетами хранит четыре значения:</p>
46 <ul><li>priority - приоритет, оценка длины пути (считается как сумма пройденного пути и оценки оставшегося пути)</li>
46 <ul><li>priority - приоритет, оценка длины пути (считается как сумма пройденного пути и оценки оставшегося пути)</li>
47 <li>elapsed - пройденный путь (считается как сумма значений во всех посещенных ячейках)</li>
47 <li>elapsed - пройденный путь (считается как сумма значений во всех посещенных ячейках)</li>
48 <li>cell - ячейка, до которой добрался алгоритм</li>
48 <li>cell - ячейка, до которой добрался алгоритм</li>
49 <li>path - путь до этой ячейки</li>
49 <li>path - путь до этой ячейки</li>
50 </ul><p>Исходные данные для функции astar() - двумерная карта или массив, где каждый элемент содержит трудность прохождения этой клетки. Минимальное значение 1 соответствует обычной клетке. Значение 3 означает клетку, которая соответствует трем обычным клеткам. Очень большие значения (в нашем случае 999) означает непреодолимое препятствие:</p>
50 </ul><p>Исходные данные для функции astar() - двумерная карта или массив, где каждый элемент содержит трудность прохождения этой клетки. Минимальное значение 1 соответствует обычной клетке. Значение 3 означает клетку, которая соответствует трем обычным клеткам. Очень большие значения (в нашем случае 999) означает непреодолимое препятствие:</p>
51 <p>На схеме ниже показано, как выглядит эта карта. Области с более насыщенным цветом труднее для прохождения. Черным цветом мы обозначили непроходимые участки карты - стены или заборы:</p>
51 <p>На схеме ниже показано, как выглядит эта карта. Области с более насыщенным цветом труднее для прохождения. Черным цветом мы обозначили непроходимые участки карты - стены или заборы:</p>
52 <p>На схеме мы видим маршрут, построенный алгоритмом. Большей частью он проходит по простым клеткам. Такой же маршрут построил бы и алгоритм Дейкстры, затратив больше времени.</p>
52 <p>На схеме мы видим маршрут, построенный алгоритмом. Большей частью он проходит по простым клеткам. Такой же маршрут построил бы и алгоритм Дейкстры, затратив больше времени.</p>
53 <p>Алгоритм А* работает быстрее благодаря эвристике. Мы знаем, что граф на самом деле представляет собой карту, и мы можем геометрически оценить расстояние между клетками:</p>
53 <p>Алгоритм А* работает быстрее благодаря эвристике. Мы знаем, что граф на самом деле представляет собой карту, и мы можем геометрически оценить расстояние между клетками:</p>
54 <h2>Выводы</h2>
54 <h2>Выводы</h2>
55 <p>Повторим ключевые выводы этого урока:</p>
55 <p>Повторим ключевые выводы этого урока:</p>
56 <ul><li>Эвристические алгоритмы широко применяются для решения задач класса NP</li>
56 <ul><li>Эвристические алгоритмы широко применяются для решения задач класса NP</li>
57 <li>Эвристическим называется алгоритм, который позволяет найти не оптимальное, но достаточно хорошее решение - например, жадный алгоритм</li>
57 <li>Эвристическим называется алгоритм, который позволяет найти не оптимальное, но достаточно хорошее решение - например, жадный алгоритм</li>
58 <li>Также эвристическим называется алгоритм, который может сократить полный перебор - например, метод ветвей и границ</li>
58 <li>Также эвристическим называется алгоритм, который может сократить полный перебор - например, метод ветвей и границ</li>
59 <li>Ко второму варианту относится и алгоритм А*, который позволяет быстро построить кратчайший путь на географической карте</li>
59 <li>Ко второму варианту относится и алгоритм А*, который позволяет быстро построить кратчайший путь на географической карте</li>
60 <li>Алгоритм А* - это расширенная версия алгоритма Дейкстры, которая пытается минимизировать длину пути от начальной до конечной клетки</li>
60 <li>Алгоритм А* - это расширенная версия алгоритма Дейкстры, которая пытается минимизировать длину пути от начальной до конечной клетки</li>
61 <li>Полная длина для каждой клетки рассчитывается как сумма двух слагаемых<ul><li>Пройденного пути - это точное значение</li>
61 <li>Полная длина для каждой клетки рассчитывается как сумма двух слагаемых<ul><li>Пройденного пути - это точное значение</li>
62 <li>Оставшегося пути - это предполагаемая оценка</li>
62 <li>Оставшегося пути - это предполагаемая оценка</li>
63 </ul></li>
63 </ul></li>
64 </ul>
64 </ul>