Все главы учебника
Содержание учебника
Глава 10 / Алгоритмы

Графы и BFS: путь с минимальным числом переходов

6 мин чтенияКонтент v0.10.0

Есть станции метро и переходы между ними. Нужно найти маршрут с минимальным числом переходов. Номер станции не говорит о соседстве; список связей описывает отдельную структуру — граф.

Выбираем представление

Вершины — станции, рёбра — допустимые переходы. В ориентированном графе связь u→v не подразумевает обратную. Для двустороннего перехода в списке смежности добавляем оба направления. Петля возвращает вершину к себе; параллельные рёбра связывают ту же пару несколько раз. Решение должно явно сказать, допустимы ли они.

g := [][]int{
    {1, 2}, // из 0
    {3},    // из 1
    {3},    // из 2
    {},     // из 3
    {},     // изолированная 4
}

Список смежности занимает O(V+E) памяти: V вершин, E записей о рёбрах. Матрица смежности имеет V² ячеек и позволяет за O(1) проверить наличие заданного ребра, но просмотр всех соседей вершины требует O(V). Ни одна структура не «лучше вообще»: учитывайте плотность и нужные операции.

Обход слоями

BFS, поиск в ширину, использует FIFO-очередь. Сначала рассматриваем источник, затем все вершины на расстоянии одного ребра, затем двух и так далее. Отмечайте вершину посещённой в момент добавления в очередь, иначе два соседа могут одновременно поставить её несколько раз даже в последовательном коде.

parent := make([]int, len(g))
for i := range parent { parent[i] = -1 }
parent[source] = source
queue := []int{source}
for head := 0; head < len(queue); head++ {
    v := queue[head]
    for _, next := range g[v] {
        if parent[next] == -1 {
            parent[next] = v
            queue = append(queue, next)
        }
    }
}

Здесь parent одновременно хранит признак открытия и предшественника. −1 означает «ещё не открыта», а source ссылается на себя. Нужны допустимые source и номера соседей; reference.BFSPath проверяет их до обхода. Сам по себе тип int не доказывает, что индекс существует.

Восстанавливаем маршрут

Если parent[target] остаётся−1, пути нет. Иначе идём от target к parent до source, записываем вершины, затем переворачиваем результат. Для source=target ответ состоит из одной вершины; количество рёбер пути равно len(path)−1.

Для графа из примера есть пути 0→1→3 и 0→2→3. BFS вернёт один из них в зависимости от порядка соседей. Нельзя требовать всегда один и тот же маршрут, если договор обещает только минимальную длину. Детерминированный порядок можно задать отдельно.

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

Очередь извлекает вершины в порядке неубывающего расстояния по числу рёбер. Когда вершина открыта впервые из слоя d, её расстояние d+1. Более короткий путь пришёл бы из более раннего слоя и открыл её раньше. Это доказательство зависит от одинаковой цены каждого перехода.

Если один переход стоит 1 минуту, другой 100, число рёбер не означает минимальное время. Взвешенные пути потребуют другого алгоритма. Время BFS O(V+E), дополнительная память O(V), не считая хранения входного графа.

Практика

Реализуйте BFSPath и проверку входа. Для target3 получите путь длины 2 ребра; для target4 — отсутствие; для source=target — путь из одного элемента. Добавьте цикл, петлю и повторное ребро: обход не должен бесконечно увеличивать очередь.

Для проверки пути проверяйте его начало, конец и существование каждого ребра, затем длину. Не сравнивайте только с одной заранее выбранной последовательностью при нескольких равных вариантах. Практика — 55 минут.

Критерии готовности: каждая вершина ставится в очередь не больше одного раза; направление рёбер соблюдается; отсутствие пути отделено от ошибки входа; результат отвечает выбранной метрике.

Разбор

В этом ориентированном графе путь 0→3 есть, а 3→0 нет. Добавление обратного пути без изменения входа нарушает договор. Изолированная вершина тоже принадлежит графу и должна присутствовать в представлении, даже если у неё пустой список соседей.

Источники: Princeton: undirected graphs и BFS, directed graphs.