Графы для самых маленьких: DFS и BFS

от автора

Многие объекты окружающей действительности могут быть смоделированы с использованием графов: карта метро, лабиринт, знакомства в соцсетях, возможные ходы в настольной игре — все это графы. Самое простое и частое действие, которое можно сделать с графом — это перебрать его вершины в каком-то порядке. Под катом я бы хотел рассказать про два самых известных алгоритма на графах — об обходах в глубину и в ширину.

image

Немного теории

Итак, что же такое граф? Это множество объектов, называемых вершинами, и некоторое подмножество их пар, называемых ребрами. Например, вот так:

image

Граф может быть ориентированным или неориентированным — в первом случае предполагается, что ребро (u, v) — это не то же самое, что (v, u).

Очевидно, что добавлением обратных ребер можно превратить неориентированный граф в ориентированный, поэтому мы будем рассматривать только ориентированные графы.

Количество вершин в графе обозначается V, а ребер — E. Если вершин сильно меньше, чем ребер — значит, в графе большое количество вершин не соединены ни с чем и их можно легко выкинуть. Здесь мы будем предполагать, что V = O(E).

Обход графа — это перебор его вершин в определенном порядке.

DFS

imageDepth First Search, или обход в глубину, является самым интуитивно понятным: в каждый момент времени мы перемещаемся из текущей вершины в любого из ранее непосещенных соседей (и помечаем его посещенным), а если таких нет — идем обратно. Если дополнительно записывать в массив для каждой вершины номер той, из которой мы в нее пришли — можно найти какой-нибудь путь из точки А в точку Б.

И что-нибудь еще можно найти

Разумеется, с помощью DFS можно искать еще большое количество других полезных вещей, например, циклы, мосты или точки сочленения графа. Для этого бывает необходимо помечать вершину, обработка которой уже завершена, каким-нибудь особым маркером, отличным от того, которым помечаются обрабатываемые вершины.
Сложность алгоритма

Для каждой вершины и каждого ребра мы выполняем константное количество действий, следовательно, сложность алгоритма — O(V + E) = O(E).

И, конечно же, код

Здесь и далее предполагается, что граф хранится в vector<vector> edges, где для каждой вершины записаны номера всех ее соседей, а также объявлен и изначально заполнен нулями массив пометок вершин vector mark.

DFS

void DFS(int v) {     if (mark[v] > 0) // если вершина уже посещена, делать тут больше нечего     {         return;     }     // Совершаем какие-нибудь полезные действия, например, можно вывести номер вершины на экран     mark[v] = 1; // начали обработку     for (int i = 0; i < (int)edges[v].size(); ++i) // запускаемся от всех соседей     {         DFS(edges[v][i]);     }     mark[v] = 2; // завершили обработку } 

BFS

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

Часто бывает полезно использовать обход графа, в котором вершины перебираются в порядке возрастания их расстояния от исходной. Но как именно можно это сделать?

Сначала рассмотрим начальную вершину, и добавим всех ее соседей в очередь. Потом для каждого из этих соседей рассмотрим уже всех его соседей и опять добавим их в очередь. Такой алгоритм обойдет все вершины в порядке, показанном на рисунке.

Сложность алгоритма

Для каждой вершины и каждого ребра мы выполняем константное количество действий, следовательно, сложность алгоритма — O(V + E) = O(E).

И, конечно же, код

BFS

void BFS(int v) {     // Инициализация     queue<int> q;     q.push(v);     mark[v] = 1;     // Работа самого алгоритма     while (!q.empty())     {         // Берем вершину         v = q.front();         q.pop();         // Смотрим всех ее соседей         for (int i = 0; i < (int)edges[v].size(); ++i)         {             // И добавляем, если нужно             if (mark[edges[v][i]] == 0)             {                 markv[edges[v][i]] = mark[v] + 1;                 q.push(edges[v][i]);             }         }     } } 

PS. Нерекурсивный DFS

Есть теорема о том, что любой рекурсивный алгоритм можно переписать в нерекурсивном виде. Вот код DFS:

DFS

void DFS(int v) {     stack<int> q;     q.push(v);     mark[v] = 1;     while (!q.empty())     {         v = q.front();         q.pop();         for (int i = 0; i < (int)edges[v].size(); ++i)         {             if (mark[edges[v][i]] == 0)             {                 markv[edges[v][i]] = mark[v] + 1;                 q.push(edges[v][i]);             }         }     } } 

Ничего не напоминает?

ссылка на оригинал статьи http://habrahabr.ru/post/199304/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *