12

The website GeeksforGeeks has presented a solution for the problem of Maximum path sum for a binary tree. The question is as follows:

Given a binary tree, find the maximum path sum. The path may start and end at any node in the tree.

The core of the solution is as follows:

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 has the following definition:

 class Res { 
    public int val; 
}

I am confused about the reasoning behind these lines of code:

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; 

I believe the code above follows this logic but I do not understand WHY this logic is correct or valid:

For each node there can be four ways that the max path goes through the node:

  1. Node only
  2. Max path through Left Child + Node
  3. Max path through Right Child + Node
  4. Max path through Left Child + Node + Max path through Right Child

In particular, I do not understand why max_single is being returned in the function findMaxUtil when we the variable res.val contains the answer we are interested in. The following reason is given on the website but I do not understand it:

An important thing to note is, root of every subtree need to return maximum path sum such that at most one child of root is involved.

Could someone provide an explanation for this step of the solution?

a_sid
  • 577
  • 10
  • 28
  • This approach is used because `findMaxUtil` is a recursive function. Here, `res` is used to track overall maximum value which will be passed across when calling `findMaxUtil`, and subsequently will be used to compare against the path sums during a pass on tree node. Other approach would be to make `res` as global variable when the language support this. – Agus Dec 06 '20 at 03:34
  • @akuzminykh The given solution passes [Leetcode tests](https://leetcode.com/problems/binary-tree-maximum-path-sum/) – a_sid Dec 06 '20 at 03:53
  • `the recursion will already implicitly keep track of the largest sum?` No, it is not. One way to pass around variable that can be updated is through an object as provided ont that solution(`int findMaxUtil(Node node, Res res) `), cannot do this using primitive type variable btw. Other is through static variable on the same class. The last one is by creating a specific class as as return value for `findMaxUtil`, which will hold both the `max_single` and `"res"`. – Agus Dec 06 '20 at 04:02
  • 1
    The logic states that maximum path need not to go from root always. It is possible that max path sum is one of it's subtree. – Nitin Singhal Dec 06 '20 at 04:07
  • @Agus _and subsequently will be used to compare against the path sums during a pass on tree node._ This is the part of the solution I am struggling to understand. – a_sid Dec 06 '20 at 04:07
  • 1
    @a_sid, on each pass of a node, there are 5 candidates of new max-sum-path, i.e. `left+current node`, `right+current node`, `current node`, `left+right+current node` and max-sum-path or `res`. Comparing these 5 values will yield new max-sum-path/`res`. If we observe, `current node` is considered here since a node can have negative value, so there is a chance that `current node` data is still greater than `current node+left` or `current node+right`. – Agus Dec 06 '20 at 04:39
  • Java naming conventions have classes, methods, and variables use camelCase without underlines. – NomadMaker Dec 06 '20 at 20:16
  • @NomadMaker _variables use camelCase without underlines._ What do you mean by this? – a_sid Dec 07 '20 at 05:50
  • 3
    @a_sid For example, your max_single would be maxSingle if you follow the Java naming conventions. – NomadMaker Dec 07 '20 at 10:17
  • @Agus Thank you for the explanation. Could you please also explain WHY the function should return `max_single` to its parent/calling function? – a_sid Dec 10 '20 at 06:54

3 Answers3

6

In particular, I do not understand why max_single is being returned in the function findMaxUtil when we the variable res.val contains the answer we are interested in.

The problem is that findMaxUtil() really does two things: it returns largest sum of the tree that it's applied to, and it updates a variable that keeps track of the largest sum yet encountered. There's a comment to that effect in the original code, but you edited it out in your question, perhaps for brevity:

// This function returns overall maximum path sum in 'res' 
// And returns max path sum going through root. 
int findMaxUtil(Node node, Res res) 

Because Java passes parameters by value, but every object variable in Java implicitly references the actual object, it's easy to miss the fact that the Res that's passed in the res parameter may be changed by this function. And that's exactly what happens in the lines you asked about:

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;

That first line finds the maximum of the node itself or the node plus the greatest subtree, and that result is the max path sum going through root. Returning that value on the last line is one thing that this function does. The second and third lines look at that value and consider whether either it or the path that includes both children is larger than any previously seen path, and if so, it updates res, which is the other thing this function does. Keep in mind that res is some object that exists outside the method, so changes to it persist until the recursion stops and findMaxSum(Node), which started the whole thing, returns the res.val.

So, getting back to the question at the top, the reason that the findMaxUtil returns max_single is that it uses that value to recursively determine the max path through each subtree. The value in res is also updated so that findMaxSum(Node) can use it.

Caleb
  • 124,013
  • 19
  • 183
  • 272
  • 1
    This. Thanks for explaining before I could get to it… One could also note that this might be not the best way to write the function. Output parameters are pretty confusing. If one uses a `Res` class already, why not just give it a second attribute, make the objects immutable (`public final int maxLengthInside; public final int maxLengthFromRoot;`) and have each call return one such instance. – Bergi Dec 08 '20 at 18:20
  • @Bergi In addition to being a little easier to understand, your suggestion would also work well with threads. The implementation in the question would at least require locks around `res` in order to be thread safe, and that'd slow things down. – Caleb Dec 08 '20 at 19:17
  • @Caleb _...the reason that the `findMaxUtil` returns `max_single` is that it uses that value to recursively determine the max path through each subtree._ Why though should the function return `max_single` and not either `max_top` or `l+r+node.data`? What is the intuition behind it? – a_sid Dec 09 '20 at 23:02
  • @a_sid Look at where it's *called* and look at the comments. The caller wants to know the maximum sum through either side of the tree, not both. `max_top` includes both sides of the current subtree; it could be the max subpath sum, but it can't be part of a higher node's subpath sum. – Caleb Dec 10 '20 at 01:18
  • @Caleb I see that. I don't understand the rationale behind it. – a_sid Dec 10 '20 at 02:11
  • @a_sid Draw a binary tree and pick any two nodes; there is exactly one *path* (direct route) between those nodes. A path that includes the root can include no more than one node at any given level in the left subtree, and no more than one node at any level in the right subtree. Once you can see why a path through the root can't include e.g. two leaves in the same subtree, I think you'll see why `findUtilMax()` returns `max_single` and not `max_top`. – Caleb Dec 10 '20 at 05:35
  • @Caleb Thank you for the explanation and I understand that a path can only go through one node at every level of the tree. But I still do not understand **WHY it is necessary to return the maximum path** out of the right and left subtrees/children to the calling function. How is doing so contributing to the overall solution? – a_sid Dec 10 '20 at 07:14
  • @a_sid It's necessary because the calling function needs the max path sums of the left and right subtrees in order to calculate the path sums of 3 of the 4 possible paths that include its node. `findUtilMax` needs to save `max_top` in `res` if it's greater than the value that's already there, but it can't return `max_top` to the caller because `max_top` might include subpaths (`l + r + node.data`) and the corresponding path therefore *cannot* be part of the paths that the caller is considering. `max_single` only includes one subpath or the other so may be part of the caller's paths. – Caleb Dec 10 '20 at 08:07
  • @Caleb Ok. So would it be correct to say `max_single` is being returned because `max path sum going through root` as you say it in the answer is one of the possible scenarios that can yield the final maximum path sum? – a_sid Dec 10 '20 at 13:02
  • @a_sid Not necessarily through the root, but through the caller's node. Each recursive call to `findMaxUtil` is contemplating the set of possible paths through a different node. – Caleb Dec 10 '20 at 14:34
  • @Caleb Ok that is true – a_sid Dec 10 '20 at 20:10
4

You're missing the value of res.val. The algorithm is trying to explore the whole tree, using res.val equal to the maximum path length explored up till then. In each step it iterates recursively across the children and updates res.val with the maximum path length, if higher than the one already present.

Proof:

Assume your algorithm works with trees with height n. For trees with height n+1 there's a root and 2 sub trees of height n. Also consider that findMaxUtil works fine for i<=n and will return the maximum path, starting with the partial root of the sub trees.

So the maximum path in your tree with height n+1 is calculated as follows

  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

And finally the result is: findmaxUtil(newTree)=max(items 1:6).

Adriaan
  • 17,741
  • 7
  • 42
  • 75
Ehsan Gerayli
  • 495
  • 2
  • 9
  • Why'd you do steps 3 and 4? They seem to be contained within 5 already. And I take it that 6 is actually your final result? As in: Find the max of `subtree1`, then of `subtree2`, then the max between the two of them and their common root. – Adriaan Dec 07 '20 at 22:07
  • 2
    @Adriaan The final result is whichever of the six values calculated in 1 through 6 is largest. 3 & 4 are needed because either of them might be larger than the value calculated in 5. Let's say `subtree1` gives 5 and `root.data` is 2, but `subtree2` is -11. Then items 1-6 are: `{5, -11, 7, -9, -4, res.val}`, and the largest is either item 3 or `res.val`. – Caleb Dec 08 '20 at 15:00
3

Honestly I think the description on that website is very unclear. I'll try to convince you of the reasoning behind the algorithm as best I can.

We have a binary tree, with values at the nodes:

enter image description here

And we are looking for a path in that tree, a chain of connected nodes.

enter image description here

As it's a directed tree, any nonempty path consists of a lowest-depth node (i.e. the node in the path that is closest to the root of the tree), a path of zero or more nodes descending to the left of the lowest-depth node, and a path of zero or more nodes descending to the right of the lowest-depth node. In particular, somewhere in the tree there is a node that is the lowest-depth node in the maximum path. (Indeed, there might be more than one such path tied for equal value, and they might each have their own distinct lowest-depth node. That's fine. As long as there's at least one, that's what matters.)

