【发布时间】:2009-08-18 15:40:27
【问题描述】:
回溯和深度优先搜索有什么区别?
【问题讨论】:
标签: algorithm
回溯和深度优先搜索有什么区别?
【问题讨论】:
标签: algorithm
Backtracking 是一种更通用的算法。
Depth-First search 是一种与搜索树结构相关的回溯的特定形式。来自维基百科:
从根开始(在图中选择某个节点作为根),并在回溯之前沿着每个分支尽可能地探索。
它使用回溯作为其处理树的方法的一部分,但仅限于树结构。
不过,回溯可以用于任何类型的结构,其中可以消除域的某些部分——无论它是否是逻辑树。 Wiki 示例使用棋盘和特定问题 - 您可以查看特定的移动,然后将其消除,然后回溯到下一个可能的移动,消除它,等等。
【讨论】:
我认为this answer 对另一个相关问题提供了更多见解。
对我来说,回溯和 DFS 之间的区别在于回溯处理隐式树,而 DFS 处理显式树。这看似微不足道,但意义重大。当通过回溯访问问题的搜索空间时,隐式树在其中被遍历和修剪。然而对于 DFS,它处理的树/图是显式构造的,并且在任何搜索完成之前已经抛出,即修剪掉不可接受的情况。
所以,回溯是隐式树的 DFS,而 DFS 是不修剪的回溯。
【讨论】:
恕我直言,大多数答案要么在很大程度上不精确和/或没有任何参考可验证。所以让我分享一个非常清晰的解释和参考。
首先,DFS 是一种通用的图遍历(和搜索)算法。所以它可以应用于任何图(甚至森林)。树是一种特殊的图,所以 DFS 也适用于树。本质上,我们不要再说它只适用于一棵树,或者类似的东西。
基于[1],回溯是一种特殊的DFS,主要用于节省空间(内存)。我将要提到的区别可能看起来令人困惑,因为在这种类型的图形算法中,我们已经习惯于使用邻接表表示并使用迭代模式来访问所有直接邻居(对于树来说,它是直接的孩子),我们经常忽略 get_all_immediate_neighbors 的错误实现可能会导致底层算法的内存使用不同。
此外,如果图节点具有分支因子 b 和直径 h(对于树,这是树的高度),如果我们在访问节点的每一步存储所有直接邻居,内存要求将是 big-O(bh)。但是,如果我们一次只取一个(直接)邻居并扩展它,那么内存复杂度会降低到 big-O(h)。 前一种实现称为DFS,后一种称为Backtracking。
现在您明白了,如果您使用高级语言,很可能您实际上是在以 DFS 的名义使用回溯。此外,为一个非常大的问题集跟踪访问过的节点可能会占用大量内存。需要仔细设计 get_all_immediate_neighbors(或可以处理重新访问节点而不会陷入无限循环的算法)。
[1] Stuart Russell 和 Peter Norvig,人工智能:现代方法,第三版
【讨论】:
根据 Donald Knuth 的说法,情况是一样的。 这是他论文中关于 Dancing Links 算法的链接,该算法用于解决诸如 N-queens 和 Sudoku 求解器等“非树”问题。
【讨论】:
回溯通常作为 DFS 加搜索修剪来实现。您一路遍历搜索空间树深度优先构造部分解决方案。蛮力 DFS 可以构建所有搜索结果,甚至是那些实际上没有意义的搜索结果。这对于构建所有解决方案(n!或 2^n)也可能非常低效。所以实际上,当你做 DFS 时,你还需要修剪在实际任务的上下文中没有意义的部分解决方案,并专注于部分解决方案,这可以导致有效的最佳解决方案。这是实际的回溯技术——您尽早丢弃部分解决方案,退后一步并尝试再次找到局部最优值。
没有什么停下来使用 BFS 遍历搜索空间树并一路执行回溯策略,但在实践中没有意义,因为您需要将搜索状态逐层存储在队列中,并且树的宽度呈指数增长到高度,所以我们很快就会浪费很多空间。这就是为什么通常使用 DFS 遍历树的原因。在这种情况下,搜索状态存储在堆栈(调用堆栈或显式结构)中,并且不能超过树的高度。
【讨论】:
通常,深度优先搜索是一种遍历实际图/树结构以寻找值的方法,而回溯则是遍历问题空间以寻找解决方案。回溯是一种更通用的算法,甚至不一定与树相关。
【讨论】:
我想说,DFS 是回溯的特殊形式;回溯是DFS的一般形式。
如果我们将 DFS 扩展到一般问题,我们可以称之为回溯。 如果我们使用回溯来解决与树/图相关的问题,我们可以称之为 DFS。
它们在算法方面具有相同的想法。
【讨论】:
DFS 描述了您想要探索或遍历图形的方式。它侧重于在有选择的情况下尽可能深入的概念。
回溯虽然通常通过 DFS 实现,但更侧重于尽早修剪没有希望的搜索子空间的概念。
【讨论】:
IMO,在回溯的任何特定节点上,您首先尝试深入分支到其每个子节点,但在分支到任何子节点之前,您需要“清除”前一个子节点的状态(这一步是相当于返回到父节点)。换句话说,每个兄弟姐妹的状态不应该相互影响。
相反,在正常的 DFS 算法中,你通常没有这个约束,你不需要为了构造下一个兄弟节点而清除(回溯)之前的兄弟节点状态。
【讨论】:
深度优先搜索(DFS)和回溯是不同的搜索和遍历算法。 DFS 更广泛,可用于 graph 和 tree 数据结构,而 DFS 仅限于 tree。话虽如此,但这并不意味着 DFS 不能用于图形。它也用于图形,但只生成生成树,一种没有循环的树(多条边从同一顶点开始和结束)。这就是为什么它仅限于树。
现在回到回溯,DFS在树数据结构中使用回溯算法,因此在树中,DFS和回溯是相似的。
因此,我们可以说,它们在树数据结构中是相同的,而在图数据结构中,它们是不相同的。
【讨论】:
想法 - 从任何点开始,检查它是否是所需的端点,如果是,那么我们找到了一个解决方案,否则会转到所有下一个可能的位置,如果我们不能更进一步,则返回上一个位置并寻找其他替代方案标记当前路径不会引导我们找到解决方案。
现在回溯和 DFS 是 2 个不同的名称,用于表示应用于 2 种不同抽象数据类型的相同想法。
如果这个想法应用于矩阵数据结构,我们称之为回溯。
如果将相同的想法应用到树或图上,我们称之为 DFS。
这里的陈词滥调是矩阵可以转换为图形,而图形可以转换为矩阵。所以,我们实际上应用了这个想法。如果在图上,我们称之为 DFS,如果在矩阵上,我们称之为回溯。
两种算法的思路是一样的。
【讨论】:
在深度优先搜索中,您从树的根开始,然后沿着每个分支一直探索,然后回溯到每个后续的父节点,然后遍历它的孩子
回溯是一个通用术语,表示从目标结束开始,逐步向后移动,逐步构建解决方案。
【讨论】:
回溯只是具有特定终止条件的深度优先搜索。
考虑在迷宫中行走,您在每一步都做出决定,该决定是对调用堆栈(进行深度优先搜索)的调用......如果您到达终点,您可以返回您的路径。但是,如果您遇到死胡同,您希望退出某个决定,实质上是退出调用堆栈上的一个函数。
所以当我想到回溯时,我关心的是
我在我的回溯视频中解释了它here。
回溯代码分析如下。在这个回溯代码中,我想要所有能够产生某个总和或目标的组合。因此,我有 3 个决定调用我的调用堆栈,在每个决定中,我可以选择一个数字作为到达目标 num 的路径的一部分,跳过该数字,或者选择它并再次选择它。然后如果我达到终止条件,我的回溯步骤就是return。返回是回溯步骤,因为它退出了调用堆栈上的调用。
class Solution:
"""
Approach: Backtracking
State
-candidates
-index
-target
Decisions
-pick one --> call func changing state: index + 1, target - candidates[index], path + [candidates[index]]
-pick one again --> call func changing state: index, target - candidates[index], path + [candidates[index]]
-skip one --> call func changing state: index + 1, target, path
Base Cases (Termination Conditions)
-if target == 0 and path not in ret
append path to ret
-if target < 0:
return # backtrack
"""
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
"""
@desc find all unique combos summing to target
@args
@arg1 candidates, list of ints
@arg2 target, an int
@ret ret, list of lists
"""
if not candidates or min(candidates) > target: return []
ret = []
self.dfs(candidates, 0, target, [], ret)
return ret
def dfs(self, nums, index, target, path, ret):
if target == 0 and path not in ret:
ret.append(path)
return #backtracking
elif target < 0 or index >= len(nums):
return #backtracking
# for i in range(index, len(nums)):
# self.dfs(nums, i, target-nums[i], path+[nums[i]], ret)
pick_one = self.dfs(nums, index + 1, target - nums[index], path + [nums[index]], ret)
pick_one_again = self.dfs(nums, index, target - nums[index], path + [nums[index]], ret)
skip_one = self.dfs(nums, index + 1, target, path, ret)
【讨论】:
在我看来,区别在于修剪树。回溯可以通过检查给定条件(如果条件不满足)来停止(完成)搜索中间的某个分支。但是,在 DFS 中,您必须到达分支的叶子节点才能确定条件是否满足,因此您不能停止搜索某个分支,直到您到达其叶子节点。
【讨论】:
我看待 DFS 与回溯的方式是回溯更强大。 DFS 可以帮助我回答树中是否存在节点,而回溯可以帮助我回答 2 个节点之间的所有路径。
注意区别:DFS 访问一个节点并将其标记为已访问,因为我们主要是搜索,所以查看一次就足够了。回溯会多次访问一个节点,因为它是一个路线修正,因此得名回溯。
大多数回溯问题涉及:
def dfs(node, visited):
visited.add(node)
for child in node.children:
dfs(child, visited)
visited.remove(node) # this is the key difference that enables course correction and makes your dfs a backtracking recursion.
【讨论】: