不理解二叉树最大路径和问题的解决方案。

12

网站GeeksforGeeks提出了二叉树最大路径和问题的解决方案。问题如下:

给定一棵二叉树,找到最大的路径和。该路径可以从树中的任何节点开始和结束。

该解决方案的核心如下:

int findMaxUtil(Node node, Res res) 
{ 
  
    if (node == null) 
        return 0; 
  
    // l and r store maximum path sum going through left and 
    // right child of root respectively 
    int l = findMaxUtil(node.left, res); 
    int r = findMaxUtil(node.right, res); 
  
    // Max path for parent call of root. This path must 
    // include at-most one child of root 
    int max_single = Math.max(Math.max(l, r) + node.data, 
                              node.data); 
  
  
    // Max Top represents the sum when the Node under 
    // consideration is the root of the maxsum path and no 
    // ancestors of root are there in max sum path 
    int max_top = Math.max(max_single, l + r + node.data); 
  
    // Store the Maximum Result. 
    res.val = Math.max(res.val, max_top); 
  
    return max_single; 
} 

int findMaxSum() { 
    return findMaxSum(root); 
 } 
  
// Returns maximum path sum in tree with given root 
int findMaxSum(Node node) { 
  
    // Initialize result 
    // int res2 = Integer.MIN_VALUE; 
    Res res = new Res(); 
    res.val = Integer.MIN_VALUE; 
  
    // Compute and return result 
    findMaxUtil(node, res); 
    return res.val; 
} 

Res 的定义如下:

 class Res { 
    public int val; 
}
我对这些代码的理由感到困惑:

我对这些代码的原因感到困惑:

int max_single = Math.max(Math.max(l, r) + node.data, node.data);  

int max_top = Math.max(max_single, l + r + node.data); 

res.val = Math.max(res.val, max_top); 

return max_single; 

我认为上面的代码遵循这个逻辑,但我不明白为什么这个逻辑是正确或有效的

对于每个节点,最大路径可以通过以下四种方式之一经过该节点:

  1. 仅通过节点
  2. 通过左子节点最大路径 + 节点
  3. 通过右子节点最大路径 + 节点
  4. 通过左子节点最大路径 + 节点 + 通过右子节点最大路径

特别地,我不明白为什么在函数findMaxUtil中返回max_single,而不是变量res.val包含我们感兴趣的答案。网站上给出了以下原因,但我不理解:

需要注意的重要一点是,每个子树的根节点都需要返回最大路径和,使得根节点的最多一个子节点参与其中。

有人能为这个解决方案的步骤提供解释吗?


这种方法是因为findMaxUtil是一个递归函数。在这里,res用于跟踪整体最大值,该值将在调用findMaxUtil时传递,并随后用于在树节点上进行遍历时与路径和进行比较。当语言支持时,另一种方法是将res作为全局变量。 - Agus
“递归本身会隐式地跟踪最大和吗?”不是的。传递可以更新的变量的一种方式是通过对象,就像解决方案(int findMaxUtil(Node node, Res res))中所提供的那样。顺便提一下,不能使用原始类型变量来做到这一点。另一种方式是通过在同一类上创建静态变量。最后一种方法是为findMaxUtil创建一个特定的类作为返回值,该类将保存max_single和“res”两个参数。 - Agus
1
逻辑表明,最大路径不一定总是从根节点开始。可能最大路径和在其子树中的某一个。 - Nitin Singhal
1
@a_sid,在每个节点的每次遍历中,有5个新的最大路径和候选者,即左+当前节点右+当前节点当前节点左+右+当前节点和最大路径和或res。比较这5个值将产生新的最大路径和/res。如果我们观察到,这里考虑了当前节点,因为一个节点可能有负值,所以当前节点数据仍然可能大于当前节点+左当前节点+右 - Agus
3
@a_sid 举个例子,如果你遵循Java的命名规范,那么max_single应该改为maxSingle。 - NomadMaker
显示剩余5条评论
3个回答

