Пришла пора рассмотреть первые алгоритмы на графах. К классическим алгоритмам относятся обходы графов. Под обходом графа обычно понимают процесс систематического просмотра всех вершин или рёбер графа, чтобы найти некоторые вершины, удовлетворяющие определённым условиям. Мы рассмотрим обход в ширину и обход в глубину.

Обход в глубину заключается в систематическом просмотре вершин графа и прохождении его ветвями. Иными словами, идея поиска в глубину — когда возможные пути по рёбрам, выходящим из вершин, разветвляются, нужно сначала полностью исследовать одну ветку и только потом переходить к другим веткам (если они останутся нерассмотренными).

Опишем алгоритм поиска в глубину:

  • Шаг 1. Все вершины графа отмечаем, как не посещенные. Выбирается первая вершина и помечается как посещённая.
  • Шаг 2. Для последней помеченной как посещённой вершины выбирается смежная вершина, которая первая помеченная как не посещенная, и ей присваивается значение посещённой. Если таких вершин нет, то берётся предыдущая помеченная вершина.
  • Шаг 3. Повторяем шаг 2 до тех пор, пока все вершины не будут помечены как посещённые.

Пример реализации приведён ниже.

DFS(graph, v, used):
    used[v] = 1
    for (var u : graph[v])
        if (!used[u])
           DFS(graph, u, used)

✒️ Упражнение:
Попробуйте выполнить алгоритм поиска в глубину пошагово для графа.

Обратите внимание, сейчас мы посмотрели на рекурсивную реализацию. Конечно, преимущество использования рекурсивного подхода заключается в простоте его написания, однако, рекурсивный подход имеет свои ограничения.

Можно переписать алгоритм поиска в глубину с использованием особых структур данных. Например, стека.

Опишем алгоритм поиска в глубину в нерекурсивной форме:

  • Шаг 1. Все вершины графа отмечаем, как не посещённые. Выбирается первая вершина и помечается как посещённая. Эту вершину кладем в контейнер — стек.
  • Шаг 2. Пока стек не пустой:
    • Извлекаем последнюю добавленную вершину.
    • Просматриваем все смежные с ней не посещённые вершины и помещаем их в стек.
      Порядок выхода вершин из стека и будет порядком обхода вершин графа.

Пример работы не рекурсивного алгоритма можно посмотреть на анимации.

algosy

Пример реализации приведён ниже.

DFS(graph, v, used):
    stack q
    q.push(v)
    used[v] = 1
    while(!q.empty())
      v = q.front()
      q.pop()
      for (var to : graph[v]):
        if (!used[to]):
          used[to] = true
          q.push(to)

Ещё один способ обхода графа — обход в ширину. Основное его отличие в том, что сначала исследуются смежные вершины, а уже потом вершины на следующем уровне. Иначе говоря, сначала исследуются все вершины, смежные с начальной вершиной (вершина с которой начинается обход). Эти вершины находятся на расстоянии 1 от начальной. Затем исследуются все вершины на расстоянии 2 от начальной, затем все на расстоянии 3 и так далее. Обратим внимание, что при этом для каждой вершины сразу находятся длина кратчайшего маршрута от начальной вершины.

Опишем алгоритм поиска в ширину:

  • Шаг 1. Всем вершинам графа присваивается значение не посещённой. Выбирается первая вершина и помечается как посещённая и заносится в очередь.
  • Шаг 2. Посещается первая вершина из очереди (если она не помечена как посещённая). Все её соседние вершины заносятся в очередь. После этого она удаляется из очереди.
  • Шаг 3. Повторяется шаг 2 до тех пор, пока очередь не станет пустой.

Пример реализации алгоритма можно посмотреть на анимации

algosy

Пример реализации приведён ниже.

BFS(graph, v, used):
    queue q
    q.push(v)
    used[v] = 1
    while(!q.empty())
      v = q.front()
      q.pop()
      for (var to : graph[v]):
        if (!used[to]):
          used[to] = true
          q.push(to)

✒️ Упражнение:
Подумайте, какое отличие алгоритма поиска в ширину от алгоритма поиска в глубину?

Асимптотическая сложность алгоритма поиска в глубину и ширину — , где — число вершин, а — число рёбер и дуг. Обходы графов могут применяться для решения задач, связанных с теорией графов:

  • Волновой алгоритм поиска пути в лабиринте.
  • Волновая трассировка печатных плат.
  • Поиск компонент связности в графе.
  • Поиск кратчайшего пути между двумя узлами невзвешенного графа.
  • Поиск в пространстве состояний: нахождение решения задачи с наименьшим числом ходов, если каждое состояние системы можно представить вершиной графа, а переходы из одного состояния в другое — рёбрами графа.
  • Нахождение кратчайшего цикла в ориентированном невзвешенном графе.
  • Нахождение всех вершин и рёбер, лежащих на каком-либо кратчайшем пути между двумя вершинами.
  • Поиск увеличивающего пути в алгоритме Форда-Фалкерсона (алгоритм Эдмондса-Карпа).

Отмечайте параграфы как прочитанные чтобы видеть свой прогресс обучения

Вступайте в сообщество хендбука

Здесь можно найти единомышленников, экспертов и просто интересных собеседников. А ещё — получить помощь или поделиться знаниями.
Вступить
Сообщить об ошибке
Предыдущий параграф10.2. Представление графа в памяти компьютера
Следующий параграф10.4. Алгоритм нахождения компонент связности в графе