Поиск в ширину (BFS)
Начинаем с вершины и рассматриваем её соседей, затем соседей этих соседей. Очередь «первым пришёл — первым вышел» позволяет BFS двигаться по слоям и находить кратчайшие пути по числу рёбер.
Загружаем интерактивный граф…
Исходный граф
Вершины A–H; неориентированные рёбра AB, AC, BD, BE, CE, CF, DG, EG и FG. У H нет рёбер. Изначально старт — A, цель — G.
Слои, а не геометрическое расстояние
BFS сначала обнаруживает старт с расстоянием 0, затем вершины в одном ребре от него, потом в двух и так далее. Расположение на рисунке условно: длина линии на экране не влияет на ответ. Граф остаётся неподвижным на протяжении всего воспроизведения.
В примере используется простой неориентированный граф без весов. Плеер полностью обходит компоненту, достижимую из старта, и только после этого восстанавливает путь до цели. При первом обнаружении цели поиск не останавливается. Остальные компоненты остаются необойдёнными.
Отмечаем вершину при добавлении в очередь
Берём вершину из начала очереди, рассматриваем соседей в заданном порядке и добавляем каждого ещё не обнаруженного соседа в конец. В этот же момент задаём расстояние и предка. Вершина может быть обнаружена, но ещё ждать обработки в очереди.
Каждая вершина попадает в очередь один раз. Поэтому цикл не приводит к бесконечному обходу, а два разных предка не добавляют одного соседа повторно. Расстояния вершин очереди не убывают. Текущая вершина уже извлечена из очереди; обработанная — та, у которой рассмотрены все соседи.
Цикл и вершина, которая уже в очереди
В основном примере соседи перебираются по алфавиту. После A очередь равна [B, C]. При обработке B добавляем D и E, получаем [C, D, E]. У C тоже есть сосед E, но он уже обнаружен: пропускаем и сохраняем предка B.
Полный порядок обработки: A, B, C, D, E, F, G. Расстояния: A — 0, B/C — 1, D/E/F — 2, G — 3. Изолированная H недостижима. Обход обнаруживает и обрабатывает 7 вершин, просматривает 18 записей соседей, максимум очереди — 3.
Счётчик просмотров учитывает записи списков смежности, включая пропуски. Неориентированное ребро рассматривается с двух концов: 9 рёбер достижимой компоненты дают 18 просмотров. Старт включён в число обнаруженных. Восстановление пути не меняет счётчики обхода.
Один кратчайший путь, но не обязательно единственный
Идём по предкам от G: G, D, B, A. Разворачиваем последовательность и получаем A → B → D → G длиной 3 ребра. У предка, выбранного при первом обнаружении, расстояние всегда на единицу меньше. Поэтому цепочка предков дойдёт до старта без цикла.
Через E или F тоже можно пройти за 3 ребра. Другой порядок соседей может изменить предка и выбранный путь, но не кратчайшее расстояние. Выберите целью H, чтобы увидеть отсутствие пути, или сам старт для пути длиной 0. В режиме «Без цели» плеер показывает только завершённый обход.
До окончания обхода прочерк означает, что вершина ещё не обнаружена. После него ∞ означает недостижимость из этого старта. Во время восстановления частичный путь показан от цели к старту, окончательный результат — от старта к цели.
Время, память и сохранённая история
При списках смежности верхняя граница времени — O(V + E). Инициализация расстояний и предков охватывает все вершины, сам обход рассматривает только достижимую компоненту. Очередь и данные вершин занимают O(V) дополнительной памяти, хранение графа — O(V + E). Восстановление одного пути требует O(V) времени и памяти результата.
У визуализации отдельная стоимость: до O(V + E) неизменяемых снимков с O(V) данными вершин в каждом, то есть O(V(V + E)) памяти истории плюс общий граф. Лимит сцены в восемь вершин нужен для читаемости, а не из-за ограничения BFS. В реализации используется индекс головы очереди вместо повторного сдвига массива через shift().
Реализация на JavaScript
Код принимает корректный простой неориентированный граф как Map от id вершины к упорядоченному массиву соседей, существующий старт и необязательную существующую цель. Полный обход и выбор предка совпадают с плеером. Исходные Map и массивы не меняются. Плеер дополнительно проверяет и копирует граф перед созданием снимков.
function breadthFirstSearch(adjacency, start, target = null) {
// Map<vertexId, vertexId[]>; a valid undirected graph.
const distance = new Map([...adjacency.keys()].map(id => [id, null]));
const parent = new Map([...adjacency.keys()].map(id => [id, null]));
const queue = [start];
distance.set(start, 0);
let edgeChecks = 0;
let maxQueueSize = 1;
for (let head = 0; head < queue.length; head++) {
const vertex = queue[head];
for (const neighbor of adjacency.get(vertex)) {
edgeChecks++;
if (distance.get(neighbor) !== null) continue;
// Mark on enqueue, not when the vertex is removed.
distance.set(neighbor, distance.get(vertex) + 1);
parent.set(neighbor, vertex);
queue.push(neighbor);
maxQueueSize = Math.max(maxQueueSize, queue.length - head - 1);
}
}
const path = [];
if (target !== null && distance.get(target) !== null) {
for (let vertex = target; vertex !== null; vertex = parent.get(vertex)) {
path.push(vertex);
}
path.reverse();
}
return { distance, parent, path, order: queue, edgeChecks, maxQueueSize };
}Где применим BFS
Кратчайший путь здесь — путь с минимальным числом рёбер. Если стоимость рёбер различается, обычный BFS не гарантирует минимальную суммарную стоимость. DFS идёт в глубину и полезен для других задач, но его первый путь в общем случае не кратчайший по числу рёбер.
Алгоритм и раскладки графов определены на сайте. AlgiViz 0.8.0 предоставляет оболочки шагов, временную шкалу и пользовательские объекты сцены. Граф, очередь и доступный текст описывают одно сохранённое состояние; перемотка не запускает обход заново.