EssayAI
Блог
Блог
Математика и алгоритмы

Обход графа в глубину: как работает DFS по шагам

24 сентября 2026Время чтения: 10 минут
#обход графа в глубину#DFS#алгоритмы на графах#стек вызовов#поиск цикла
Обход графа в глубину: как работает DFS по шагам

Обход графа в глубину (depth-first search, DFS) перебирает вершины по принципу «иди, пока идётся»: из текущей вершины алгоритм уходит по первому же непройденному ребру как можно дальше и пятится назад только тогда, когда впереди всё уже посещено. Из этой простой дисциплины вырастают почти все структурные алгоритмы на графах: проверка ацикличности, поиск компонент, мосты и точки сочленения, сильно связные компоненты. Разберём обход на одном конкретном графе из восьми вершин и девяти рёбер, а в калькуляторе ниже прокрутите ту же трассировку по шагам.

Что делает обход графа в глубину

Граф задан списками смежности, соседи в каждом списке перебираются по алфавиту:

A: B, D     E: D, F
B: C        F: E
C: D        G: H
D: B

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

Из вершины AA алгоритм спускается по цепочке A→B→C→DA \to B \to C \to D и только там упирается: единственный сосед вершины DD это BB, а она уже серая. Вершины закрываются в обратном порядке: DD, CC, BB, AA. Ключевая особенность обхода в глубину в том, что он не идёт «кольцами» вокруг стартовой вершины, как обход в ширину, а сразу проваливается в одну ветку до упора. Именно поэтому он даёт информацию о структуре графа, а не о расстояниях.

Одного запуска почти никогда не хватает: из AA недостижимы ни EE, ни GG. Поэтому снаружи стоит цикл по всем вершинам, который запускает обход из каждой ещё не найденной. В нашем графе он сработает трижды, и получится лес обхода в глубину из трёх деревьев с корнями AA, EE, GG.

Рекурсия и явный стек: две записи одного алгоритма

Рекурсивная запись занимает шесть строк:

dfs(u):
    color[u] = серый
    for w in adj[u]:
        if color[w] == белый:
            dfs(w)
    color[u] = чёрный

Рекурсия здесь не украшение: стек вызовов интерпретатора хранит ровно текущий путь от корня дерева обхода до активной вершины. Его глубина равна длине этого пути, и в худшем случае (граф-цепочка) она равна числу вершин. Отсюда практическая опасность: на графе из миллиона вершин рекурсивный DFS почти наверняка упадёт с переполнением стека, и обход переписывают через явный стек - ту же структуру «последним пришёл, первым вышел», только в своей памяти, а не в системной (как устроен стек и его операции).

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

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

Три цвета вершин, время входа и время выхода

Стандартная реализация хранит для каждой вершины цвет и две метки времени. Белая вершина ещё не найдена, серая найдена, но её обход не завершён (она лежит в стеке), чёрная завершена полностью. Счётчик времени увеличивается на единицу при каждом событии: tin(v)t_{in}(v) ставится при входе, tout(v)t_{out}(v) при выходе. Всего событий ровно 2V2V, поэтому в нашем графе время пробегает значения от 1 до 16.

Интервалы времени вершин: интервал потомка целиком лежит внутри интервала предка, а интервалы разных ветвей не пересекаются
Интервалы времени вершин: интервал потомка целиком лежит внутри интервала предка, а интервалы разных ветвей не пересекаются

Интервалы [tin(v),tout(v)][t_{in}(v), t_{out}(v)] подчиняются теореме о скобках: для любых двух вершин интервалы либо вложены один в другой, либо не пересекаются вовсе. Частичного перекрытия не бывает - ровно как у правильно расставленных скобок. Вложенность означает родство: DD с интервалом (4,5)(4, 5) лежит внутри CC с интервалом (3,6)(3, 6), значит DD - потомок CC в дереве обхода. Непересекающиеся интервалы AA (1,8)(1, 8) и EE (9,12)(9, 12) говорят, что вершины лежат в разных ветвях или даже в разных деревьях леса.

Из длины интервала бесплатно получается размер поддерева:

∣Tv∣=tout(v)−tin(v)+12|T_v| = \frac{t_{out}(v) - t_{in}(v) + 1}{2}

