Обход графа в ширину: очередь, слои и кратчайший путь

Обход графа в ширину (BFS, breadth-first search) - базовый способ систематически посетить все вершины, до которых можно добраться из заданной. Его идея в одной фразе: сначала осматриваем всех соседей старта, потом соседей соседей, и так далее, расходясь от начальной вершины ровной волной. Волну держит обычная очередь, а побочный эффект такого порядка оказывается важнее самого обхода: номер слоя, в котором вершина оказалась, и есть кратчайшее расстояние до неё в невзвешенном графе. Ниже разберём порядок посещения, восстановление пути, проверку двудольности и сложность . А для начала прокрутите обход по шагам в калькуляторе: он показывает, что лежит в очереди на каждом шаге и как растут расстояния.
Что такое обход графа в ширину
Пусть граф задан списками смежности, а обход стартует из вершины . Обход в ширину строит слои:
Слой - это множество вершин, до которых от старта ровно рёбер. На учебном графе из калькулятора (8 вершин, 10 рёбер) обход из даёт слои , , , , : сначала три прямых соседа старта, затем то, что видно с них, и так до самой дальней вершины на расстоянии 4.
Вручную слои строить неудобно, поэтому алгоритм заменяет их очередью. Вершина попадает в конец очереди в тот момент, когда её впервые увидели, и выходит из начала, когда приходит время осматривать её соседей. Очередь работает по принципу «первым пришёл, первым вышел», поэтому вершины выходят из неё ровно в порядке неубывания расстояния от старта: сначала весь слой 1, потом весь слой 2 и так далее. В любой момент в очереди лежат вершины не более чем двух соседних слоёв - это и есть тот самый «фронт волны».
Очередь и порядок посещения
Ключевая деталь реализации: вершина помечается посещённой в момент добавления в очередь, а не в момент извлечения. Иначе одна и та же вершина попадёт в очередь столько раз, сколько у неё соседей в предыдущем слое, и обход разбухнет.
d[s] ← 0; p[s] ← нет; очередь ← [s]
пока очередь не пуста:
u ← извлечь из начала очереди
для каждого соседа v вершины u:
если d[v] ещё не определено:
d[v] ← d[u] + 1
p[v] ← u
добавить v в конец очереди
Здесь - расстояние от старта в рёбрах, - вершина, из которой открыли. Массив пригодится дальше для восстановления пути.
На нашем графе порядок выхода из очереди получается , , , , , , , . Обратите внимание: когда обрабатывается , вершина уже открыта из , поэтому ребро ничего не открывает. Таких рёбер здесь три: , , . Остальные семь образуют дерево обхода, и это не случайность: каждое ребро дерева открывает ровно одну новую вершину, а новых вершин на единицу меньше, чем всего, значит в дереве обхода связного графа всегда ребро.
Кратчайший путь в невзвешенном графе
Главное свойство обхода в ширину формулируется так: если все рёбра равноценны (вес каждого равен 1), то найденное значение равно минимальному числу рёбер в пути от до .
Почему так. Во-первых, не может быть меньше истинного расстояния: значения выставляются вдоль рёбер по цепочке, то есть - длина какого-то реального пути. Во-вторых, оно не может быть и больше. Пусть кратчайший путь до проходит через вершину , для которой утверждение уже доказано. Вершина выходит из очереди раньше, чем любая вершина со строго большим расстоянием, и в этот момент либо уже открыта с расстоянием не больше , либо открывается прямо сейчас именно с этим значением. Отсюда же следует полезное следствие: для любого ребра выполняется , то есть ребро не может перепрыгнуть через слой.

Важна именно невзвешенность. Как только у рёбер появляются разные веса, первая встреча с вершиной перестаёт быть самой дешёвой, и обход в ширину даёт неверный ответ: на графе с весами нужен алгоритм Дейкстры с приоритетной очередью. Частный случай на стыке - граф с весами 0 и 1: там достаточно заменить очередь на дек и класть вершину в начало при нулевом ребре.
Восстановление пути по массиву предков
Сами расстояния отвечают на вопрос «сколько», а маршрут хранится в массиве предков . Чтобы получить путь до вершины , идут от неё назад: , , и так до старта, у которого предка нет, а затем разворачивают последовательность.
Для нашего графа: , , , . Разворачиваем и получаем путь длиной 4 ребра - ровно столько, сколько обещало значение . Кратчайший путь не обязан быть единственным: через и до ведёт путь той же длины, просто обход открыл первым. Хранить в очереди сами пути целиком не нужно и вредно: массив предков занимает памяти вместо .
Проверка двудольности одним обходом
Тем же обходом проверяют, можно ли раскрасить вершины в два цвета так, чтобы концы каждого ребра были разного цвета. Раскрасим вершину в цвет по чётности её слоя. Если граф двудольный, любое ребро обязано соединять соседние слои. Если же нашлось ребро внутри одного слоя, то вместе с путями от старта к его концам оно замыкает цикл нечётной длины, а граф с нечётным циклом двудольным быть не может.
На учебном графе все десять рёбер идут между соседними слоями, доли получаются и . Включите в калькуляторе ребро : оба его конца лежат в слое 1, появляется треугольник , и проверка честно сообщает, что двудольность сломалась. Сама двудольность нужна дальше для паросочетаний, например в теореме Кёнига.
Сложность обхода: O(V + E)
Каждая вершина попадает в очередь не больше одного раза, значит извлечений будет не больше . Для извлечённой вершины просматривается её список смежности, то есть каждое ребро неориентированного графа рассматривается дважды, по разу с каждого конца. Итого
и на нашем графе это элементарных шагов. Линейность держится только на списках смежности: если граф хранится матрицей, на каждую вершину уходит проверок и обход вырождается в . Отсюда же стоимость производных задач: чтобы получить матрицу кратчайших расстояний или радиус и диаметр графа, запускают обход из каждой вершины и платят .
Чем обход в ширину отличается от обхода в глубину
Различие в одной структуре данных: в ширину используют очередь, в глубину - стек или рекурсию. Как работает вторая схема со всеми её следствиями - времена входа и выхода, классификация рёбер, поиск циклов - разобрано в статье про обход графа в глубину. Отсюда расходятся и порядок посещения, и свойства полученного дерева.

Обход в ширину выдаёт вершины по слоям и даёт кратчайший путь до каждой из них. Обход в глубину уходит по первой попавшейся ветке до упора: на том же графе он доберётся до маршрутом из 6 рёбер, хотя есть путь из 4. Поэтому, когда в задаче есть слово «кратчайший», а веса рёбер одинаковы, берут именно обход в ширину. По времени оба обхода одинаковы, , но по памяти в ширину дороже: очередь может разом хранить целый слой, то есть до вершин, тогда как стек в глубину хранит лишь текущую ветку. На ациклическом графе очередь из этого же обхода работает ещё в одной классической задаче - топологической сортировке, где в неё кладут вершины с нулевой степенью входа.
Частые ошибки
- Помечать вершину при извлечении из очереди, а не при добавлении. Тогда вершина попадает в очередь столько раз, сколько у неё соседей в предыдущем слое, и время обхода растёт.
- Брать стек вместо очереди. Получится обход в глубину, а найденные расстояния перестанут быть кратчайшими.
- Применять обход в ширину к взвешенному графу. При разных весах первая встреча с вершиной не гарантирует минимальную стоимость, нужен Дейкстра.
- Обходить только из одной вершины и считать, что граф пройден. В несвязном графе обход накрывает лишь одну компоненту, остальные надо запускать отдельно.
- Класть в очередь сами пути вместо вершин. Память раздувается до , хотя достаточно массива предков.
FAQ
Обход в ширину и поиск в ширину - это одно и то же? Да, это два русских названия одного алгоритма BFS. Слово «поиск» подчёркивает задачу найти конкретную вершину, слово «обход» - задачу посетить все достижимые, но сам порядок действий совпадает.
Работает ли обход в ширину на ориентированном графе? Работает: из извлечённой вершины идут только по исходящим дугам. Расстояния при этом несимметричны, и из в может быть путь, а обратно нет, поэтому недостижимые вершины так и остаются с неопределённым .
Как обойти весь граф и посчитать компоненты связности? Перебирают вершины подряд и из каждой непосещённой запускают новый обход, увеличивая счётчик. Суммарная сложность остаётся : каждая вершина и каждое ребро обрабатываются один раз за все запуски.
Коротко
Обход графа в ширину раскладывает вершины по слоям расстояния от старта: очередь выдаёт их в порядке неубывания , вершина помечается при добавлении в очередь, а открывшее её ребро запоминается в массиве предков. В невзвешенном графе номер слоя равен кратчайшему расстоянию, а разворот массива предков даёт сам путь; той же волной проверяется двудольность по чётности слоя. Обход стоит времени и памяти на списках смежности, но вырождается в на матрице смежности; для разных весов рёбер вместо него берут алгоритм Дейкстры.
Читайте также

Алгоритм Дейкстры: как найти кратчайший путь в графе
Разбираем алгоритм Дейкстры на взвешенном графе по шагам: как он находит кратчайшие пути от стартовой вершины и почему ломается на ребрах с отрицательными весами.

Алгоритм Беллмана-Форда: пути с отрицательными весами
Разбираем алгоритм Беллмана-Форда: как искать кратчайшие пути в графе с отрицательными рёбрами, ловить отрицательные циклы и чем он отличается от Дейкстры.

Алгоритм Прима - как построить остовное дерево по шагам
Разбираем, как алгоритм Прима шаг за шагом строит минимальное остовное дерево графа: идея жадного выбора, лемма о разрезе и трассировка на конкретном примере.

Радиус и диаметр графа: как считать эксцентриситет
Что такое радиус и диаметр графа и как через эксцентриситет вершин найти центр и периферию графа. Формулы, пошаговый пример и связь с расстояниями между вершинами.

Алгоритм Эдмондса-Карпа: поиск максимального потока
Алгоритм Эдмондса-Карпа находит максимальный поток в сети: BFS ищет дополняющий путь, что даёт полиномиальную сложность. Разбираем идею, реализацию и оценку.

Алгоритм Куна: как найти максимальное паросочетание
Алгоритм Куна шаг за шагом: ищем увеличивающие цепи обычным DFS и находим максимальное паросочетание в двудольном графе за O(V·E), с разбором идеи и сложности.