(I've used "highest" in the diagram but I mean "lowest-depth". To be clear, any time I use "depth" or "descending" I'm talking about position in the tree. Any time I use "maximum" I'm talking about the value of a node or the sum of values of nodes in a path.)

enter image description here

So if we can find its lowest-depth node, we know the maximum value path is composed of the node itself, a sub-path of zero or more nodes descending from (and including) its left child, and a sub-path of zero or more nodes descending from (and including) its right child. It's a small step to conclude that the left and right descending paths must be the maximum value such descending path on each side. (If this isn't obvious, consider that whatever other path you picked, you could increase the total value by instead picking the maximum value descending path on that side.) If either or both of those paths would have a negative value then we just don't include any nodes at all on the negative side(s).

So we have a separate subproblem - given a subtree, what is the value of the maximum value path descending through its root? Well, it might just be the root itself, if all the paths rooted at its children have negative sum, or if it has no children. Otherwise it is the root plus the maximum value descending path of either of those rooted at its children. This subproblem could easily be answered on its own, but to avoid repeated traversals and redoing work we'll combine them both into one traversal of the tree.

Going back to the main problem, we know that some node is the lowest-depth node in the maximum value path. We're not even particularly concerned with knowing when we visit it - we're just going to recursively visit every node and find the maximum value path that has that path as its lowest-depth node, assured that at some point we will visit the one we want. At each node we calculate both the maximum value path starting at that point and descending within the subtree (max_single) and the maximum value path for which this node is the lowest-depth node in the path (max_top). The latter is found by taking the node and "gluing on" zero, one or both of the maximum descending-only paths through its children. (Since max_single is already the maximum value path descending from zero or one of the children, the only extra thing we need to consider is the path that goes through both children.) By calculating max_top at every node and keeping the largest value found in res.val, we guarantee that we will have found the largest of all values by the time we have finished traversing the tree. At every node we return max_single to use in the parent's calculations. And at the end of the algorithm we just pull out the answer from res.val.