Для корня AA это (8−1+1)/2=4(8 - 1 + 1)/2 = 4 вершины, и действительно его поддерево - это AA, BB, CC, DD. Проверка «является ли uu предком vv» тоже сводится к одному сравнению: tin(u)<tin(v)t_{in}(u) < t_{in}(v) и tout(v)<tout(u)t_{out}(v) < t_{out}(u). На этом приёме держатся запросы к деревьям и методы вроде разбиения на цепочки.

Четыре класса рёбер

Обход в глубину раскладывает все рёбра ориентированного графа на четыре класса. Класс определяется цветом конца ребра в момент, когда алгоритм его рассматривает.

Классификация девяти рёбер графа после обхода: пять древесных, два обратных, одно прямое и одно перекрёстное
Классификация девяти рёбер графа после обхода: пять древесных, два обратных, одно прямое и одно перекрёстное
  • Древесное: конец белый, по этому ребру обход и уходит вглубь. Таких рёбер ровно VV минус число деревьев леса, у нас 8−3=58 - 3 = 5.
  • Обратное: конец серый, то есть ребро ведёт в предка, который ещё не завершён. У нас это D→BD \to B и F→EF \to E.
  • Прямое: конец чёрный и является потомком текущей вершины (tin(u)<tin(w)t_{in}(u) < t_{in}(w)). Ребро A→DA \to D ведёт к уже завершённому потомку.
  • Перекрёстное: конец чёрный, но лежит в другой ветке или другом дереве (tin(u)>tin(w)t_{in}(u) > t_{in}(w)). Ребро E→DE \to D уходит в уже закрытое первое дерево.

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

Поиск цикла и компоненты связности

Из классификации сразу следуют два самых частых применения обхода. Третье применение выносится в отдельную задачу: если граф ациклический, порядок выхода из вершин, развёрнутый задом наперёд, даёт топологическую сортировку.

Цикл. Ориентированный граф ацикличен тогда и только тогда, когда обход в глубину не нашёл ни одного обратного ребра. Доказательство в одну сторону очевидно: обратное ребро u→wu \to w вместе с путём из ww в uu по древесным рёбрам замыкает цикл. В обратную сторону: если цикл есть, то первая вершина цикла, в которую зашёл обход, остаётся серой, пока обход не вернётся к ней по циклу, и ребро в неё окажется обратным. В нашем графе обратных рёбер два, и один из циклов читается прямо со схемы: B→C→D→BB \to C \to D \to B. Чтобы выписать сам цикл, достаточно запомнить массив предков и подняться по нему от uu до ww.

Компоненты. В неориентированном графе каждый запуск обхода из внешнего цикла накрывает ровно одну компоненту связности, поэтому число компонент равно числу запусков. В нашем графе их две: {A,B,C,D,E,F}\{A, B, C, D, E, F\} и {G,H}\{G, H\}. В ориентированном случае число деревьев леса зависит от порядка перебора вершин и компонентой не является: там нужны более тонкие схемы вроде алгоритма Тарьяна для сильно связных компонент, который тоже построен на одном проходе обхода в глубину и на временах входа.

Сложность обхода: почему O(V + E)

Каждая вершина становится серой ровно один раз, значит рекурсивный вызов делается VV раз. Ровно та же оценка выходит и у обхода в ширину: отличаются они порядком посещения и памятью, а не асимптотикой. Каждый список смежности просматривается ровно один раз целиком, значит суммарно рассматривается EE рёбер (в неориентированном графе каждое ребро попадается дважды, что даёт 2E2E и ту же асимптотику). Итого:

T(V,E)=O(V+E)T(V, E) = O(V + E)

Для нашего графа это 8+9=178 + 9 = 17 элементарных действий. Оценка линейна именно потому, что граф хранится списками смежности. При хранении матрицей смежности просмотр соседей одной вершины стоит O(V)O(V), и обход деградирует до O(V2)O(V^2). Память под цвета и времена - O(V)O(V), плюс до O(V)O(V) на стек.

