如何在深度优先搜索算法中正确回溯?

Posted

技术标签:

【中文标题】如何在深度优先搜索算法中正确回溯?【英文标题】:How to properly backtrack in Depth First Search Algorithm? 【发布时间】:2021-12-24 04:22:04 【问题描述】:

我正在尝试解决一个问题:在二叉树中查找特定节点的所有祖先。

Input: root, targetNode
Output: An array/list containing the ancestors

假设,我们以上面的二叉树为例。我们想找到节点 4 的祖先。输出应该是 [3, 5, 2, 4]。如果节点为8,则输出为[3, 1, 8]

为了解决这个问题,我编写了一个实现 DFS 的函数。

var ancestor = function(root, target) 
    var isFound = false;
    const dfs = (node, curr) => 
        if (node === null) 
            return curr;
        
        
        if (node.val === target.val) 
            curr.push(node.val);
            isFound = true;
            return curr;
        
        
        curr.push(node.val);
        const left = dfs(node.left, curr);
        if (!isFound) 
            const right = dfs(node.right, curr);
            curr.pop();
            return right;
         else 
            curr.pop();
            return left;
        
        
    
    
    console.log(dfs(root, []));
;

但它没有返回正确的输出。例如,如果targetNode为7,则输出为[3],如果targetNode为8,则输出也是[3]。如果我删除curr.pop() 行,输出也无效。对于 targetNode 7,它是 [3, 5, 6, 2, 7]。我想我发现了我犯错的问题。在回溯时,我在删除curr 数组中推送的节点时做错了。如果我传递一个字符串而不是数组,它会正确打印输出。

var ancestor = function(root, target) 
    var isFound = false;
    const dfs = (node, curr) => 
        if (node === null) 
            return curr;
        
        
        if (node.val === target.val) 
            curr += node.val;
            isFound = true;
            return curr;
        
        
        const left = dfs(node.left, curr + node.val + '->);
        if (!isFound) 
            const right = dfs(node.right, curr + node.val + '->);
            return right;
         else 
            return left;
        
        
    
    
    console.log(dfs(root, ''));

上面的代码用字符串而不是数组正确打印输出,如果我通过targetNode 7,输出是3->5->2->7 我的问题是,如何在这里正确取消选择/回溯?还是我做错了什么?提前致谢。

【问题讨论】:

【参考方案1】:

自然环境中的递归

递归是一种函数式遗产,因此将其与函数式风格一起使用会产生最佳效果。这意味着要避免诸如pushcur += node.val 之类的突变、isFound = true 之类的变量重新分配以及其他副作用之类的命令性事情。我们可以将ancestor 写成一个简单的基于生成器的函数,它将每个节点添加到递归子问题的输出之前 -

const empty =
  Symbol("tree.empty")

function node(val, left = empty, right = empty) 
  return  val, left, right 


function* ancestor(t, q) 
  if (t == empty) return
  if (t.val == q) yield [t.val]
  for (const l of ancestor(t.left, q)) yield [t.val, ...l]
  for (const r of ancestor(t.right, q)) yield [t.val, ...r]


const mytree =
  node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
  
for (const path of ancestor(mytree, 7))
  console.log(path.join("->"))
3->5->2->7

使用模块

最后,我会为这段代码推荐一种基于模块的方法 -

// tree.js

const empty =
  Symbol("tree.empty")

function node(val, left = empty, right = empty) 
  return  val, left, right 


function* ancestor(t, q) 
  if (t == empty) return
  if (t.val == q) yield [t.val]
  for (const l of ancestor(t.left, q)) yield [t.val, ...l]
  for (const r of ancestor(t.right, q)) yield [t.val, ...r]


function insert(t, val) 
  // ...


function remove(t, val) 
  // ...


function fromArray(a) 
  // ...


// other tree functions...

export  empty, node, ancestor, insert, remove, fromArray 
// main.js

import  node, ancestor  from "./tree.js"

const mytree =
  node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
  
for (const path of ancestor(mytree, 7))
  console.log(path.join("->"))
3->5->2->7

私有生成器

在前面的实现中,我们的模块为ancestor 的公共接口公开了一个生成器。另一种选择是在找不到节点且没有祖先时返回undefined。考虑这个隐藏生成器并要求调用者对结果进行空检查的替代实现 -

const empty =
  Symbol("tree.empty")

function node(val, left = empty, right = empty) 
  return  val, left, right 


function ancestor(t, q) 
  function* dfs(t) 
    if (t == empty) return
    if (t.val == q) yield [t.val]
    for (const l of dfs(t.left)) yield [t.val, ...l]
    for (const r of dfs(t.right)) yield [t.val, ...r]
  
  return Array.from(dfs(t))[0]


const mytree =
  node(3, node(5, node(6), node(2, node(7), node(4))), node(1, node(0), node(8)))
  
const result =
  ancestor(mytree, 7)

if (result)
  console.log(result.join("->"))
else
  console.log("no result")
3->5->2->7

【讨论】:

非常感谢您对递归副作用的回答和洞察。我不熟悉 javascript 中的生成器概念,我现在一定会研究一下。 我会尽量避免递归中的变异,但是你能告诉我我写的代码哪里做错了吗?【参考方案2】:

需要检查右孩子的DFS是否找到了节点。 修复:

        const left = dfs(node.left, curr);
        if (!isFound) 
            const right = dfs(node.right, curr);
            if(isFound) 
                return right;
            
            curr.pop();
            return; // return nothing, backtracking
        
        return left;

【讨论】:

感谢您的回复。我已经尝试过您的解决方案,它仍然给我错误的输出。对于目标节点 7,输出为 [3,5,6,2,7] @h_a 那是因为我忘记包含 pop()-s,修复它。 成功了。谢谢你。 :-) 所以如果我总结一下我做错了什么,即使我找到了解决方案,我还是从curr 数组中弹出了该项目。那是我的问题,对吧?如果它不能引导我找到正确的解决方案,我应该只 pop() 项目吗? @h_a 是的,您不需要在 DFS 中返回任何特定值,因此修复代码的一种方法是,就像您说的那样,仅弹出当前的如果它没有得到到节点【参考方案3】:

在数组示例中,您的循环以 DFS 方式遍历节点,因此每个节点都以这种方式连接。如果我们计算 DFS 算法中的树节点,[3 , 5, 6, 2, 7] 实际上是按 1, 2, 3, 4 和 5 的顺序排列的。这样,您在数组中的整个树应该是这样的; [3、5、6、2、7、4、1、0、8]。

所以当你找到目标值时,你会从当前节点弹出,并在 DFS 算法中将其全部追溯到头节点。

我要么建议找到一种方法来解决这个问题,要么你可以保存每个节点的父节点。这意味着您可以使用元组而不是 int 数组(如果可以接受的话)。索引可能如下所示 = (node value, parent value)

[(3,NULL),(5,3),(6,5),(2,5)...]

然后相应地回溯……

【讨论】:

谢谢。我会这样想。

以上是关于如何在深度优先搜索算法中正确回溯?的主要内容,如果未能解决你的问题,请参考以下文章

递归/回溯/深度优先搜索/广度优先搜索 /动态规划/二分搜索/贪婪算法

DFS ( 深度优先/回溯算法 ) 一

广度优先和深度优先算法

深入浅出回溯算法

Num 36 : ZOJ 2100 [ 深度优先搜索算法 ] [ 回溯 ]

算法-回溯回溯总结