DP on Trees Maximum Path Sum in DSA Rust - Time & Space Complexity
We want to understand how the time needed grows when finding the maximum path sum in a tree using dynamic programming.
How does the number of steps change as the tree gets bigger?
Analyze the time complexity of the following code snippet.
fn max_path_sum(root: &Option>) -> i32 {
fn helper(node: &Option>, max_sum: &mut i32) -> i32 {
if let Some(n) = node {
let left = helper(&n.left, max_sum).max(0);
let right = helper(&n.right, max_sum).max(0);
*max_sum = (*max_sum).max(left + right + n.val);
left.max(right) + n.val
} else {
0
}
}
let mut max_sum = i32::MIN;
helper(root, &mut max_sum);
max_sum
}
This code finds the maximum sum of values along any path in a binary tree using recursion and dynamic programming.
Identify the loops, recursion, array traversals that repeat.
- Primary operation: Recursive calls visiting each node once.
- How many times: Each node is visited exactly one time.
As the tree grows, the number of nodes increases, and the function visits each node once.
| Input Size (n) | Approx. Operations |
|---|---|
| 10 | About 10 visits |
| 100 | About 100 visits |
| 1000 | About 1000 visits |
Pattern observation: The steps grow directly with the number of nodes, so doubling nodes doubles work.
Time Complexity: O(n)
This means the time needed grows linearly with the number of nodes in the tree.
[X] Wrong: "Because the function calls itself twice, the time is O(2^n)."
[OK] Correct: Each node is visited once, not repeatedly, so calls do not multiply exponentially.
Understanding this helps you explain how recursion and dynamic programming work together efficiently on trees.
"What if the tree was very unbalanced, like a linked list? How would the time complexity change?"
Practice
Solution
Step 1: Understand the definition of path sum
The path sum is the total of node values along a path connecting two nodes in the tree.Step 2: Identify the maximum path sum meaning
The maximum path sum is the largest such sum among all possible paths in the tree.Final Answer:
The largest sum of node values along any path between two nodes in the tree -> Option AQuick Check:
Maximum path sum = largest node value sum on any path [OK]
- Confusing maximum path sum with sum of all nodes
- Thinking it is just the max node value
- Counting number of nodes instead of sum
struct TreeNode { val: i32, left: Option, right: Option }?Solution
Step 1: Understand ownership and borrowing in Rust
Recursive tree functions usually borrow nodes to avoid ownership transfer and cloning.Step 2: Match function signature to tree node type
The tree node is wrapped in Option, so borrowing it as &Option is correct for recursion.Final Answer:
fn max_path_sum(node: &Option) -> i32 -> Option CQuick Check:
Borrow Option and return i32 [OK]
- Taking ownership of node causing move errors
- Returning Option<i32> unnecessarily
- Using &TreeNode instead of &Option
struct TreeNode {
val: i32,
left: Option,
right: Option,
}
fn max_gain(node: &Option, max_sum: &mut i32) -> i32 {
if let Some(n) = node {
let left_gain = max_gain(&n.left, max_sum).max(0);
let right_gain = max_gain(&n.right, max_sum).max(0);
let price_newpath = n.val + left_gain + right_gain;
*max_sum = (*max_sum).max(price_newpath);
n.val + left_gain.max(right_gain)
} else {
0
}
}
fn max_path_sum(root: &Option) -> i32 {
let mut max_sum = i32::MIN;
max_gain(root, &mut max_sum);
max_sum
}
fn main() {
let root = Some(Box::new(TreeNode {
val: -10,
left: Some(Box::new(TreeNode {
val: 9,
left: None,
right: None,
})),
right: Some(Box::new(TreeNode {
val: 20,
left: Some(Box::new(TreeNode {
val: 15,
left: None,
right: None,
})),
right: Some(Box::new(TreeNode {
val: 7,
left: None,
right: None,
})),
})),
}));
println!("{}", max_path_sum(&root));
}Solution
Step 1: Calculate max path sums for each subtree
Calculate max gains: - Left subtree (9): max gain = 9 - Right subtree: - Left child (15): max gain = 15 - Right child (7): max gain = 7 - Node 20: max gain = 20 + max(15,7) = 35 - Max path through 20: 20 + 15 + 7 = 42Step 2: Calculate max path including root (-10)
Root max gain = -10 + max(9, 35) = 25 Max path sum is max of: - Left subtree max: 9 - Right subtree max: 42 - Root path: 25 So max path sum = 42Final Answer:
42 -> Option AQuick Check:
Max path sum = 42 [OK]
- Ignoring negative values and not using max(0, gain)
- Adding gains incorrectly for path through root
- Confusing max gain with max path sum
fn max_gain(node: &Option, max_sum: &mut i32) -> i32 {
if let Some(n) = node {
let left_gain = max_gain(&n.left, max_sum);
let right_gain = max_gain(&n.right, max_sum);
let price_newpath = n.val + left_gain + right_gain;
*max_sum = (*max_sum).max(price_newpath);
n.val + left_gain.max(right_gain)
} else {
0
}
}Solution
Step 1: Analyze gain calculation for left and right subtrees
The function uses left_gain and right_gain directly without ignoring negative values, which can reduce the path sum.Step 2: Identify the fix for negative gains
Correct approach is to use max_gain = max(0, recursive_gain) to ignore negative paths that reduce sum.Final Answer:
It does not ignore negative gains by using max(0, gain) -> Option DQuick Check:
Use max(0, gain) to avoid negative path sums [OK]
- Forgetting to ignore negative subtree sums
- Updating max_sum incorrectly
- Returning wrong values from recursion
Solution
Step 1: Understand the root-included path sum requirement
The path must include the root node, so the path is root plus one subtree path (left or right), not both sides.Step 2: Modify the gain calculation accordingly
Return root.val + max(left_gain, right_gain) ignoring negative gains, and track max sum as this value.Final Answer:
Return the sum of the root value plus the maximum gain from either left or right subtree only, and track max sum accordingly -> Option BQuick Check:
Root-included max path = root + max(left, right) [OK]
- Adding both left and right gains when root must be included
- Ignoring negative gains
- Trying to filter paths after calculation
Ready to prove it?
Pass the test and this pattern moves unknown
