Нерекурсивный поиск по глубине (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 и параметр


Я думаю, что мне удалось написать гораздо более простой псевдокод.

но сначала несколько замечаний, чтобы сделать вещи немного прояснить:

  1. v. pDescendant-указатель на потомок вершины, заданный его списком смежности.
  2. в списке смежности, я предположил, что каждый элемент имеет поле "pNext", который указывает на следующий элемент в связанном списке.
  3. я использовал синтаксис C++, в основном "- > "и"&", чтобы подчеркнуть, что такое указатель и что такое не.
  4. стек.топ() возвращает указатель на первый элемент стека. но в отличие от 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 за исключением корневого узла. Это означает, что вложенная модель набора атрибуты .ds и .fs не будет правильным. (Если вы не знаете, что .ds и .fs, прочтите эту статью, это было очень поучительно для меня в те дни. Статья 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 (второе и третье издание)