6
特别是在函数findMaxUtil中,我不明白为什么会返回max_single,而我们感兴趣的答案已经包含在变量res.val中。
问题在于findMaxUtil()实际上做了两件事情:它返回应用于树的最大总和,并更新一个变量来跟踪迄今为止遇到的最大总和。原始代码中有一条注释说明这一点,但您在问题中将其删除了,可能是为了简洁。
// This function returns overall maximum path sum in 'res' 
// And returns max path sum going through root. 
int findMaxUtil(Node node, Res res) 

因为Java通过传递参数,但是Java中的每个对象变量都隐含地引用实际对象, 所以很容易忽略这个函数可能会改变res参数中传递的Res对象。这就是你所询问的代码行发生的情况。
int max_single = Math.max(Math.max(l, r) + node.data, node.data);  

int max_top = Math.max(max_single, l + r + node.data); 

res.val = Math.max(res.val, max_top); 

return max_single;

那个第一行找到节点本身或节点加上最大子树的最大值,该结果是“通过根的最大路径和”。在最后一行返回该值,这是函数所做的“一件事”。第二行和第三行检查该值并考虑其中是否有任何一个更大,还是包括两个子节点的路径更大,如果是,则更新“res”,这是该函数所做的“另一件事”。请记住,“res”是某个“方法外部存在”的对象,因此对其所做的更改将持续到递归停止,并且“findMaxSum(Node)”(启动整个过程的函数)将返回“res.val”。
那么回到顶部的问题,findMaxUtil返回max_single的原因是它使用该值递归确定每个子树中的最大路径。res中的值也会更新,以便findMaxSum(Node)可以使用它。

1
谢谢您在我之前解释了这个问题。还可以注意到,这可能不是编写函数的最佳方式。输出参数非常令人困惑。如果已经使用了Res类,为什么不只是给它一个第二个属性,使对象不可变(public final int maxLengthInside; public final int maxLengthFromRoot;),并让每个调用返回一个这样的实例。 - Bergi
@Bergi除了更容易理解之外,您的建议也可以很好地与线程一起使用。问题中的实现至少需要在res周围加锁才能保证线程安全,这会减慢速度。 - Caleb
@Caleb ...findMaxUtil返回max_single的原因是它使用该值递归地确定每个子树的最大路径。 但是,为什么函数应该返回max_single而不是max_topl+r+node.data?这背后的直觉是什么? - a_sid
@a_sid 看看它被调用的地方和注释。调用者想知道树的任一侧的最大总和,而不是两侧都要算。max_top 包括当前子树的两侧;它可能是最大的子路径总和,但不能成为更高节点的子路径总和的一部分。 - Caleb
@Caleb 好的,没错 - a_sid
显示剩余6条评论

4
你缺少了res.val的值。该算法尝试探索整棵树,使用res.val等于到目前为止探索过的最大路径长度。在每一步中,它递归地遍历子节点并更新res.val的值,如果更高则更新,以保持最大路径长度。
证明:
假设你的算法适用于高度为n的树。对于高度为n+1的树,有一个根和两个高度为n的子树。此外,考虑到findMaxUtil对于i<=n的树可以正常工作,并返回以子树的部分根开始的最大路径。
因此,计算高度为n+1的树中的最大路径如下所示:
  1. findMaxUtil(subtree1)
  2. findMaxUtil(subtree2)
  3. findmaxUtil(subtree1)+root.data
  4. findmaxUtil(subtree2)+root.data
  5. findmaxUtil(subtree1)+findmaxUtil(subtree2)+root.data
  6. res.val
最终的结果是: findmaxUtil(newTree)=max(items 1:6)

你为什么要做第三步和第四步?它们似乎已经包含在第五步中了。我猜第六步实际上是你的最终结果?也就是说:找到subtree1的最大值,然后找到subtree2的最大值,再找到它们和它们共同的根节点之间的最大值。 - Adriaan
2
@Adriaan 最终结果是1到6中计算出的最大值。需要3和4,因为它们中的任何一个都可能比5中计算出的值更大。假设subtree1给出5,root.data为2,但subtree2为-11。那么项目1-6为:{5,-11,7,-9,-4,res.val},最大值是项目3或res.val - Caleb

