【问题标题】:Non-recursive Depth-First Search (DFS) Using a Stack使用堆栈的非递归深度优先搜索 (DFS)
【发布时间】:2012-04-26 22:29:26
【问题描述】:

好的,这是我在 Stack Overflow 上的第一篇文章,我已经阅读了一段时间,非常欣赏这个网站。我希望这是可以接受的问题。因此,我一直在阅读 Intro to Algorithms (Cormen. MIT Press),并且一直在学习图形算法。我一直在非常详细地研究为广度和深度优先搜索制定的正式算法。

这是深度优先搜索的伪代码:

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 是发现顶点(涂成灰色)或完成顶点(涂成黑色)的时间戳。第一次绘制节点时,它的时间戳为 1,每次绘制另一个节点时(无论是灰色还是黑色),它都会递增到下一个整数值。 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

* 2012 年 10 月 31 日编辑 * 很尴尬,我的算法已经错误了这么久,它在大多数情况下都可以工作,但不是全部。我刚刚为该问题获得了一个受欢迎的问题徽章,我看到 Irfy 在下面的答案中发现了问题,所以这就是功劳所在。我只是在这里为将来需要它的人修复它。

有人发现上述算法存在缺陷吗?到目前为止,我还不是图论方面的专家,但我认为我对递归和迭代有很好的掌握,我相信这应该也是一样的。我希望使它在功能上等同于递归算法。它应该保留第一个算法的所有属性,即使它们不需要。

当我开始写它时,我认为我不会有一个三重循环,但结果就是这样。当我环顾谷歌时,我看到了其他只有双重嵌套循环的迭代算法,但是,它们似乎并没有从多个来源进行。 (即他们不会发现未连接图的每个顶点)

【问题讨论】:

  • 请在第二个算法的第二个循环中更正if 语句后的缩进。理想情况下也是第一个 for 循环后的缩进
  • 刚才编辑了,原来是这个。
  • 非递归版本计算的完成时间会出错。 u.f
  • 完成时间计算错误,请更正。
  • CLRS 声明每个顶点最初都是白色的,直到第一次检查(发现)它时它变灰并且u.d 设置为新时间。它保持灰色,直到它的邻接列表被完全检查(在所有相邻节点上设置 u.d),此时它是黑色的,并且完成时间 u.f 设置为新时间。

标签: algorithm graph graph-algorithm depth-first-search


【解决方案1】:

两种算法都很好。第二个是从递归到基于堆栈的直接转换。所有变异状态都存储在堆栈中。 G 在算法执行期间永远不会改变。

算法将根据算法访问每个节点的顺序为每个断开连接的区域生成生成树。这些树将通过对父节点 (u.π) 的引用和段树(u.du.f)来表示。

子节点将引用其父节点(或 NULL,如果它是根节点),并且其范围 (child.d .. child.f) 包含在其父节点范围内。

parent.d < child.d < child.f < parent.f

child.π = parent

编辑:我发现翻译有误。您实际上并不是将当前状态推入堆栈,而是未来的方法参数。此外,您没有为弹出的节点GRAY 着色并设置f 字段。

这里是对原始第一个算法的重写:

algorithm Stack-DFS(G)
    for each vertex u ∈ G.V
        u.color ← WHITE
        u.π ← NIL
    time ← 0
    S ← empty stack
    for each vertex u ∈ G.V
        if u.color = WHITE
            # Start of DFS-VISIT
            step ← 1
            index ← 0
            loop unconditionally
                if step = 1
                    # Before the loop
                    u.color ← GRAY
                    time ← time + 1
                    u.d ← time
                    step ← 2
                if step = 2
                    # Start/continue looping
                    for each vertex v ∈ G.Adj[u]
                        i ← index of v
                        if i ≥ index and v.color = WHITE
                            v.π ← u
                            # Push current state
                            push((u, 2, i + 1), S)
                            # Update variables for new call
                            u = v
                            step ← 1
                            index ← 0
                            # Make the call
                            jump to start of unconditional loop
                    # No more adjacent white nodes
                    step ← 3
                if step = 3
                    # After the loop
                    u.color ← BLACK
                    time ← time + 1
                    u.right ← time
                    # Return
                    if S is empty
                        break unconditional loop
                    else
                        u, step, index ← pop(S)

可能有几个地方可以优化,但至少现在应该可以了。

结果:

Name   d    f   π
q      1   16   NULL
s      2    7   q
v      3    6   s
w      4    5   v
t      8   15   q
x      9   12   t
z     10   11   x
y     13   14   t
r     17   20   NULL
u     18   19   r

【讨论】:

  • 他对我来说非常有意义,这是一个有用的信息,但经过一番思考,我不确定在每种情况下都是如此,绝对是大多数情况。如果我错了,请纠正我,因为我正在尽我所能了解这一点,但我在这里制定了这个例子:link 似乎 y 的父母是 t,它不遵循你给出的不等式。 (此示例中的循环中的顶点按字母顺序选取。
  • Markus,你在识别线段树方面做得很好,这让我意识到算法是错误的。请参阅我上面的帖子。
【解决方案2】:
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);

【讨论】:

  • 虽然这可能会回答问题,但提供有关您发布的代码的一些信息可能会很有用(在 cmets 中或作为单独的解释)。
  • 同意@Hooked,问题的作者似乎没有要求实现。如果您认为它可以提供帮助,最好有一些文字来解释确切的方法。
【解决方案3】:

我想我设法编写了一个更简单的伪代码。

但首先要说几句让事情变得更清楚:

  1. v.pDescendant - 指向由其邻接列表给出的顶点后代的指针。
  2. 在邻接表中,我假设每个元素都有一个字段“pNext”,它指向链表上的下一个元素。
  3. 我使用了一些 C++ 语法,主要是“->”和“&”来强调什么是指针,什么不是。
  4. Stack.top() 返回指向堆栈第一个元素的指针。但与 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      

【讨论】:

    【解决方案4】:

    这是 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。您还可以查看发现时间和完成时间。有任何疑问请评论。我已经包含了足够的 cmets 来理解代码。

    【讨论】:

      【解决方案5】:

      您的非递归代码确实存在严重缺陷。

      检查v是否为WHITE后,在push之前从未标记GRAY,所以可能会被其他未访问的节点一次又一次地视为WHITE,导致多次引用该@987654326 @node 被推入堆栈。这可能是一个灾难性的缺陷(可能导致无限循环等)。

      此外,除了根节点,您不设置 .d。这意味着Nested set model 属性.ds 和.fs 将不正确。 (如果您不知道.ds 和.fs 是什么,请阅读那篇文章,它在过去对我很有启发。文章的left 是您的.d 和@987654334 @ 是你的.f。)

      您的内部if 基本上需要与外部if 相同,减去循环,加上父引用。那就是:

      11                  if v.color = WHITE
      ++                      v.color ← GRAY
      ++                      time ← time + 1
      ++                      v.d ← time
      12                      v.π ← u
      13                      push(v, S)
      

      更正,它应该是一个真正的等价物。

      【讨论】:

      • 这发生在我从 MS Word 复制我的算法时。我以为我已经解决了所有问题,但有时我在校对时可能会粗心。我将以上内容编辑为我所拥有的。对于那个很抱歉。我希望它现在是正确的。
      • 我在原始帖子中添加了一张图片,说明了如何分配 u.d 和 u.f。
      • 更新了与原始帖子非常非常不同的信息。 :)
      • parent 的f 字段将设置为推送所有孩子之后的时间,而不是实际访问它们之后的时间。父级将包装其子级的开始时间,而不是它们的整个子树。
      • 我刚刚在收到热门问题徽章时在这里看到了您的更新,并且我已经更正了我的原始帖子,非常感谢。
      【解决方案6】:

      在非递归版本中,我们需要另一种颜色来反映递归堆栈中的状态。因此,我们将添加一个 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
      

      【讨论】:

        【解决方案7】:
        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();
                    }
                }
            }
        

        【讨论】:

          【解决方案8】:

          我相信至少有一种情况是递归版本和堆栈版本在功能上不等效。考虑三角形的情况 - 顶点 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],这可能会变得相关。 一个例子是,只有当我们使用 DFS 的递归版本时,以下语句才成立。

          一个根顶点是一个关节点当且仅当它至少有 深度优先树中的两个孩子。

          在一个三角形中,显然没有关节点,但是 stack-DFS 仍然为深度优先树中的任何源顶点提供两个孩子(A 有孩子 B 和 C)。只有当我们使用递归 DFS 创建深度优先树时,上述语句才成立。

          [1] 算法简介,CLRS - 问题 22-2(第二版和第三版)

          【讨论】:

          • 您是说从算法的本质上讲,这就是堆栈和递归实现之间的区别吗?或者您是说两者之间的这些差异源于我的实现,并且可以修复它们以使其行为相同?
          • 我的观点是差异是由于实现而产生的。正如我所说,在堆栈版本中,我们一次性推送一个顶点的所有邻居,这与递归情况不同,我们递归地访问每个邻居节点。我没有彻底浏览这个链接,但看起来它处理的是同样的问题。 jroller.com/bobfoster/entry/depth_first_search_with_explicit
          猜你喜欢
          • 2020-03-28
          • 1970-01-01
          • 1970-01-01
          • 2011-07-13
          • 2016-02-16
          • 1970-01-01
          • 2011-04-14
          • 1970-01-01
          相关资源
          最近更新 更多