Поиск в глубину ( DFS ) — это алгоритм обхода или поиска по древовидным или графовым структурам данных. Алгоритм начинается с корневого узла (выбирая произвольный узел в качестве корневого узла в случае графа) и исследует как можно дальше каждую ветвь перед возвратом. Дополнительная память, обычно стек , необходима для отслеживания узлов, обнаруженных до сих пор вдоль указанной ветви, что помогает в возврате графа.
Вариант поиска в глубину был исследован в 19 веке французским математиком Шарлем Пьером Тремо [1] как стратегия решения лабиринтов . [2] [3]
Анализ времени и пространства DFS различается в зависимости от области его применения. В теоретической информатике DFS обычно используется для обхода всего графа и занимает время , [4] где — число вершин и число ребер . Это линейно по размеру графа. В этих приложениях он также использует пространство в худшем случае для хранения стека вершин на текущем пути поиска, а также набора уже посещенных вершин. Таким образом, в этой настройке временные и пространственные ограничения такие же, как и для поиска в ширину , и выбор того, какой из этих двух алгоритмов использовать, зависит не столько от их сложности, сколько от различных свойств упорядочения вершин, которые производят два алгоритма.
Для приложений DFS в отношении определенных областей, таких как поиск решений в области искусственного интеллекта или веб-сканирования, граф, который необходимо пройти, часто либо слишком велик для посещения целиком, либо бесконечен (DFS может страдать от невыполнения ). В таких случаях поиск выполняется только на ограниченную глубину ; из-за ограниченных ресурсов, таких как память или дисковое пространство, структуры данных обычно не используются для отслеживания набора всех ранее посещенных вершин. Когда поиск выполняется на ограниченную глубину, время по-прежнему линейно с точки зрения количества расширенных вершин и ребер (хотя это число не совпадает с размером всего графа, поскольку некоторые вершины могут быть просмотрены более одного раза, а другие — нет), но пространственная сложность этого варианта DFS пропорциональна только пределу глубины и, как следствие, намного меньше пространства, необходимого для поиска на ту же глубину с использованием поиска в ширину. Для таких приложений DFS также гораздо лучше подходит для эвристических методов выбора вероятно выглядящей ветви. Когда соответствующий предел глубины неизвестен априори, итеративное углубление поиска в глубину применяет DFS многократно с последовательностью увеличивающихся пределов. В режиме анализа искусственного интеллекта, с фактором ветвления больше единицы, итеративное углубление увеличивает время выполнения только на постоянный множитель по сравнению со случаем, в котором правильный предел глубины известен из-за геометрического роста числа узлов на уровень.
DFS также может использоваться для сбора выборки узлов графа. Однако неполный DFS, подобно неполному BFS , смещен в сторону узлов высокой степени .
Для следующего графика:
поиск в глубину, начинающийся с узла A, предполагая, что левые ребра в показанном графе выбираются раньше правых ребер, и предполагая, что поиск запоминает ранее посещенные узлы и не будет их повторять (так как это небольшой граф), посетит узлы в следующем порядке: A, B, D, F, E, C, G. Ребра, пройденные в этом поиске, образуют дерево Тремо , структуру, имеющую важные приложения в теории графов . Выполнение того же поиска без запоминания ранее посещенных узлов приводит к посещению узлов в порядке A, B, D, F, E, A, B, D, F, E и т. д. навсегда, попадая в цикл A, B, D, F, E и никогда не достигая C или G.
Итеративное углубление — один из методов, позволяющий избежать этого бесконечного цикла и позволяющий охватить все узлы.
Результат поиска в глубину графа можно удобно описать в терминах остовного дерева вершин, достигнутых во время поиска. На основе этого остовного дерева ребра исходного графа можно разделить на три класса: прямые ребра , которые указывают от узла дерева к одному из его потомков, обратные ребра , которые указывают от узла к одному из его предков, и перекрестные ребра , которые не делают ни того, ни другого. Иногда древесные ребра , ребра, которые принадлежат самому остовному дереву, классифицируются отдельно от прямых ребер. Если исходный граф неориентированный, то все его ребра являются древесными ребрами или обратными ребрами.
Также возможно использовать поиск в глубину для линейного упорядочивания вершин графа или дерева. Существует четыре возможных способа сделать это:
Для бинарных деревьев дополнительно существует упорядочение по входу и обратное упорядочение по входу .
Например, при поиске по направленному графу ниже, начиная с узла A, последовательность обходов — это либо ABDBACA, либо ACDCABA (выбор первого посещения B или C из A зависит от алгоритма). Обратите внимание, что повторные посещения в форме возврата к узлу для проверки того, есть ли у него еще непосещенные соседи, включены сюда (даже если обнаружено, что их нет). Таким образом, возможными предварительными порядками являются ABDC и ACDB, тогда как возможными поздними порядками являются DBCA и DCBA, а возможными обратными поздними порядками являются ACBD и ABC D.
Обратный постпорядок производит топологическую сортировку любого направленного ациклического графа . Этот порядок также полезен в анализе потока управления , поскольку он часто представляет собой естественную линеаризацию потоков управления. Граф выше может представлять поток управления в фрагменте кода ниже, и естественно рассматривать этот код в порядке ABCD или ACBD, но неестественно использовать порядок ABDC или ACD B.
если ( А ) то { Б} еще { С}Д
Рекурсивная реализация DFS: [5]
процедура DFS( G , v ) помечает v как обнаруженную для всех направленных ребер от v до w, которые находятся в G . adjacentEdges( v ) делает, если вершина w не помечена как обнаруженная, то рекурсивно вызывает DFS( G , w )
Нерекурсивная реализация DFS с наихудшей сложностью пространства , с возможностью дублирования вершин в стеке: [6]
процедура DFS_iterative( G , v ) — пусть S будет стеком S .push( v ) пока S не пусто do v = S .pop() если v не помечен как обнаруженный, то пометить v как обнаруженный для всех ребер от v до w в G .adjacentEdges( v ) do S .push( w )
Эти две вариации DFS посещают соседей каждой вершины в порядке, противоположном друг другу: первый сосед v , посещенный рекурсивной вариацией, является первым в списке смежных ребер, тогда как в итеративной вариации первый посещенный сосед является последним в списке смежных ребер. Рекурсивная реализация посетит узлы из примера графа в следующем порядке: A, B, D, F, E, C, G. Нерекурсивная реализация посетит узлы как: A, E, F, B, D, C, G.
Нерекурсивная реализация похожа на поиск в ширину, но отличается от него двумя способами:
Если G — дерево , замена очереди алгоритма поиска в ширину на стек даст алгоритм поиска в глубину. Для общих графов замена стека итеративной реализации поиска в глубину на очередь также даст алгоритм поиска в ширину, хотя и несколько нестандартный. [7]
Другая возможная реализация итеративного поиска в глубину использует стек итераторов списка соседей узла вместо стека узлов. Это дает тот же обход, что и рекурсивный DFS. [8]
процедура DFS_iterative( G , v ) — пусть S будет стеком пометить v как обнаруженный S .push(итератор G .adjacentEdges( v )) пока S не пусто do if S .peek().hasNext() then w = S .peek().next() если w не помечен как обнаруженный , то пометить w как обнаруженный S .push(итератор G .adjacentEdges( w )) else S .pop()
Алгоритмы, использующие поиск в глубину в качестве строительного блока, включают:
Вычислительная сложность DFS была исследована Джоном Рейфом . Точнее, если задан граф , пусть будет порядком, вычисленным стандартным рекурсивным алгоритмом DFS. Этот порядок называется лексикографическим порядком поиска в глубину. Джон Рейф рассмотрел сложность вычисления лексикографического порядка поиска в глубину, заданного графом и источником. Решающая версия задачи (проверка того, встречается ли некоторая вершина u перед некоторой вершиной v в этом порядке) является P -полной , [12] что означает, что это «кошмар для параллельной обработки ». [13] : 189
Поиск в глубину (не обязательно лексикографический) может быть вычислен рандомизированным параллельным алгоритмом в классе сложности RNC . [14] По состоянию на 1997 год оставалось неизвестным, можно ли построить обход в глубину с помощью детерминированного параллельного алгоритма в классе сложности NC . [15]