Частые ошибки

  • Два цвета вместо трёх. С флагом «посещено» обход всё ещё обойдёт граф, но отличить обратное ребро от прямого или перекрёстного уже не получится, и проверка на цикл сломается: она начнёт находить «цикл» на ациклическом графе A→BA \to B, A→CA \to C, B→CB \to C.
  • Пометка при добавлении в стек. В итеративной версии вершину нужно красить в серый при извлечении, иначе она попадёт в стек по каждому входящему ребру и времена собьются.
  • Ребро к родителю в неориентированном графе. Его нельзя считать обратным: иначе любое ребро объявит цикл. Пропускают по номеру ребра, а не по имени вершины, иначе кратные рёбра дадут ложный ответ.
  • Рекурсия на больших данных. Глубина стека равна длине самого длинного пути в дереве обхода; на цепочке из 10610^6 вершин рекурсия падает. Итеративная версия или увеличенный лимит стека обязательны.
  • Вывод о расстояниях. Порядок обхода в глубину ничего не говорит о длине пути: до вершины DD алгоритм добрался за три шага, хотя есть прямое ребро A→DA \to D. Кратчайшие пути ищут другими алгоритмами, например алгоритмом Дейкстры.

FAQ

Чем обход в глубину отличается от обхода в ширину? Дисциплиной хранения ещё не обработанных вершин: в глубину работает стек, в ширину очередь. Отсюда разные результаты: DFS даёт структурную информацию (времена, классификация рёбер, циклы), а обход в ширину - расстояния в рёбрах от старта. Асимптотика у обоих O(V+E)O(V + E).

Единственный ли порядок обхода у графа? Нет. Он зависит от стартовой вершины и от порядка соседей в списках смежности. Времена входа и выхода, число деревьев в лесу и даже классы отдельных рёбер меняются - в калькуляторе выше это видно при смене старта. Неизменным остаётся факт наличия или отсутствия обратных рёбер, то есть ответ про ацикличность.

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

Коротко

Обход графа в глубину идёт по рёбрам вглубь до упора и возвращается по стеку вызовов, храня для каждой вершины цвет и пару меток времени tint_{in} и toutt_{out}. Интервалы времени вкладываются друг в друга как скобки, из чего мгновенно читаются родство вершин и размеры поддеревьев. Рёбра распадаются на древесные, обратные, прямые и перекрёстные, причём наличие обратного ребра равносильно наличию цикла, а число запусков внешнего цикла в неориентированном графе равно числу компонент связности. Всё это обходится в O(V+E)O(V + E) при хранении графа списками смежности.

Доверьте текст нейросети EssayAI

Открыть EssayAI

Бесплатно, на русском языке и без VPN

Читайте также

Алгоритм Тарьяна: поиск компонент связности орграфа

Алгоритм Тарьяна: поиск компонент связности орграфа

Алгоритм Тарьяна находит сильно связные компоненты орграфа за один проход DFS. Разбираем идею с disc и low, псевдокод и пример, чтобы вы научились раскладывать граф на SCC.

28 января 20269 минут
Агрегатные функции SQL: COUNT, SUM, AVG и GROUP BY

Агрегатные функции SQL: COUNT, SUM, AVG и GROUP BY

Как работают агрегатные функции SQL: COUNT, SUM, AVG, MIN и MAX, группировка GROUP BY, порядок выполнения запроса, разница HAVING и WHERE и поведение NULL внутри агрегата.

24 сентября 20269 минут
Интегральный синус Si(x): ряд Тейлора и максимум

Интегральный синус Si(x): ряд Тейлора и максимум

Интегральный синус Si(x): почему интеграл sin t / t не берётся в элементарных функциях, разложение в ряд, предел π/2, максимум Si(π) и выброс Гиббса. С калькулятором.

24 сентября 202610 минут
Куча как структура данных: массив и просеивание

Куча как структура данных: массив и просеивание

Куча как структура данных: свойство кучи, хранение в обычном массиве и индексы 2i+1 и 2i+2, просеивание sift-up и sift-down, построение за O(n) и приоритетная очередь.

24 сентября 202610 минут
Линейная зависимость векторов: критерии и примеры

Линейная зависимость векторов: критерии и примеры

Что такое линейная зависимость векторов, как проверить её определителем и рангом матрицы, чем коллинеарность отличается от компланарности и как всё это связано с базисом.

24 сентября 202610 минут
Линейное уравнение второго порядка: общее решение

Линейное уравнение второго порядка: общее решение

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

24 сентября 202612 минут