3
老实说,我认为该网站上的描述非常不清楚。我会尽我所能说服您理解算法背后的推理。
我们有一棵二叉树,节点上有值:

enter image description here

我们正在寻找树中的一条路径,一系列相连的节点。

enter image description here

由于它是一棵有向树,任何非空路径都包括一个最低深度节点(即路径中距离树根最近的节点),一条从最低深度节点下降到左侧的零个或多个节点的路径,以及一条从最低深度节点下降到右侧的零个或多个节点的路径。特别地,树中某处存在一个节点,它是最大路径中的最低深度节点。(实际上,可能会有多个等值的这样的路径并列,并且它们每个都可能有自己独特的最低深度节点。这没关系。只要至少有一个,那就是最重要的。)
(我在图表中使用了“最高”,但我的意思是“最低深度”。为了明确,任何时候我使用“深度”或“下降”,我都是在谈论树中的位置。任何时候我使用“最大值”,我都是在谈论节点的值或路径中节点值的总和。)

enter image description here

因此,如果我们能找到它的最低深度节点,我们就知道最大值路径由节点本身、从(包括)其左子节点下降的零个或多个节点的子路径以及从(包括)其右子节点下降的零个或多个节点的子路径组成。很容易得出结论,左侧和右侧下降路径必须是各自的最大值下降路径。(如果这不是很明显,请考虑无论你选择哪条其他路径,你都可以通过选择该侧的最大值下降路径来增加总值。) 如果这两条路径中的任何一条或两条路径都具有负值,则我们根本不在负面方面包含任何节点。
因此,我们有一个单独的子问题-给定一个子树,通过其根节点下降的最大值路径的价值是多少?如果所有以其子节点为根的路径的总和为负,或者如果它没有子节点,则可能只是根本身。否则,它是根加上其子节点之一所在的最大值下降路径。这个子问题可以很容易地被回答,但为了避免重复遍历和重新做工作,我们将把它们合并成一次遍历树。
回到主要问题,我们知道某些节点是最大值路径中深度最浅的节点。我们并不特别关心何时访问它 - 我们只会递归地访问每个节点,并找到具有该路径作为其最低深度节点的最大值路径,确信在某个时候我们会访问到我们想要的那个节点。在每个节点上,我们计算从该点开始并在子树内下降的最大值路径(max_single)和该节点是路径中深度最浅的节点的最大值路径(max_top)。后者通过将节点和经过其子代的最大下降路径中的零、一个或两个连接起来得到。(由于max_single已经是从零或一个子节点下降的最大值路径,我们需要考虑的唯一额外事项是穿过两个子节点的路径。)通过在每个节点计算max_top并保持在res.val中发现的最大值,我们保证在完成遍历树时会找到所有值中最大的值。在每个节点上,我们返回max_single以供父级计算使用。在算法结束时,我们只需从res.val中提取答案即可。

这只是一个小步骤,就可以得出左右下降路径必须是每侧的最大值。 (如果这不明显,请考虑无论选择哪条其他路径,都可以通过选择该侧上的最大值下降路径来增加总值。)我不明白你在这里的意思。您是否在说明,在左右子树中选择一个具有最高价值的子树将产生最大的路径和? - a_sid
我认为我应该使用术语“最低深度”而不是“最高深度”,以避免混淆。 - Weeble
1
哦,好的。所以当你在第一条评论中说“路径部分位于左子树中必须是从该点下降的最高值路径,而位于右子树中的部分必须是从该点下降的最高值路径。”时,你的意思是在父节点左侧所有可用路径中,我们应该选择具有最大总和的左侧路径(对于右侧也是如此)。我理解得对吗? - a_sid
你最后两条评论互相回答了!当我们寻找下降到左右子树中的最大值路径时,这正是“max_single”告诉我们的!这与知道子树中任何地方的最大值路径不同。“max_single”仅是在子树根处终止的所有路径中的最大值。 - Weeble
1
没关系,这不是比赛。:D 只要我的回答有帮助,我就很开心。 - Weeble
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接