Алгоритмы

Поиск A*

Жадный поиск смотрит, насколько клетка кажется близкой к цели. A* учитывает ещё и путь, уже найденный до этой клетки. Сравним правила на том же лабиринте.

S — старт · G — цель · Числа — f = g + h (∞ до обнаружения); h не учитывает стены

Начало

Текущая: —

Граница поиска (порядок обнаружения): (1, 3) 0+6=6

Обнаружено / извлечено / просмотров соседей: 1 / 0 / 0; улучшений пути: 0

Фиолетовый — граница поиска · Рамка — текущая клетка · Зелёный — путь. Координаты (столбец, строка) начинаются с 1. Плеер переключает состояния без движения.

Видеоразбор

Два расстояния — один приоритет

g — длина лучшего пока найденного пути от S до клетки. h — манхэттенская оценка до G: |столбец − столбецЦели| + |строка − строкаЦели|, без учёта стен. Сумма f = g + h оценивает длину всего маршрута через эту клетку. Это ещё не готовый путь.

В начале g(S)=0, у остальных клеток — бесконечность. Фиолетовая граница поиска содержит обнаруженные, ещё не извлечённые клетки. Выбираем минимальное f из всей границы; при равенстве раньше выбираем раньше обнаруженную клетку. Соседей смотрим в порядке север, восток, юг, запад. Поиск может переключаться между ветками: выбор клетки не означает физический переход из предыдущей.

Более короткий путь меняет предка

Для соседа через открытый проход пробуем candidate = g(текущая) + 1. Если это меньше сохранённого g, обновляем расстояние и предка. Новую клетку добавляем в границу поиска; уже ожидающая остаётся на месте с новым приоритетом. При равной длине сохраняем прежнего предка. Такое улучшение называют релаксацией.

Просто заменить h на g+h в Greedy с первым найденным предком недостаточно. Сначала до клетки может обнаружиться длинный путь, а затем более короткий. Здесь эвристика согласованная, поэтому у извлечённой для обработки клетки расстояние уже кратчайшее и повторно открывать её не нужно.

Восемь переходов вместо десяти

Перед нами тот же лабиринт 7×4, что в выпуске про Greedy. Координаты (столбец, строка) начинаются с 1. S=(1,3), G=(7,3). У восточного соседа g=1, h=5, f=6; у южного — g=1, h=7, f=8. A* тоже начинает справа и в итоге встречает тупик рядом с целью.

Верхний обход поднимает оценку до 10. У нижней ветки остаются клетки с f=8, и A* выбирает их раньше. Итоговый путь проходит по четвёртой строке и содержит 8 переходов; верхний маршрут Greedy — 10. Зелёный путь восстанавливается по предкам, а не по порядку обработки клеток. Пример показывает выбор по приоритету; не каждый вид релаксации обязательно встречается в этом обходе.

Почему путь здесь кратчайший

Каждый проход стоит единицу, движения разрешены по четырём направлениям, лабиринт конечный. Манхэттенская h равна нулю у цели, не завышает оставшийся путь и согласованна: для любого прохода u→v выполняется h(u) ≤ 1 + h(v). Оценку получаем, убрав стены; возвращение стен не может сократить маршрут.

При этих условиях закрытую клетку не нужно открывать заново. Завершаем поиск, когда цель извлечена с минимальным f, а не при первом обнаружении. Тогда A* возвращает кратчайший путь либо сообщает, что пути нет, если граница опустела. При несогласованной эвристике версия без повторного открытия может потерять эту гарантию. Для диагональных ходов, весов рёбер и других эвристик условия и код нужно пересмотреть.

Как читать плеер

Числа в клетках показывают f; бесконечность означает, что клетка ещё не обнаружена. S и G сохраняют свои обозначения. Под лабиринтом приведено g+h=f для текущей клетки и всей границы поиска. Фиолетовый цвет обозначает границу, рамка — текущую клетку, зелёный — восстановленный маршрут.

«Обнаружено» включает S и каждое первое добавление; «извлечено» считает выбор из границы, включая цель. «Просмотров соседей» учитывает открытые записи смежности, даже уже известных соседей; стены и соседи цели не входят в счётчик. «Улучшений пути» считает уменьшения уже конечного g. При восстановлении путь растёт от цели к старту, затем разворачивается в итоговом снимке. Попробуйте шаги, перемотку и пример с нулевым путём, когда старт совпадает с целью.

Стоимость этой версии

Для V клеток и E проходов реализация на массиве требует O(V² + E) времени и O(V) дополнительной рабочей памяти без хранения графа. Каждая клетка извлекается не более одного раза; поиск и удаление минимума стоят O(V), релаксация — O(1) на соседа. Бинарная куча с уменьшением ключа позволяет получить O((V+E) log V); здесь ради наглядности используется просмотр массива.

Для перемотки сохраняем полный снимок размером O(V) на каждом видимом событии. Событий O(V+E); в сетке с четырьмя соседями E=O(V), поэтому история занимает O(V²) памяти плюс общий лабиринт. Стоимость алгоритма и хранения визуализации различается.

Реализация на JavaScript

Код принимает конечный Map от id к упорядоченным массивам соседей, существующие id старта и цели, единичную стоимость рёбер и конечную неотрицательную согласованную h с h(цель)=0. Id не может быть null. Для повторения примера используйте открытые проходы в порядке С/В/Ю/З и манхэттенскую h. Вход не меняется; null означает отсутствие пути. Эта версия не поддерживает произвольные веса и не открывает закрытые вершины заново.

function aStar(adjacency, start, goal, h) {
    const open = [start];
    const closed = new Set();
    const g = new Map([[start, 0]]);
    const parent = new Map([[start, null]]);
    const f = v => g.get(v) + h(v);
    while (open.length) {
        let best = 0;
        for (let i = 1; i < open.length; i++) {
            if (f(open[i]) < f(open[best])) best = i;
        }
        const current = open.splice(best, 1)[0];
        if (current === goal) {
            const path = [];
            for (let v = goal; v !== null; v = parent.get(v)) path.push(v);
            return path.reverse();
        }
        closed.add(current);
        for (const next of adjacency.get(current)) {
            if (closed.has(next)) continue;
            const candidate = g.get(current) + 1;
            if (candidate >= (g.get(next) ?? Infinity)) continue;
            const unseen = !g.has(next);
            g.set(next, candidate);
            parent.set(next, current);
            if (unseen) open.push(next);
        }
    }
    return null;
}

UC Berkeley CS 188 — Informed Search