Нерекурсивный поиск по глубине (DFS) с использованием стека
Ok это мой первый пост о переполнении стека, который я читал некоторое время и действительно восхищаюсь сайтом. Я надеюсь, что это то, что будет приемлемо спросить. Поэтому я читал введение в алгоритмы (Cormen. MIT Press) До конца, и я до алгоритмов графика. Я изучал формальные алгоритмы, разработанные для первого поиска по ширине и глубине, очень подробно.
вот psuedo-код, заданный для глубины поиск:
DFS(G)
-----------------------------------------------------------------------------------
1 for each vertex u ∈ G.V
2 u.color ← WHITE // paint all vertices white; undiscovered
3 u.π ← NIL
4 time ← 0 // global variable, timestamps
5 for each vertex u ∈ G.V
6 if u.color = WHITE
7 DFS-VISIT(G,u)
DFS-VISIT(G, u)
-----------------------------------------------------------------------------------
1 u.color ← GRAY // grey u; it is discovered
2 time ← time + 1
3 u.d ← time
4 for each v ∈ G.Adj[u] // explore edge (u,v)
5 if v.color == WHITE
6 v.π ← u
7 DFS-VISIT(G,v)
8 u.color ← BLACK // blacken u; it is finished
9 time ← time + 1
10 u.f ← time
этот алгоритм рекурсивен и исходит из нескольких источников (будет обнаружена каждая вершина в несвязанном графе). Он имеет несколько свойств,которые могут отсутствовать в большинстве языковых реализаций. Он различает 3 разных цветов вершин. Сначала он окрашивает их все в белый цвет, затем, когда они "обнаружены" (посещаются в DFS-VISIT), они затем окрашиваются в серый цвет. Алгоритм DFS-VISIT запускает цикл, рекурсивно вызывающий себя на список смежности текущей вершины и только рисует вершину черным, когда у нее больше нет ребер к любому белому узлу.
также поддерживаются два других атрибута каждой вершины u.d и u.f-отметки времени, когда вершина была обнаружена (окрашена в серый цвет) или когда вершина закончена (окрашена в черный цвет). Первый раз, когда узел окрашен, он имеет отметку времени одного и увеличивается до следующего целочисленного значения для каждого раза, когда окрашен другой (будь то серый или черный). u.π также поддерживается, что является просто указателем на узел, из которого был обнаружен u.
Algorithm Non-Recursive-DFS(G)
-----------------------------------------------------------------------------------
1 for each vertex u ∈ G.V
2 u.color ← WHITE
3 u.π ← NIL
4 time = 0
5 for each vertex u ∈ G.V
6 if u.color = WHITE
7 u.color ← GRAY
8 time ← time + 1
9 u.d ← time
7 push(u, S)
8 while stack S not empty
9 u ← pop(S)
10 for each vertex v ∈ G.Adj[u]
11 if v.color = WHITE
12 v.color = GRAY
13 time ← time + 1
14 v.d ← time
15 v.π ← u
16 push(v, S)
17 u.color ← BLACK
18 time ← time + 1
19 u.f ← time
* редактирование 10/31/12 * это смущает, что мой алгоритм был неправильным так долго, он будет работать в большинстве случаев, но не во всех. Я только что получил популярный значок вопроса для вопроса, и я увидел, где Ирфи заметил проблему в своем ответе ниже, так что вот где кредит. Я просто фиксирую его здесь для всех, кто нуждается в этом в будущем.
тут кто-нибудь видит недостаток с вышеуказанным алгоритмом? Я далеко не эксперт по теории графов, но я думаю, что у меня есть довольно хорошее понимание рекурсии и итерации, и я считаю, что это должно работать одинаково. Я ищу, чтобы сделать его функционально эквивалентным рекурсивному алгоритму. Он должен поддерживать все атрибуты первого алгоритма, даже если они не нужны.
когда я начал писать его, я не думал, что у меня будет тройной цикл, но так получилось. Я видел других итеративные алгоритмы, когда я смотрел вокруг Google, которые имеют только дважды вложенный цикл, однако, они, похоже, не исходят из нескольких источников. (т. е. они не будут обнаруживать каждую вершину несвязанного графа)
8 ответов
оба алгоритма хорошо. Второй - это прямой перевод с рекурсивного на стековый. Все мутирующие состояния хранятся в стеке. G
никогда не изменяется во время выполнения алгоритма.
алгоритмы будут создавать связующее дерево для каждой отключенной области на основе порядка, в котором алгоритм посетил каждый узел. Деревья будут представлены как со ссылками на родительские узлы (u.π
), и как сегмент деревья (u.d
и u.f
).
у ребенка будет ссылка на его родительский узел (или NULL
если это корень), а также имеющие ассортимента (child.d .. child.f
) содержится в пределах родительского диапазона.
parent.d < child.d < child.f < parent.f
child.π = parent
Edit: я нашел ошибку в переводе. На самом деле вы не толкаете текущее состояние в стек, а будущий аргумент метода. Кроме того, вы не раскрашиваете всплывающие узлы GRAY
и параметр
Я думаю, что мне удалось написать гораздо более простой псевдокод.
но сначала несколько замечаний, чтобы сделать вещи немного прояснить:
- v. pDescendant-указатель на потомок вершины, заданный его списком смежности.
- в списке смежности, я предположил, что каждый элемент имеет поле "pNext", который указывает на следующий элемент в связанном списке.
- я использовал синтаксис C++, в основном "- > "и"&", чтобы подчеркнуть, что такое указатель и что такое не.
- стек.топ() возвращает указатель на первый элемент стека. но в отличие от pop (), он не удаляет его.
алгоритм основан на следующем наблюдении: вершина помещается в стек при посещении. и убрали (выскочили) только тогда, когда мы закончили изучение (очернение) всех его потомков.
DFS(G)
1. for all vertices v in G.V do
2. v.color = WHITE; v.parent = NIL; v.d = NIL; v.f = NIL; v.pDescendant = adj[v].head
3. time = 0
4. Initialize Stack
5. for all vertices v in G.V s.t. v.color == WHITE do
6. time++
7. Stack.push(&v)
8. v.color = GRAY
9. v.d = time
10. DFS-ITERATIVE(G,v)
DFS-ITERATIVE(G,s)
1. while Stack.Empty() == FALSE do
2. u = Stack.top();
3. if u.pDescendant == NIL // no Descendants to u || no more vertices to explore
4. u.color = BLACK
5. time++
6. u.f = time
7. Stack.pop()
8. else if (u.pDescendant)->color == WHITE
9. Stack.push(u.pDescendant)
10. time++
10. (u.pDescendant)->d = time
11. (u.pDescendant)->color = GRAY
12. (u.pDescendant)->parent = &u
12. u.pDescendant= (u.pDescendant)->pNext // point to next descendant on the adj list
13. else
14. u.pDescendant= (u.pDescendant)->pNext // not sure about the necessity of this line
int stackk[100];
int top=-1;
void graph::dfs(int v){
stackk[++top]=v;
// visited[v]=1;
while(top!=-1){
int x=stackk[top--];
if(!visited[x])
{visited[x]=1;
cout<<x<<endl;
}
for(int i=V-1;i>=0;i--)
{
if(!visited[i]&&adj[x][i])
{ //visited[i]=1;
stackk[++top]=i;
}
}
}
}
void graph::Dfs_Traversal(){
for(int i=0;i<V;i++)
visited[i]=0;
for(int i=0;i<V;i++)
if(!visited[i])
g.dfs(i);
вот код на C++.
class Graph
{
int V; // No. of vertices
list<int> *adj; // Pointer to an array containing adjacency lists
public:
Graph(int V); // Constructor
void addEdge(int v, int w); // function to add an edge to graph
void BFS(int s); // prints BFS traversal from a given source s
void DFS(int s);
};
Graph::Graph(int V)
{
this->V = V;
adj = new list<int>[V]; //list of V list
}
void Graph::addEdge(int v, int w)
{
adj[v].push_back(w); // Add w to v’s list.
}
void Graph::DFS(int s)
{
//mark unvisited to each node
bool *visited = new bool[V];
for(int i = 0; i<V; i++)
visited[i] =false;
int *d = new int[V]; //discovery
int *f = new int[V]; //finish time
int t = 0; //time
//now mark current node to visited
visited[s] =true;
d[s] = t; //recored the discover time
list<int> stack;
list<int>::iterator i;
stack.push_front(s);
cout << s << " ";
while(!(stack.empty()))
{
s= stack.front();
i = adj[s].begin();
while ( (visited[*i]) && (i != --adj[s].end()) )
{
++i;
}
while ( (!visited[*i]) )
{
visited[*i] =true;
t++;
d[*i] =t;
if (i != adj[s].end())
stack.push_front(*i);
cout << *i << " ";
i = adj[*i].begin();
}
s = stack.front();
stack.pop_front();
t++;
f[s] =t;
}
cout<<endl<<"discovery time of the nodes"<<endl;
for(int i = 0; i<V; i++)
{
cout<< i <<" ->"<< d[i] <<" ";
}
cout<<endl<<"finish time of the nodes"<<endl;
for(int i = 0; i<V; i++)
{
cout<< i <<" ->"<< f[i] <<" ";
}
}
int main()
{
// Create a graph given in the above diagram
Graph g(5);
g.addEdge(0, 1);
g.addEdge(0, 4);
g.addEdge(1, 4);
g.addEdge(1, 2);
g.addEdge(1, 3);
g.addEdge(3, 4);
g.addEdge(2, 3);
cout << endl<<"Following is Depth First Traversal (starting from vertex 0) \n"<<endl;
g.DFS(0);
return 0;
}
простой итеративный DFS. Вы также можете увидеть время обнаружения и время окончания. Любые сомнения, пожалуйста, прокомментируйте. Я включил достаточно комментариев,чтобы понять код.
у вас есть серьезный недостаток в вашем нерекурсивном коде.
после того, как вы проверите, есть ли v
is WHITE
, вы никогда не отметить его GRAY
перед нажатием, так что это можно рассматривать как WHITE
снова и снова из других неявленных узлов, в результате чего несколько ссылок на это v
узел проталкивается в стек. Это потенциально катастрофический недостаток (может вызвать бесконечные петли или что-то в этом роде).
кроме того, вы не устанавливаете .d
за исключением корневого узла. Это означает, что вложенная модель набора атрибуты .d
s и .f
s не будет правильным. (Если вы не знаете, что .d
s и .f
s, прочтите эту статью, это было очень поучительно для меня в те дни. Статья left
ваш .d
и right
ваш .f
.)
ваш внутренний if
в основном должен быть таким же, как внешний if
минус циклы, плюс родительская ссылка. То есть:
11 if v.color = WHITE
++ v.color ← GRAY
++ time ← time + 1
++ v.d ← time
12 v.π ← u
13 push(v, S)
исправьте это, и он должен истинный эквивалент.
в нерекурсивной версии нам нужен другой цвет, который отражает состояние в рекурсивном стеке. Итак, мы добавим color=RED, чтобы указать, что все дочерние элементы узла были помещены в стек. Я также предположу, что стек имеет метод peek () (который в противном случае может быть смоделирован с помощью pop и немедленного нажатия)
Итак, с этим добавлением обновленная версия оригинального сообщения должна выглядеть так:
for each vertex u ∈ G.V
u.color ← WHITE
u.π ← NIL
time = 0
for each vertex u ∈ G.V
if u.color = WHITE
u.color ← GRAY
time ← time + 1
u.d ← time
push(u, S)
while stack S not empty
u ← peek(S)
if u.color = RED
//means seeing this again, time to finish
u.color ← BLACK
time ← time + 1
u.f ← time
pop(S) //discard the result
else
for each vertex v ∈ G.Adj[u]
if v.color = WHITE
v.color = GRAY
time ← time + 1
v.d ← time
v.π ← u
push(v, S)
u.color = RED
I used Adjacency Matrix:
void DFS(int current){
for(int i=1; i<N; i++) visit_table[i]=false;
myStack.push(current);
cout << current << " ";
while(!myStack.empty()){
current = myStack.top();
for(int i=0; i<N; i++){
if(AdjMatrix[current][i] == 1){
if(visit_table[i] == false){
myStack.push(i);
visit_table[i] = true;
cout << i << " ";
}
break;
}
else if(!myStack.empty())
myStack.pop();
}
}
}
Я считаю, что есть по крайней мере один случай, когда рекурсивные и стековые версии функционально не эквивалентны. Рассмотрим случай треугольника - вершины A, B и C связаны друг с другом. Теперь, с рекурсивным DFS, граф-предшественник, который можно было бы получить с источником A, будет либо A->B->C, либо A->C->B ( A - >B подразумевает, что A является родителем B в глубине первого дерева).
однако, если вы используете версию стека DFS, родители обоих B и C всегда будут записываться как A. никогда не может быть так, что родителем B является C или наоборот (что всегда имеет место для рекурсивных DFS). Это потому, что при изучении списка смежности любой вершины (здесь A) мы нажимаем все члены списка смежности (здесь B и C) за один.
Это может стать актуальным, если вы попытаетесь использовать DFS для поиска точек сочленения в графе[1]. Например, следующее высказывание справедливо только если мы используем рекурсивную версию ДФХ.
корневая вершина является точкой сочленения, если и только если она имеет хотя бы двое детей в глубине первого дерева.
в треугольнике, очевидно, нет точки сочленения, но stack-DFS по-прежнему дает два дочерних элемента для любой исходной вершины в дереве глубины (A имеет дочерние элементы B и C). Только если мы создадим первое дерево глубины с помощью рекурсивного DFS, то приведенное выше утверждение будет истинным.
[1] Введение в алгоритмы, CLRS - Задача 22-2 (второе и третье издание)