回溯和深度优先搜索有什么区别?
回溯和深度优先搜索有什么区别?
Answers:
回溯是一种更通用的算法。
深度优先搜索是与搜索树结构有关的回溯的一种特定形式。从维基百科:
一个从根开始(在图例中选择一个节点作为根),并在回溯之前沿每个分支尽可能地探索。
它使用回溯作为其与树一起工作的一部分,但仅限于树结构。
但是,回溯可以用于可以消除部分域的任何类型的结构-无论它是否是逻辑树。Wiki示例使用棋盘和一个特定的问题-您可以查看特定的动作,并消除它,然后回溯到下一个可能的动作,消除它,等等。
对我而言,回溯和DFS之间的区别在于回溯处理隐式树,而DFS处理显式树。这看似微不足道,但却意义非凡。当通过回溯访问问题的搜索空间时,隐式树将被遍历并修剪到中间。但是对于DFS,它要处理的树/图形是显式构造的,在进行任何搜索之前,已经抛出了不可接受的情况(即修剪了)。
因此,回溯是隐式树的DFS,而DFS无需修剪即可回溯。
回溯通常实现为DFS加上搜索修剪。您遍历搜索空间树时,深度优先构造部分解决方案。蛮力DFS可以构造所有搜索结果,即使是实际上没有意义的搜索结果。构造所有解(n!或2 ^ n)也可能非常低效。因此,在执行DFS时,实际上,您还需要修剪部分解决方案(对于实际任务而言没有意义),并专注于部分解决方案,这可以导致有效的最佳解决方案。这是实际的回溯技术-您尽早丢弃部分解决方案,后退一步,然后尝试再次找到局部最优值。
一直没有停止使用BFS遍历搜索空间树并执行回溯策略的方法,但是在实践中这没有意义,因为您需要将搜索状态逐层存储在队列中,并且树的宽度成倍增加到高度,所以我们会很快浪费很多空间。这就是为什么通常使用DFS遍历树的原因。在这种情况下,搜索状态存储在堆栈(调用堆栈或显式结构)中,并且不能超过树的高度。
我想说,DFS是回溯的一种特殊形式。回溯是DFS的一般形式。
如果我们将DFS扩展到一般问题,我们可以称其为回溯。如果我们使用回溯来解决与树/图有关的问题,则可以将其称为DFS。
它们在算法方面具有相同的想法。
根据Donald Knuth所说,是相同的。这是他的论文中有关“跳舞链接”算法的链接,该算法用于解决“非树”问题,例如N-queens和Sudoku求解器。
恕我直言,大多数答案要么很不精确,和/或没有任何参考可验证。因此,让我与参考者分享一个非常清晰的解释。
首先,DFS是一种通用的图形遍历(和搜索)算法。因此它可以应用于任何图形(甚至森林)。树是一种特殊的图,因此DFS也适用于树。从本质上讲,让我们停止说它仅适用于树木或类似树木。
基于[1],回溯是一种特殊的DFS,主要用于节省空间(内存)。我要提到的区别似乎令人困惑,因为在这种Graph算法中,我们习惯于使用邻接列表表示并使用迭代模式来访问节点的所有直接邻居(对于树,它是直接子节点) ,我们通常会忽略get_all_immediate_neighbors的错误实现可能会导致底层算法的内存使用有所不同。
此外,如果一个图节点的分支因子为b,直径为h(对于树来说,这是树的高度),如果我们在访问该节点的每个步骤中存储所有直接邻居,则内存需求将为big-O(bh)。但是,如果我们一次只接受一个(立即)邻居,然后对其进行扩展,则内存复杂度将降低为big-O(h)。前一种实现称为DFS,后一种称为Backtracking。
现在您将看到,如果您使用的是高级语言,则很可能实际上是在以DFS为幌子使用回溯。此外,针对非常大的问题集跟踪访问的节点可能确实会占用大量内存。要求仔细设计get_all_immediate_neighbors(或可以处理节点重新访问而无需陷入无限循环的算法)。
[1] Stuart Russell和Peter Norvig,《人工智能:现代方法》,第三版
在深度优先搜索中,您从树的根部开始,然后沿每个分支进行探索,然后回溯到每个后续的父节点并遍历其子节点
回溯是一个广义术语,表示从目标结束开始,然后逐步向后移动,逐步建立解决方案。
想法-从任何一点开始,检查其是否是所需的端点,如果是,则找到一个解决方案,否则转到所有下一个可能的位置,如果不能继续执行,则返回到先前的位置,并寻找其他标记该当前位置的替代方法路径不会把我们引向解决方案。
现在,回溯和DFS是应用于2种不同抽象数据类型的同一个想法的2个不同名称。
如果将该思想应用于矩阵数据结构,则称其为回溯。
如果将相同的想法应用于树或图,则将其称为DFS。
这里的陈词滥调是,矩阵可以转换为图,图形可以转换为矩阵。因此,我们实际上应用了这个想法。如果在图形上,则称为DFS;在矩阵上,则称为回溯。
两种算法的思想都是相同的。
回溯只是具有特定终止条件的深度优先搜索。
考虑走过一个迷宫,在每个迷宫中做出决定的地方,该决定就是对调用堆栈的调用(进行深度优先搜索)...如果到达终点,则可以返回路径。但是,如果达到死胡同,则想退出某个决定,实质上就是退出调用堆栈中的函数。
所以当我想到回溯的时候
我在这里的回溯视频中对此进行了解释。
下面是对回溯代码的分析。在此回溯代码中,我希望所有将导致一定总和或目标的组合。因此,我有3个决策调用到我的调用堆栈,在每个决策中,我都可以选择一个号码作为到达目标num的路径的一部分,跳过该号码,或者选择并再次选择它。然后,如果我达到终止条件,那么我的回溯步骤就是返回。返回是回溯步骤,因为它退出了调用堆栈中的该调用。
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)