最近我需要实现非递归DFS作为更复杂算法的一部分,Tarjan的算法是精确的。递归实现非常优雅,但不适合大型图形。当我实现迭代版本时,我感到震惊的是它最终变得多么优雅,我想知道我是否做错了什么。
迭代DFS有两种基本方法。首先,您可以将节点的所有子节点一次推入堆栈(似乎更常见)。或者你可以推一个。我会专注于第一个,因为看起来每个人都这样做。
我对这个算法有各种各样的问题,最后我意识到为了有效地做到这一点,我不需要1,而不是2,但是3个布尔标志(我不一定意味着你需要三个显式的布尔变量,你可能会存储信息间接通过通常为整数的变量的特殊值,但你需要以这种或那种方式访问这3条信息。这三个标志是:1)访问过。这是为了防止儿童被非常多余地推到堆叠上。 2)完成。防止对同一节点进行冗余处理。 3)升序/降序。指示孩子是否已被推入堆叠。伪代码看起来像这样:
while(S)
if S.peek().done == True
S.pop()
continue
S.peek().visited = True
if S.peek().descending == True
S.peek().descending = False
for c in S.peek().children
if c.visited == False
S.push(c)
doDescendingStuff()
else
w = S.pop()
w.done = True
doAscendingStuff()
一些注意事项:1)您不需要技术上升/下降,因为您可以看到孩子是否全部完成。但是在密集图中效率非常低。
2),主要踢球者:访问/完成的事情似乎没有必要。这就是为什么(我认为)你需要它。在堆栈中访问之前,您无法标记访问过的内容。如果这样做,您可以按错误的顺序处理事情。例如。假设A链接到B和C,B链接到D,D链接到C.然后从A,您将在堆栈上推送B和C.从B你把D推到堆栈上......然后是什么?如果在将它们推入堆栈时标记访问的内容,则不会在此处将C推入堆栈。但这是错误的,应该从D中访问C,而不是从该图中的A访问(假设A在C之前访问B)。因此,在处理之前,您不会标记访问过的内容。但是,你将在堆栈上有两次C。所以你需要另一个标志来表明你已经完成了它,所以你不要再次处理C.
我没有看到如何避免所有这一切都有一个完全正确的非递归DFS,它支持绕线和展开的动作。但本能地说它感觉很狡猾。有没有更好的办法?我在网上咨询的几乎所有地方都掩盖了如何实际实现非递归DFS,说它可以完成并提供一个非常基本的算法。当算法是正确的(在适当支持到同一节点的多条路径方面)这是罕见的,它很少适当地支持在卷绕和展开时做东西。
答案 0 :(得分:2)
我认为最优雅的基于堆栈的实现将在堆栈上具有子进程的迭代器,而不是节点。将迭代器想象为在子节点中存储节点和位置。
while (!S.empty)
Iterator i = S.pop()
bool found = false
Iterator temp = null
while (i.hasNext())
Node n = i.next()
if (n.visited == false)
n.visited = true
doDescendingStuff(n)
temp = n.getChildrenIterator()
break
if (!i.hasNext())
doAscendingStuff(i.getNode())
else
S.push(i)
if (temp != null)
S.push(temp)
通过将节点和位置分成2个堆栈,可以优化i.t.o存储空间。
答案 1 :(得分:1)
您的代码无法完全模拟递归DFS实现的情况。 在递归DFS实现中,每个节点在任何时候只在堆栈中出现一次。
Dukeling给出的解决方案是一种迭代方式。基本上,您必须在堆栈中一次只推送一个节点,而不是一次只推送所有节点。
你断言这需要更多存储是错误的:在你的实现中,一个节点可以在堆栈上多次。事实上,如果你从一个非常密集的图形(所有顶点上的完整图形)开始,这将会发生。 使用Dukeling解决方案,堆栈的大小为O(顶点数)。在您的解决方案中,它是O(边数)。
答案 2 :(得分:1)
算法BFS(G,v)
enqueue(Q, v)
mark v as visited
while Q is not empty do
let w = front(Q)
visit(w)
dequeue(Q)
for each vertex u adjacent to w do
if u is not marked
enqueue(Q, u)
mark u as visited
算法DFS(G,v)
push(S, v)
mark v as visited
visit(v)
while S is not empty do
let w = top(S)
pop(S)
find the first umarked vertex u that is adjacent to w
if found such vertex u
push(S, u)
mark u as visited
visit(u)
else if not found such vertex u
pop(S)
答案 3 :(得分:0)
Robert Sedgewick在cpp book中的算法讨论了一个只保留一个项目副本的特殊堆栈,并忘记了旧的副本。不完全确定如何做到这一点,但它消除了堆栈中有多个项目的问题。
答案 4 :(得分:0)
Tl;博士,你不需要一个以上的旗帜。
实际上,您可以通过显式执行编译器对运行时堆栈的操作,将递归DFS转换为迭代。该技术使用goto
来模拟调用和返回,但这些可以转换为更易读的循环。我将在C中工作,因为您实际上可以编译中间结果:
#include <stdio.h>
#include <stdlib.h>
#define ARITY 4
typedef struct node_s {
struct node_s *child[ARITY];
int visited_p;
} NODE;
// Recursive version.
void dfs(NODE *p) {
p->visited_p = 1;
for (int i = 0; i < ARITY; ++i)
if (p->child[i] && !p->child[i]->visited_p)
dfs(p->child[i]);
}
// Model of the compiler's stack frame.
typedef struct stack_frame_s {
int i;
NODE *p;
} STACK_FRAME;
// First iterative version.
void idfs1(NODE *p) {
// Set up the stack.
STACK_FRAME stack[100];
int i, sp = 0;
// Recursive calls will jump back here.
start:
p->visited_p = 1;
// Simplify by using a while rather than for loop.
i = 0;
while (i < ARITY) {
if (p->child[i] && !p->child[i]->visited_p) {
stack[sp].i = i; // Save params and locals to stack.
stack[sp++].p = p;
p = p->child[i]; // Update the param to its new value.
goto start; // Emulate the recursive call.
rtn: ; // Emulate the recursive return.
}
++i;
}
// Emulate restoring the previous stack frame if there is one.
if (sp) {
i = stack[--sp].i;
p = stack[sp].p;
goto rtn; // Return from previous call.
}
}
现在做一些&#34;代数&#34;关于代码开始摆脱goto
s。
void idfs2(NODE *p) {
STACK_FRAME stack[100];
int i, sp = 0;
start:
p->visited_p = 1;
i = 0;
loop:
while (i < ARITY) {
if (p->child[i] && !p->child[i]->visited_p) {
stack[sp].i = i;
stack[sp++].p = p;
p = p->child[i];
goto start;
}
++i;
}
if (sp) {
i = stack[--sp].i + 1;
p = stack[sp].p;
goto loop;
}
}
继续转型,我们最终来到这里:
void idfs3(NODE *p) {
STACK_FRAME stack[100];
int i, sp = 0;
p->visited_p = 1;
i = 0;
for (;;) {
while (i < ARITY) {
if (p->child[i] && !p->child[i]->visited_p) {
stack[sp].i = i;
stack[sp++].p = p;
p = p->child[i];
p->visited_p = 1;
i = 0;
} else {
++i;
}
}
if (!sp) break;
i = stack[--sp].i + 1;
p = stack[sp].p;
}
}
这很好。还有一个可选的&#34;抛光&#34;步。我们最初可以在堆栈上推根,以简化外部循环:
void idfs3(NODE *p) {
STACK_FRAME stack[100];
p->visited_p = 1;
stack[0].i = 0
stack[0].p = p;
int sp = 1;
while (sp > 0) {
int i = stack[--sp].i;
p = stack[sp].p;
while (i < ARITY) {
if (p->child[i] && !p->child[i]->visited_p) {
stack[sp].i = i + 1;
stack[sp++].p = p;
p = p->child[i];
p->visited_p = 1;
i = 0;
} else {
++i;
}
}
}
}
此时很明显,您确实使用了一堆迭代器:指向节点的指针和反映当前搜索该节点的子节点的进度的索引。使用像Java这样的语言,我们可以明确这一点。 (不利的一面是我们在处理孩子时失去了对父母的访问权限,这在某些情况下可能是个问题。)
在这里,我将使用一个单独的集合来保留被访问的节点。这是首选,因为它使得多个搜索和部分搜索更加简单。
void search(Node p) {
Set<Node> visited = new HashSet<>();
Deque<Iterator<Node>> stack = new ArrayDeque<>();
visited.add(p); // Visit the root.
stack.push(p.children.iterator());
while (!stack.isEmpty()) {
Iterator<Node> i = stack.pop(); // Backtrack to a child list with work to do.
while (i.hasNext()) {
Node child = i.next();
if (!visited.contains(child)) {
stack.push(i); // Save progress on this child list.
visited.add(child); // Descend to visit the child.
i = child.children.iterator(); // Process its children next.
}
}
}
}
作为最终的微优化,您可以跳过在堆栈上推送耗尽的迭代器(在i
数组末尾的C,child
值),因为它们被忽略了弹出后。
void search(Node p) {
Set<Node> visited = new HashSet<>();
Deque<Iterator<Node>> stack = new ArrayDeque<>();
visited.add(p); // Visit the root.
if (!p.children.isEmpty()) stack.push(p.children.iterator());
while (!stack.isEmpty()) {
Iterator<Node> i = stack.pop(); // Backtrack to a child list with work to do.
while (i.hasNext()) {
Node child = i.next();
if (!visited.contains(child)) {
if (i.hasNext()) stack.push(i); // Save progress on this child list.
visited.add(child); // Descend to visit the child.
i = child.children.iterator(); // Process its children next.
}
}
}
}
答案 5 :(得分:0)
这里是指向Java程序的链接,该Java程序同时显示了递归和非递归方法的DFS,并且还计算了 发现 和 完成 时间,但没有边缘间隙。
__init__
答案 6 :(得分:-1)
为了使用堆栈进行DFS遍历,从堆栈中弹出一个节点(记得在栈中推送初始节点)并检查它是否已被访问过。如果已经访问过,则忽略并弹出next,否则输出pop-ed节点,将其标记为已访问并将其所有邻居推送到堆栈。继续这样做,直到堆栈为空。