2538. Difference Between Maximum and Minimum Price Sum
Problem Description
You have an undirected tree with n
nodes labeled from 0
to n - 1
. The tree structure is given by an array edges
where each edges[i] = [ai, bi]
represents an edge between nodes ai
and bi
. Each node has a price value given in the array price
, where price[i]
is the price of node i
.
The key concepts to understand:
-
Path Price Sum: For any path in the tree, the price sum is the total of all node prices along that path.
-
Rooting the Tree: You can choose any node as the root. Once a root is chosen, all paths are considered as starting from that root and going downward through the tree.
-
Cost of a Root: After choosing a root node, the cost is calculated as the difference between:
- The maximum price sum among all paths starting from the root
- The minimum price sum among all paths starting from the root
-
Objective: Find the maximum possible cost across all possible choices of root nodes.
For example, if you choose node x
as root and the paths from x
have price sums of [10, 15, 8, 20]
, then:
- Maximum price sum =
20
- Minimum price sum =
8
- Cost for this root =
20 - 8 = 12
You need to try all possible root nodes and return the highest cost achievable.
The solution uses DFS (Depth First Search) to explore the tree. For each node, it tracks two values:
a
: Maximum path sum including the current nodeb
: Maximum path sum excluding the current node (essentially the path ends at a child)
By maintaining these values and combining paths from different subtrees, the algorithm efficiently computes the maximum cost without explicitly trying every root.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem explicitly mentions an undirected tree with nodes and edges. A tree is a special type of graph (connected and acyclic).
Is it a tree?
- Yes: The problem states "There exists an undirected and initially unrooted tree with n nodes" and provides edges that form a tree structure.
DFS
- Conclusion: Since we have a tree structure, the flowchart directly leads us to use DFS (Depth-First Search).
The flowchart confirms that DFS is the appropriate algorithm for this problem. This makes sense because:
-
Tree Traversal: We need to explore all possible paths from each potential root node, which is a natural fit for DFS on trees.
-
Path Calculations: DFS allows us to recursively calculate path sums from leaves up to the root, maintaining the maximum and minimum values as we traverse.
-
Subtree Information: For each node, we need to gather information from its subtrees (children), which DFS handles efficiently through its recursive nature.
-
Re-rooting Technique: The solution uses DFS to compute results for all possible roots in a single traversal by tracking two values at each node:
- Maximum path sum including the current node
- Maximum path sum excluding the current node
This allows us to efficiently calculate the maximum difference between path sums without explicitly re-rooting the tree multiple times.
Conclusion: The flowchart correctly identifies DFS as the optimal approach for this tree-based path sum problem.
Intuition
The key insight is that we want to find the maximum difference between two paths starting from the same root. The minimum path sum from any root is always just the root itself (a path of length 0), so we're essentially looking for the maximum path sum from each potential root.
But here's the clever part: instead of trying every node as root separately (which would be inefficient), we can solve this in a single DFS traversal by thinking about paths differently.
For any node in the tree, a path passing through it can be viewed as:
- A path coming from one subtree, passing through the node, and potentially extending into another subtree
- The combination of the node with paths from its children
This leads us to track two important values for each node:
a
: The maximum path sum that includes the current node (the path starts from somewhere below and goes up to include this node)b
: The maximum path sum that excludes the current node (the path starts from somewhere below but doesn't include this node's price)
Why these two values? Because when we're at a node with multiple children:
- We can combine a path that includes prices from one child (
a
from child 1) with a path that excludes the current node from another child (b
from child 2) - Or vice versa (
b
from child 1 witha
from child 2)
This combination gives us the maximum "spread" or difference we're looking for. The path including the node represents our maximum sum, while implicitly the minimum is just the node itself.
The beauty of this approach is that as we traverse up from the leaves, we're constantly computing the best possible paths and their differences at each node, effectively considering all possible root choices in a single pass.
For example, if we're at node i
with children, and child j
returns (c, d)
:
c
represents the best path in childj
's subtree that includesj
d
represents the best path in childj
's subtree that excludesj
We can then combine these with paths from other children to find the maximum difference at node i
, which could be a candidate for our final answer.
Learn more about Tree, Depth-First Search and Dynamic Programming patterns.
Solution Approach
The implementation uses DFS with a clever state tracking mechanism. Let's walk through the key components:
Data Structure Setup:
- Build an adjacency list
g
usingdefaultdict(list)
to represent the tree - Since the tree is undirected, add edges in both directions
DFS Function Design:
The dfs(i, fa)
function processes node i
with parent fa
and returns two values:
a
: Maximum path sum that includes nodei
b
: Maximum path sum that excludes nodei
Base Case: For a leaf node (no children except parent):
a = price[i]
(path is just the node itself)b = 0
(empty path, excluding the node)
Recursive Case:
For each child j
of node i
:
-
Recursively call
dfs(j, i)
to get(c, d)
where:c
: Maximum path including childj
d
: Maximum path excluding childj
-
Update the global answer by considering two scenarios:
a + d
: Current best path includingi
combined with path excluding childj
b + c
: Current best path excludingi
combined with path including childj
These combinations represent complete paths through node
i
that maximize the difference. -
Update local values:
a = max(a, price[i] + c)
: Best path includingi
is either justi
itself ori
plus the best path from a childb = max(b, price[i] + d)
: Best path "excluding"i
actually means the path ends ati
but came from below, so we addprice[i]
to the child's excluded path
Key Insight in the Update Logic:
When we compute ans = max(ans, a + d, b + c)
, we're finding paths that:
- Start from one subtree
- Pass through node
i
- End at node
i
or continue to another subtree
The difference between including/excluding helps us track whether we're counting the node's price once (as an intermediate node) or as an endpoint.
Final Answer: Start DFS from any node (here node 0) with parent -1, and return the maximum difference found across all nodes during the traversal.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example to illustrate the solution approach.
Consider a tree with 4 nodes and prices:
- Nodes: 0, 1, 2, 3
- Edges: [[0,1], [1,2], [1,3]]
- Prices: [2, 3, 5, 4]
The tree structure looks like:
0 (price=2) | 1 (price=3) / \ 2 3 (5) (4)
Step 1: Build adjacency list
g[0] = [1] g[1] = [0, 2, 3] g[2] = [1] g[3] = [1]
Step 2: Start DFS from node 0
Call dfs(0, -1)
:
- Node 0 has one child: node 1
- Need to process
dfs(1, 0)
first
Step 3: Process dfs(1, 0)
Node 1 has children 2 and 3 (excluding parent 0).
First, process dfs(2, 1)
:
- Node 2 is a leaf (no children except parent 1)
- Returns
(a=5, b=0)
- Meaning: max path including node 2 is 5, excluding is 0
Next, process dfs(3, 1)
:
- Node 3 is a leaf
- Returns
(a=4, b=0)
Now at node 1:
- Initialize
a = price[1] = 3
,b = 0
Process child 2 with (c=5, d=0)
:
- Update answer:
max(ans, a+d, b+c) = max(0, 3+0, 0+5) = 5
- Update
a = max(3, 3+5) = 8
(path: 2â1) - Update
b = max(0, 3+0) = 3
Process child 3 with (c=4, d=0)
:
- Update answer:
max(ans, a+d, b+c) = max(5, 8+0, 3+4) = 8
- Update
a = max(8, 3+4) = 8
(still path: 2â1) - Update
b = max(3, 3+0) = 3
Node 1 returns (a=8, b=3)
to node 0.
Step 4: Complete dfs(0, -1)
At node 0:
- Initialize
a = price[0] = 2
,b = 0
- Process child 1 with
(c=8, d=3)
:- Update answer:
max(ans, a+d, b+c) = max(8, 2+3, 0+8) = 8
- Update
a = max(2, 2+8) = 10
(path: 2â1â0) - Update
b = max(0, 2+3) = 5
- Update answer:
Returns (a=10, b=5)
Final Answer: 8
The maximum cost is 8, which represents:
- Choosing node 1 as root
- Maximum path sum: 8 (path from node 2 to node 1)
- Minimum path sum: 0 (empty path, just considering paths going down from root)
- Cost = 8 - 0 = 8
The algorithm found this optimal value when processing node 1, considering the path from node 2 (price 5) through node 1 (price 3), giving us the maximum difference of 8.
Solution Implementation
1class Solution:
2 def maxOutput(self, n: int, edges: List[List[int]], price: List[int]) -> int:
3 from collections import defaultdict
4 from typing import List
5
6 def dfs(node: int, parent: int) -> tuple[int, int]:
7 """
8 DFS to calculate maximum path sums.
9
10 Returns:
11 - max_path_with_node: Maximum path sum starting from current node (including its price)
12 - max_path_without_node: Maximum path sum passing through current node (excluding its price)
13 """
14 # Initialize: path including current node starts with its price, path excluding it starts at 0
15 max_path_with_node = price[node]
16 max_path_without_node = 0
17
18 # Traverse all adjacent nodes (children in the tree)
19 for neighbor in adjacency_list[node]:
20 if neighbor != parent: # Skip the parent to avoid revisiting
21 # Recursively get the maximum paths from the child
22 child_with, child_without = dfs(neighbor, node)
23
24 # Update global answer by combining paths:
25 # 1. Current path with node + child path without its starting node
26 # 2. Current path without node + child path with its starting node
27 nonlocal max_result
28 max_result = max(max_result, max_path_with_node + child_without, max_path_without_node + child_with)
29
30 # Update local maximums:
31 # Path with current node = max of existing or current node price + child's path with child
32 max_path_with_node = max(max_path_with_node, price[node] + child_with)
33 # Path without current node = max of existing or current node price + child's path without child
34 max_path_without_node = max(max_path_without_node, price[node] + child_without)
35
36 return max_path_with_node, max_path_without_node
37
38 # Build adjacency list representation of the tree
39 adjacency_list = defaultdict(list)
40 for node_a, node_b in edges:
41 adjacency_list[node_a].append(node_b)
42 adjacency_list[node_b].append(node_a)
43
44 # Initialize the maximum result
45 max_result = 0
46
47 # Start DFS from node 0 with -1 as parent (no parent for root)
48 dfs(0, -1)
49
50 return max_result
51
1class Solution {
2 // Adjacency list to represent the tree
3 private List<Integer>[] adjacencyList;
4 // Maximum path sum result
5 private long maxPathSum;
6 // Node prices array
7 private int[] nodePrices;
8
9 public long maxOutput(int n, int[][] edges, int[] price) {
10 // Initialize the adjacency list for n nodes
11 adjacencyList = new List[n];
12 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
13
14 // Build the undirected tree from edges
15 for (int[] edge : edges) {
16 int nodeA = edge[0];
17 int nodeB = edge[1];
18 adjacencyList[nodeA].add(nodeB);
19 adjacencyList[nodeB].add(nodeA);
20 }
21
22 // Store the price array
23 this.nodePrices = price;
24
25 // Start DFS from node 0 with no parent (-1)
26 dfs(0, -1);
27
28 return maxPathSum;
29 }
30
31 /**
32 * DFS to calculate maximum path sums
33 * @param currentNode - current node being processed
34 * @param parent - parent node to avoid revisiting
35 * @return array of two values:
36 * [0] - max path sum including current node's price
37 * [1] - max path sum excluding current node's price
38 */
39 private long[] dfs(int currentNode, int parent) {
40 // maxWithCurrent: maximum path sum from current node including its price
41 long maxWithCurrent = nodePrices[currentNode];
42 // maxWithoutCurrent: maximum path sum from current node excluding its price
43 long maxWithoutCurrent = 0;
44
45 // Traverse all adjacent nodes (children in the tree)
46 for (int neighbor : adjacencyList[currentNode]) {
47 // Skip the parent node to avoid cycles
48 if (neighbor != parent) {
49 // Recursively get the maximum path sums from the child
50 long[] childResults = dfs(neighbor, currentNode);
51 long childMaxWithPrice = childResults[0];
52 long childMaxWithoutPrice = childResults[1];
53
54 // Update the global maximum by considering:
55 // 1. Current path with current node's price + child path without child's starting price
56 // 2. Current path without current node's price + child path with child's price
57 maxPathSum = Math.max(maxPathSum, Math.max(maxWithCurrent + childMaxWithoutPrice,
58 maxWithoutCurrent + childMaxWithPrice));
59
60 // Update local maximums:
61 // maxWithCurrent: best path including current node's price
62 maxWithCurrent = Math.max(maxWithCurrent, nodePrices[currentNode] + childMaxWithPrice);
63 // maxWithoutCurrent: best path excluding current node's price (but including it in the path)
64 maxWithoutCurrent = Math.max(maxWithoutCurrent, nodePrices[currentNode] + childMaxWithoutPrice);
65 }
66 }
67
68 // Return both maximum values for the parent's calculation
69 return new long[] {maxWithCurrent, maxWithoutCurrent};
70 }
71}
72
1class Solution {
2public:
3 long long maxOutput(int n, vector<vector<int>>& edges, vector<int>& price) {
4 // Build adjacency list representation of the tree
5 vector<vector<int>> adjacencyList(n);
6 for (auto& edge : edges) {
7 int nodeA = edge[0];
8 int nodeB = edge[1];
9 adjacencyList[nodeA].push_back(nodeB);
10 adjacencyList[nodeB].push_back(nodeA);
11 }
12
13 // Type aliases for clarity
14 using ll = long long;
15 using pll = pair<ll, ll>;
16
17 ll maxPathSum = 0;
18
19 // DFS function to calculate maximum path sums
20 // Returns a pair where:
21 // - first: maximum path sum starting from current node (including current node's price)
22 // - second: maximum path sum ending at current node (excluding current node's price)
23 function<pll(int, int)> dfs = [&](int currentNode, int parentNode) {
24 // Initialize values for current node
25 // maxWithCurrent: max path sum including current node at the start
26 ll maxWithCurrent = price[currentNode];
27 // maxWithoutCurrent: max path sum excluding current node at the start
28 ll maxWithoutCurrent = 0;
29
30 // Process all children (neighbors except parent)
31 for (int childNode : adjacencyList[currentNode]) {
32 if (childNode != parentNode) {
33 // Recursively calculate values for child subtree
34 auto [childMaxWith, childMaxWithout] = dfs(childNode, currentNode);
35
36 // Update global maximum by considering paths through current node
37 // Path can either:
38 // 1. Start from current subtree and end in child subtree
39 // 2. Start from child subtree and end in current subtree
40 maxPathSum = max({maxPathSum,
41 maxWithCurrent + childMaxWithout,
42 maxWithoutCurrent + childMaxWith});
43
44 // Update local maximums for paths starting/ending at current node
45 maxWithCurrent = max(maxWithCurrent, price[currentNode] + childMaxWith);
46 maxWithoutCurrent = max(maxWithoutCurrent, price[currentNode] + childMaxWithout);
47 }
48 }
49
50 return pll{maxWithCurrent, maxWithoutCurrent};
51 };
52
53 // Start DFS from node 0 with no parent (-1)
54 dfs(0, -1);
55
56 return maxPathSum;
57 }
58};
59
1function maxOutput(n: number, edges: number[][], price: number[]): number {
2 // Build adjacency list representation of the tree
3 const adjacencyList: number[][] = Array(n).fill(null).map(() => []);
4 for (const edge of edges) {
5 const nodeA = edge[0];
6 const nodeB = edge[1];
7 adjacencyList[nodeA].push(nodeB);
8 adjacencyList[nodeB].push(nodeA);
9 }
10
11 let maxPathSum = 0;
12
13 // DFS function to calculate maximum path sums
14 // Returns a tuple where:
15 // - first element: maximum path sum starting from current node (including current node's price)
16 // - second element: maximum path sum ending at current node (excluding current node's price)
17 const dfs = (currentNode: number, parentNode: number): [number, number] => {
18 // Initialize values for current node
19 // maxWithCurrent: max path sum including current node at the start
20 let maxWithCurrent = price[currentNode];
21 // maxWithoutCurrent: max path sum excluding current node at the start
22 let maxWithoutCurrent = 0;
23
24 // Process all children (neighbors except parent)
25 for (const childNode of adjacencyList[currentNode]) {
26 if (childNode !== parentNode) {
27 // Recursively calculate values for child subtree
28 const [childMaxWith, childMaxWithout] = dfs(childNode, currentNode);
29
30 // Update global maximum by considering paths through current node
31 // Path can either:
32 // 1. Start from current subtree and end in child subtree
33 // 2. Start from child subtree and end in current subtree
34 maxPathSum = Math.max(
35 maxPathSum,
36 maxWithCurrent + childMaxWithout,
37 maxWithoutCurrent + childMaxWith
38 );
39
40 // Update local maximums for paths starting/ending at current node
41 maxWithCurrent = Math.max(maxWithCurrent, price[currentNode] + childMaxWith);
42 maxWithoutCurrent = Math.max(maxWithoutCurrent, price[currentNode] + childMaxWithout);
43 }
44 }
45
46 return [maxWithCurrent, maxWithoutCurrent];
47 };
48
49 // Start DFS from node 0 with no parent (-1)
50 dfs(0, -1);
51
52 return maxPathSum;
53}
54
Time and Space Complexity
Time Complexity: O(n)
The algorithm performs a depth-first search (DFS) traversal of the tree. Each node is visited exactly once during the DFS traversal. At each node, the algorithm:
- Iterates through all adjacent nodes (children in the tree)
- Performs constant time operations for updating variables
a
,b
, andans
Since this is a tree with n
nodes and n-1
edges, the total number of edges traversed is O(n)
. Therefore, the overall time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- Graph representation: The adjacency list
g
stores all edges. Since there aren-1
edges in a tree and each edge is stored twice (bidirectional), this requiresO(n)
space. - Recursion call stack: In the worst case (when the tree is a linear chain), the DFS recursion can go up to depth
n
, requiringO(n)
stack space. - Local variables: The variables
a
,b
,c
,d
, andans
useO(1)
space at each recursion level.
The dominant factors are the graph storage and recursion stack, both being O(n)
, resulting in an overall space complexity of O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Misunderstanding the Return Values
The Problem:
A common mistake is misinterpreting what max_path_without_node
represents. Despite its name suggesting "without node", it actually includes the current node's price (price[node] + child_without
). This confuses many developers who expect it to completely exclude the current node.
Why It Happens: The variable naming is counterintuitive. When we say "without node", we're actually referring to paths that don't start from this node but pass through it as an intermediate point.
The Fix: Rename variables to better reflect their actual meaning:
def dfs(node: int, parent: int) -> tuple[int, int]:
# Better naming
max_path_starting_here = price[node] # Path starts from this node
max_path_through_here = 0 # Path passes through but doesn't start here
Pitfall 2: Incorrect Answer Updates
The Problem: Developers often update the answer incorrectly by trying combinations like:
max_result = max(max_result, child_with + child_without) # Wrong!
This doesn't properly account for the current node's contribution to the path.
Why It Happens: The logic for combining paths from different subtrees through the current node is subtle. We need to ensure we're creating valid paths that include the current node exactly once.
The Fix: Always combine one path that includes the current node with one that doesn't:
# Correct: Combine paths properly through current node
max_result = max(max_result,
max_path_with_node + child_without, # Path from one side including current
max_path_without_node + child_with) # Path from other side including current
Pitfall 3: Edge Case with Single Node
The Problem:
When the tree has only one node, the DFS never updates max_result
because there are no children to process, leading to a return value of 0 instead of the expected 0 (since max path = min path = node price).
Why It Happens: The answer is only updated when processing children. With no edges, the loop never executes.
The Fix: Handle the single node case explicitly:
def maxOutput(self, n: int, edges: List[List[int]], price: List[int]) -> int:
if n == 1:
return 0 # Single node: max_path = min_path = price[0], difference = 0
# Rest of the implementation...
Pitfall 4: Forgetting to Use Nonlocal
The Problem:
Forgetting to declare nonlocal max_result
inside the DFS function causes either an error or creates a local variable that shadows the outer one:
def dfs(node, parent):
max_result = max(max_result, ...) # UnboundLocalError without nonlocal
Why It Happens: Python's scoping rules require explicit declaration when modifying variables from outer scopes.
The Fix: Always declare nonlocal before modifying:
def dfs(node, parent):
nonlocal max_result # Declare before use
# ... rest of function
Complete Corrected Solution:
class Solution:
def maxOutput(self, n: int, edges: List[List[int]], price: List[int]) -> int:
# Handle edge case
if n == 1:
return 0
from collections import defaultdict
def dfs(node: int, parent: int) -> tuple[int, int]:
# Clearer variable names
path_from_here = price[node] # Max path starting from this node
path_through_here = 0 # Max path passing through (not starting from) this node
for child in adjacency_list[node]:
if child != parent:
child_from, child_through = dfs(child, node)
# Update answer with valid path combinations
nonlocal max_cost
max_cost = max(max_cost,
path_from_here + child_through,
path_through_here + child_from)
# Update local maximums
path_from_here = max(path_from_here, price[node] + child_from)
path_through_here = max(path_through_here, price[node] + child_through)
return path_from_here, path_through_here
adjacency_list = defaultdict(list)
for a, b in edges:
adjacency_list[a].append(b)
adjacency_list[b].append(a)
max_cost = 0
dfs(0, -1)
return max_cost
Which data structure is used to implement recursion?
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Donât Miss This!