Weeble
  • 17,058
  • 3
  • 60
  • 75
  • _It's a small step to conclude that the left and right descending paths must be the maximum value such descending path on each side. (If this isn't obvious, consider that whatever other path you picked, you could increase the total value by instead picking the maximum value descending path on that side.)_ I don't understand what you mean here. Are you stating that out of the left and right subtrees, picking one with the highest value will yield the maximum path sum? – a_sid Dec 10 '20 at 07:08
  • I'm saying that _when_ we are looking at the path in the whole graph with the maximum value, and in particular at its highest node in the graph, the part of the path lying in the left subtree _must be_ the highest value path descending from that point, and the part lying in the right subtree _must be_ the highest value path descending from that point. – Weeble Dec 10 '20 at 09:37
  • What do you mean by _highest node in the graph_? Do you mean the node with the largest value? I may not be understanding this correctly but how can the sums of the left and right descendants both be highest at the same time? – a_sid Dec 10 '20 at 12:55
  • It's at the start of the second paragraph: "a highest node (i.e. the node in the path that is closest to the root of the tree)". – Weeble Dec 10 '20 at 13:43
  • I think I should have used the term "lowest depth" instead of "highest" to avoid confusion. – Weeble Dec 10 '20 at 15:01
  • 1
    Oh ok. So when you stated in your first comment that _the part of the path lying in the left subtree must be the highest value path descending from that point, and the part lying in the right subtree must be the highest value path descending from that point._, you meant to say out of **ALL THE PATHS that are available on the left of the parent node**, we should pick the left path with the maximum sum (the same is true for the right). Am I correct? – a_sid Dec 10 '20 at 19:37
  • _At every node we return `max_single` to use in the parent's calculations._ Why is it necessary to do this? – a_sid Dec 10 '20 at 20:57
  • Your last two comments answer each other! When we are looking for the largest value paths that descend into each of the left and right subtrees, those are exactly what `max_single` tells us! That's different from knowing the maximum value path *anywhere* in the subtree. `max_single` is only the maximum value out of all the paths that terminate at the root of the subtree. – Weeble Dec 10 '20 at 21:13
  • _max_single is only the maximum value out of all the paths that terminate at the root of the subtree._ I know that. But why do we need `max_single`? – a_sid Dec 11 '20 at 02:45
  • If I could, I would choose your answer as the BA too. @Caleb answered the question a little earlier and his answer was focused on the code and my actual source of confusion. – a_sid Dec 11 '20 at 19:37
  • 1
    No worries, it's not a competition. :D As long as my answer and something helpful I'm happy. – Weeble Dec 12 '20 at 16:14