回溯和深度优先搜索有什么区别?


104

回溯和深度优先搜索有什么区别?

Answers:


98

回溯是一种更通用的算法。

深度优先搜索是与搜索树结构有关的回溯的一种特定形式。从维基百科:

一个从根开始(在图例中选择一个节点作为根),并在回溯之前沿每个分支尽可能地探索。

它使用回溯作为其与树一起工作的一部分,但仅限于树结构。

但是,回溯可以用于可以消除部分域的任何类型的结构-无论它是否是逻辑树。Wiki示例使用棋盘和一个特定的问题-您可以查看特定的动作,并消除它,然后回溯到下一个可能的动作,消除它,等等。


13
在这里回应一个古老的帖子。好的答案,但是...棋盘问题也不能表示为树吗?:-)对于给定的棋子,在棋盘上的任何位置都没有一棵可能的棋子延伸到未来吗?我的一部分感觉就像在任何情况下都可以使用回溯,也可以将其建模为一棵树,但是我不确定在这种直觉上我是否正确。
The111 2013年

4
@ The111:确实,任何搜索问题都可以表示为一棵树-您为每个可能的部分解决方案有一个节点,并且从每个节点到在此状态下可以做出的一个或多个可能的选择都有一条边。我认为lcn的回答是,回溯通常意味着递归期间生成的(通常是隐式)搜索树上的DFS最接近真实情况。
j_random_hacker

5
@j_random_hacker因此,DFS是探索树(或更一般的图形)的一种方法,而回溯是解决问题的方法(将DFS与修剪一起使用)。:-)
The111

令人困惑的是,维基百科将回溯描述为深度优先搜索算法,显然将这两个概念混为一谈。
安德森·格林

29

我认为这个对另一个相关问题的答案提供了更多的见解。

对我而言,回溯和DFS之间的区别在于回溯处理隐式树,而DFS处理显式树。这看似微不足道,但却意义非凡。当通过回溯访问问题的搜索空间时,隐式树将被遍历并修剪到中间。但是对于DFS,它要处理的树/图形是显式构造的,在进行任何搜索之前,已经抛出了不可接受的情况(即修剪了)。

因此,回溯是隐式树的DFS,而DFS无需修剪即可回溯。


我认为将回溯视为处理隐式树是令人困惑的。当我初读此书时,我同意但要更深入地了解,我才真正意识到“隐式树”确实是..递归树..以任何使用回溯的示例为例,例如置换字符串,没有逻辑树(没有隐式树)无论问题如何解决,但我们确实都有一个递归树,用于对增量字符串构建过程进行建模。至于修剪,这是对递归树执行的修剪,在该递归树上执行了总的蛮力……(待续)
Gang Fang

(续)例如,打印“答案”字符串的所有排列,并假设第三个字符必须为字符“ a”。递归树的前2个级别服从O(n!),但在第3个级别上,除添加“ a”的分支之外的所有分支均被修剪(回溯)。
Gang Fang

6

回溯通常实现为DFS加上搜索修剪。您遍历搜索空间树时,深度优先构造部分解决方案。蛮力DFS可以构造所有搜索结果,即使是实际上没有意义的搜索结果。构造所有解(n!或2 ^ n)也可能非常低效。因此,在执行DFS时,实际上,您还需要修剪部分解决方案(对于实际任务而言没有意义),并专注于部分解决方案,这可以导致有效的最佳解决方案。这是实际的回溯技术-您尽早丢弃部分解决方案,后退一步,然后尝试再次找到局部最优值。

一直没有停止使用BFS遍历搜索空间树并执行回溯策略的方法,但是在实践中这没有意义,因为您需要将搜索状态逐层存储在队列中,并且树的宽度成倍增加到高度,所以我们会很快浪费很多空间。这就是为什么通常使用DFS遍历树的原因。在这种情况下,搜索状态存储在堆栈(调用堆栈或显式结构)中,并且不能超过树的高度。


5

通常,深度优先搜索是一种遍历实际图/树结构以寻找值的方法,而回溯是遍历问题空间以寻找解决方案的方法。回溯是一种更通用的算法,它甚至不一定与树相关。


5

我想说,DFS是回溯的一种特殊形式。回溯是DFS的一般形式。

如果我们将DFS扩展到一般问题,我们可以称其为回溯。如果我们使用回溯来解决与树/图有关的问题,则可以将其称为DFS。

它们在算法方面具有相同的想法。


实际上,DFS与回溯之间的关系正好相反。检查我的回复,其中详细说明了这一点。
KGhatak '19


5

恕我直言,大多数答案要么很不精确,和/或没有任何参考可验证。因此,让我与参考者分享一个非常清晰的解释

首先,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,《人工智能:现代方法》,第三版


2

深度优先是遍历或搜索树的算法。看这里。回溯是一个更为广泛的术语,可在形成候选解决方案并随后通过回溯到以前的状态而被丢弃的地方使用。看这里。深度优先搜索使用回溯优先搜索一个分支(候选解决方案),如果搜索不成功,则使用其他分支。


2

DFS描述了您想要浏览或遍历图形的方式。它着重于在给定选择的情况下尽可能深入的概念。

回溯通常通过DFS进行,但更侧重于尽可能早地修剪无用的搜索子空间的概念。


1

深度优先搜索中,您从树的根部开始,然后沿每个分支进行探索,然后回溯到每个后续的父节点并遍历其子节点

回溯是一个广义术语,表示从目标结束开始,然后逐步向后移动,逐步建立解决方案。


4
回溯并不意味着从结尾开始并向后移动。如果发现死角,它将保留访问节点的日志以进行回溯。
半滑舌鳎耶拿

1
“从头开始...”,呵呵!
7kemZmani'2

1

IMO,在回溯的任何特定节点上,您尝试首先深度分支到它的每个子节点,但是在分支到任何子节点之前,您需要“清除”先前子节点的状态(此步骤等同于返回转到父节点)。换句话说,每个兄弟姐妹状态都不应相互影响。

相反,在普通的DFS算法中,通常没有此约束,也不需要擦除(追溯)先前的兄弟状态来构造下一个兄弟节点。


1

想法-从任何一点开始,检查其是否是所需的端点,如果是,则找到一个解决方案,否则转到所有下一个可能的位置,如果不能继续执行,则返回到先前的位置,并寻找其他标记该当前位置的替代方法路径不会把我们引向解决方案。

现在,回溯和DFS是应用于2种不同抽象数据类型的同一个想法的2个不同名称。

如果将该思想应用于矩阵数据结构,则称其为回溯。

如果将相同的想法应用于树或图,则将其称为DFS。

这里的陈词滥调是,矩阵可以转换为图,图形可以转换为矩阵。因此,我们实际上应用了这个想法。如果在图形上,则称为DFS;在矩阵上,则称为回溯。

两种算法的思想都是相同的。


0

回溯只是具有特定终止条件的深度优先搜索。

考虑走过一个迷宫,在每个迷宫中做出决定的地方,该决定就是对调用堆栈的调用(进行深度优先搜索)...如果到达终点,则可以返回路径。但是,如果达到死胡同,则想退出某个决定,实质上就是退出调用堆栈中的函数。

所以当我想到回溯的时候

  1. 决定
  2. 基本案例(终止条件)

我在这里的回溯视频中对此进行了解释。

下面是对回溯代码的分析。在此回溯代码中,我希望所有将导致一定总和或目标的组合。因此,我有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)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.