2378. Choose Edges to Maximize Score in a Tree 🔒
Problem Description
You have a weighted tree with n
nodes numbered from 0
to n - 1
, where node 0
is the root.
The tree is given as a 2D array edges
of size n
, where edges[i] = [par_i, weight_i]
means:
par_i
is the parent of nodei
- The edge connecting node
i
to its parent has weightweight_i
- For the root node (node 0),
edges[0] = [-1, -1]
since it has no parent
Your task is to select some edges from the tree such that:
- No two selected edges are adjacent (they cannot share a common node)
- The sum of weights of the selected edges is maximized
Return the maximum possible sum of weights. You can choose to select no edges at all, in which case the sum would be 0
.
Example of adjacent edges: If edge 1 connects nodes a
and b
, and edge 2 connects nodes b
and c
, then these two edges are adjacent because they share node b
.
The problem is essentially asking you to find the maximum weight independent set of edges in the tree, where no two edges in the set share a common vertex.
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 involves a weighted tree, which is a special type of graph. We have nodes (0 to n-1) and edges with weights connecting them.
Is it a tree?
- Yes: The problem clearly states we're working with a weighted tree. A tree is a connected acyclic graph with n nodes and n-1 edges, where each node (except the root) has exactly one parent.
DFS
- Yes: Since we're dealing with a tree structure, DFS (Depth-First Search) is the appropriate approach.
Conclusion: The flowchart suggests using DFS for this tree problem.
This makes perfect sense because:
- We need to explore the tree structure recursively from the root
- For each subtree, we need to make decisions about whether to include edges or not
- The problem requires computing optimal values for each subtree (dynamic programming on trees)
- DFS allows us to process children before parents, which is essential for bottom-up computation of the maximum score
The DFS approach combined with dynamic programming (tree DP) allows us to:
- Visit each node exactly once
- Calculate the maximum score for each subtree
- Track two states at each node: including or excluding the edge to its parent
- Build up the solution from leaves to root
Intuition
When we look at this problem, we need to select edges such that no two edges share a common node. This is a classic constraint that suggests we need to make local decisions at each node that will contribute to a global optimal solution.
Let's think about what happens at any node in the tree. For each node, we have an edge connecting it to its parent (except the root). The key insight is that we have two choices:
- Include the edge between this node and its parent
- Don't include the edge between this node and its parent
These two choices have different implications:
- If we include the edge to the parent, then we cannot include any edges from this node to its children (they would be adjacent and share this node)
- If we don't include the edge to the parent, then we have the freedom to potentially include one edge to one of our children
This naturally leads to a dynamic programming approach where we track two states for each node:
a
: Maximum score when we cannot use edges to children (because we used the edge to parent)b
: Maximum score when we can potentially use edges to children (because we didn't use the edge to parent)
For state a
: Since we can't use any edges to children, we simply sum up the best scores from all children when they have freedom to use their parent edges (their b
values).
For state b
: We can choose at most one edge to a child. So we try each child edge and see which gives us the maximum benefit. The benefit of choosing edge to child j
with weight w
is: w + (child j's score when it can't use parent edge) - (child j's score when it can use parent edge)
= w + x - y
.
Starting from the leaves and working our way up to the root using DFS, we can compute these values for each node. Since the root has no parent edge, we return the b
value of the root (the case where it has freedom to use edges to its children).
Learn more about Tree, Depth-First Search and Dynamic Programming patterns.
Solution Approach
Based on our intuition, we implement a tree dynamic programming solution using DFS. Here's how the solution works:
Data Structure Setup:
- We build an adjacency list
g
using adefaultdict(list)
to represent the tree structure - For each node
i
(starting from 1), we store its children as tuples(child_node, edge_weight)
- We skip
edges[0]
since it represents the root with no parent
DFS Function Design:
The dfs(i)
function computes two values for node i
:
a
: Maximum score when the edge between nodei
and its parent is selected (cannot select edges to children)b
: Maximum score when the edge between nodei
and its parent is not selected (can select at most one edge to a child)
Core Algorithm:
-
Initialize variables:
a = 0
: Score when parent edge is selectedb = 0
: Score when parent edge is not selectedt = 0
: Tracks the maximum benefit of selecting one child edge
-
Process each child
j
with edge weightw
:- Recursively call
dfs(j)
to get child's scores(x, y)
x
: Child's score when its parent edge is selectedy
: Child's score when its parent edge is not selected
- Recursively call
-
Update scores:
a += y
: If we select the parent edge of nodei
, we cannot select any edges to children, so we take each child'sy
valueb += y
: Start with the baseline of not selecting any child edgest = max(t, x - y + w)
: Calculate the benefit of selecting the edge to childj
. The benefit isw
(edge weight) plus the difference between selecting (x
) and not selecting (y
) the child's parent edge
-
Final calculation:
b += t
: Add the maximum benefit from selecting one child edge tob
- Return
(a, b)
Main Execution:
- Call
dfs(0)
for the root node - Return the second value
b
, which represents the maximum score when the root's (non-existent) parent edge is not selected
The time complexity is O(n)
as we visit each node once, and the space complexity is O(n)
for the recursion stack and adjacency list.
Ready to land your dream job?
Unlock your dream job with a 5-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:
0 (root) / \ 1 2 / 3
With edges array:
edges[0] = [-1, -1]
(root, no parent)edges[1] = [0, 5]
(node 1's parent is 0, edge weight = 5)edges[2] = [0, 3]
(node 2's parent is 0, edge weight = 3)edges[3] = [1, 2]
(node 3's parent is 1, edge weight = 2)
Step 1: Build adjacency list
g[0] = [(1, 5), (2, 3)]
(node 0 has children 1 and 2)g[1] = [(3, 2)]
(node 1 has child 3)g[2] = []
(node 2 is a leaf)g[3] = []
(node 3 is a leaf)
Step 2: DFS traversal starting from root (node 0)
Processing node 3 (leaf):
- No children, so
a = 0
,b = 0
,t = 0
- Return
(0, 0)
Processing node 2 (leaf):
- No children, so
a = 0
,b = 0
,t = 0
- Return
(0, 0)
Processing node 1:
- Has child 3 with edge weight 2
- Call
dfs(3)
→ returns(0, 0)
, sox = 0, y = 0
a = 0
(sum of children's y values)b = 0
(baseline)t = max(0, 0 - 0 + 2) = 2
(benefit of selecting edge to child 3)b = 0 + 2 = 2
- Return
(0, 2)
Processing node 0 (root):
-
Has children 1 (weight 5) and 2 (weight 3)
-
Call
dfs(1)
→ returns(0, 2)
, sox = 0, y = 2
a = 2
(running sum)b = 2
(running baseline)t = max(0, 0 - 2 + 5) = 3
(benefit of selecting edge to child 1)
-
Call
dfs(2)
→ returns(0, 0)
, sox = 0, y = 0
a = 2 + 0 = 2
(final sum)b = 2 + 0 = 2
(updated baseline)t = max(3, 0 - 0 + 3) = 3
(still edge to child 1 is best)
-
b = 2 + 3 = 5
-
Return
(2, 5)
Final answer: Return b = 5
from root
This corresponds to selecting edge (0→1) with weight 5. We cannot also select edge (1→3) with weight 2 because they share node 1, making them adjacent. The maximum possible sum is 5.
Solution Implementation
1from collections import defaultdict
2from typing import List
3
4class Solution:
5 def maxScore(self, edges: List[List[int]]) -> int:
6 """
7 Calculate the maximum score in a tree where each node can contribute to the score
8 through its edges. The tree is represented by edges array where edges[i] = [parent, weight]
9 for node i (node 0 is the root with no parent).
10
11 Args:
12 edges: List where edges[i] = [parent_of_i, weight_to_parent] for node i
13
14 Returns:
15 Maximum possible score
16 """
17
18 def dfs(node):
19 """
20 Perform DFS to calculate maximum scores for subtree rooted at 'node'.
21
22 Returns:
23 tuple: (score_without_using_node, score_with_or_without_using_node)
24 - First value: max score if we don't use any edge from this node
25 - Second value: max score considering we might use one edge from this node
26 """
27 # score_without: max score without using any edge from current node
28 # score_with: max score possibly using one edge from current node
29 score_without = 0
30 score_with = 0
31 max_edge_contribution = 0
32
33 # Process all children of current node
34 for child, edge_weight in adjacency_list[node]:
35 # Recursively get scores for child subtree
36 child_without, child_with = dfs(child)
37
38 # If we don't use current node's edges, we take the best from each child
39 score_without += child_with
40
41 # Base score (not using edge to this child)
42 score_with += child_with
43
44 # Track the maximum gain from using the edge to one specific child
45 # (child_without - child_with + edge_weight) represents the gain from using this edge
46 max_edge_contribution = max(max_edge_contribution,
47 child_without - child_with + edge_weight)
48
49 # Add the best edge contribution (if any) to score_with
50 score_with += max_edge_contribution
51
52 return score_without, score_with
53
54 # Build adjacency list representation of the tree
55 adjacency_list = defaultdict(list)
56
57 # Start from index 1 since node 0 is the root (has no parent)
58 for node_id, (parent_node, weight) in enumerate(edges[1:], start=1):
59 adjacency_list[parent_node].append((node_id, weight))
60
61 # Start DFS from root (node 0) and return the maximum score
62 _, max_score = dfs(0)
63 return max_score
64
1class Solution {
2 // Adjacency list to store the tree structure
3 // Each node maps to a list of [childNode, edgeWeight] pairs
4 private List<int[]>[] adjacencyList;
5
6 public long maxScore(int[][] edges) {
7 int nodeCount = edges.length;
8
9 // Initialize adjacency list for all nodes
10 adjacencyList = new List[nodeCount];
11 Arrays.setAll(adjacencyList, index -> new ArrayList<>());
12
13 // Build the tree structure (starting from node 1 since node 0 is root)
14 // edges[i][0] is the parent of node i, edges[i][1] is the edge weight
15 for (int childNode = 1; childNode < nodeCount; ++childNode) {
16 int parentNode = edges[childNode][0];
17 int edgeWeight = edges[childNode][1];
18 adjacencyList[parentNode].add(new int[] {childNode, edgeWeight});
19 }
20
21 // Start DFS from root node (0) and return the maximum score
22 return dfs(0)[1];
23 }
24
25 private long[] dfs(int currentNode) {
26 // scoreWithoutEdge: maximum score of subtree without selecting any edge to children
27 long scoreWithoutEdge = 0;
28
29 // scoreWithEdge: maximum score of subtree with potentially selecting one edge to a child
30 long scoreWithEdge = 0;
31
32 // maxGain: tracks the maximum gain from selecting one edge to a child
33 long maxGain = 0;
34
35 // Process all children of current node
36 for (int[] childInfo : adjacencyList[currentNode]) {
37 int childNode = childInfo[0];
38 int edgeWeight = childInfo[1];
39
40 // Recursively calculate scores for child subtree
41 long[] childScores = dfs(childNode);
42 long childScoreWithoutEdge = childScores[0];
43 long childScoreWithEdge = childScores[1];
44
45 // Add the best score from each child when not selecting current edge
46 scoreWithoutEdge += childScoreWithEdge;
47 scoreWithEdge += childScoreWithEdge;
48
49 // Calculate potential gain from selecting this edge
50 // Gain = edgeWeight + childScoreWithoutEdge - childScoreWithEdge
51 maxGain = Math.max(maxGain, childScoreWithoutEdge - childScoreWithEdge + edgeWeight);
52 }
53
54 // Add the maximum gain to scoreWithEdge (selecting at most one edge)
55 scoreWithEdge += maxGain;
56
57 // Return both scores: [without selecting edge, with selecting at most one edge]
58 return new long[] {scoreWithoutEdge, scoreWithEdge};
59 }
60}
61
1class Solution {
2public:
3 long long maxScore(vector<vector<int>>& edges) {
4 int n = edges.size();
5
6 // Build adjacency list: g[parent] = list of (child, weight) pairs
7 vector<vector<pair<int, int>>> adjacencyList(n);
8 for (int i = 1; i < n; ++i) {
9 int parent = edges[i][0];
10 int weight = edges[i][1];
11 adjacencyList[parent].emplace_back(i, weight);
12 }
13
14 using ll = long long;
15 using pll = pair<ll, ll>;
16
17 // DFS function returns pair: (score without taking any edge from subtree, max score with at most one edge from subtree)
18 function<pll(int)> dfs = [&](int node) -> pll {
19 ll scoreWithoutEdge = 0; // Score when not taking any edge from this subtree
20 ll scoreWithEdge = 0; // Score when taking at most one edge from this subtree
21 ll maxGain = 0; // Maximum gain from choosing one edge among all children
22
23 // Process all children of current node
24 for (auto& [child, edgeWeight] : adjacencyList[node]) {
25 auto [childWithoutEdge, childWithEdge] = dfs(child);
26
27 // Add the best score from each child (with at most one edge selected)
28 scoreWithoutEdge += childWithEdge;
29 scoreWithEdge += childWithEdge;
30
31 // Track the maximum gain if we choose the edge to this child
32 // Gain = score without taking edge from child + edge weight - score with edge from child
33 maxGain = max(maxGain, childWithoutEdge - childWithEdge + edgeWeight);
34 }
35
36 // Add the best gain from choosing one edge
37 scoreWithEdge += maxGain;
38
39 return make_pair(scoreWithoutEdge, scoreWithEdge);
40 };
41
42 // Return the maximum score starting from root (node 0)
43 return dfs(0).second;
44 }
45};
46
1function maxScore(edges: number[][]): number {
2 const n = edges.length;
3
4 // Build adjacency list: adjacencyList[parent] = list of [child, weight] pairs
5 const adjacencyList: number[][][] = Array(n).fill(null).map(() => []);
6 for (let i = 1; i < n; i++) {
7 const parent = edges[i][0];
8 const weight = edges[i][1];
9 adjacencyList[parent].push([i, weight]);
10 }
11
12 // DFS function returns tuple: [score without taking any edge from subtree, max score with at most one edge from subtree]
13 const dfs = (node: number): [number, number] => {
14 let scoreWithoutEdge = 0; // Score when not taking any edge from this subtree
15 let scoreWithEdge = 0; // Score when taking at most one edge from this subtree
16 let maxGain = 0; // Maximum gain from choosing one edge among all children
17
18 // Process all children of current node
19 for (const [child, edgeWeight] of adjacencyList[node]) {
20 const [childWithoutEdge, childWithEdge] = dfs(child);
21
22 // Add the best score from each child (with at most one edge selected)
23 scoreWithoutEdge += childWithEdge;
24 scoreWithEdge += childWithEdge;
25
26 // Track the maximum gain if we choose the edge to this child
27 // Gain = score without taking edge from child + edge weight - score with edge from child
28 maxGain = Math.max(maxGain, childWithoutEdge - childWithEdge + edgeWeight);
29 }
30
31 // Add the best gain from choosing one edge
32 scoreWithEdge += maxGain;
33
34 return [scoreWithoutEdge, scoreWithEdge];
35 };
36
37 // Return the maximum score starting from root (node 0)
38 return dfs(0)[1];
39}
40
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. For each node, the algorithm iterates through all its children, performing constant-time operations for each child edge. Since the total number of edges in a tree is n-1
(where n
is the number of nodes), and each edge is processed once when visiting its parent node, the total time complexity is O(n)
.
Space Complexity: O(n)
The space complexity consists of:
- The adjacency list
g
which stores all edges of the tree, requiringO(n)
space since there aren-1
edges in a tree withn
nodes - The recursion call stack for DFS, which in the worst case (a skewed tree) can go up to depth
n
, requiringO(n)
space - Local variables in each recursive call (
a
,b
,t
,x
,y
) useO(1)
space per call, totalingO(n)
across all recursive calls
Therefore, the overall space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding the Dynamic Programming States
A common mistake is incorrectly interpreting what the two DP states represent. Many developers initially think:
a
= maximum score when we select the edge from nodei
to its parentb
= maximum score when we don't select the edge from nodei
to its parent
However, the actual semantics are more nuanced:
a
= maximum score when the edge to nodei
(from its parent) is selectedb
= maximum score when the edge to nodei
(from its parent) is not selected
This distinction is crucial because when the parent edge is selected, we cannot select any edges to children (adjacency constraint).
2. Incorrectly Calculating the Benefit of Selecting a Child Edge
When calculating t = max(t, x - y + w)
, developers often make these errors:
- Using
x + w
instead ofx - y + w
: This fails to account for the opportunity cost. We need to subtracty
because that's what we would have gotten without selecting this edge. - Using
y - x + w
: This reverses the logic and will produce incorrect results.
Correct approach: The benefit is (score with child edge selected) - (baseline score without it)
= (x + w) - y
= x - y + w
3. Forgetting to Initialize the Maximum Benefit to 0
Setting t = 0
is important because it represents the option of not selecting any child edge. If you initialize it to negative infinity or skip initialization, you might force the selection of a negative-weight edge, which would decrease the total score.
4. Incorrect Tree Construction
A subtle but critical error is starting the enumeration from index 0 instead of 1:
# Wrong:
for node_id, (parent_node, weight) in enumerate(edges):
adjacency_list[parent_node].append((node_id, weight))
# Correct:
for node_id, (parent_node, weight) in enumerate(edges[1:], start=1):
adjacency_list[parent_node].append((node_id, weight))
The root node (index 0) has edges[0] = [-1, -1]
and should be skipped when building the tree.
5. Returning the Wrong Value
After calling dfs(0)
, some might return a
(first value) or max(a, b)
. The correct answer is always b
(second value) because:
- The root has no parent edge to select
b
represents the maximum score when we're free to choose whether to select edges to children- For the root, this freedom always applies
Solution to avoid these pitfalls:
- Clearly document what each state represents
- Trace through a small example by hand to verify the logic
- Pay attention to array indexing when building the tree structure
- Remember that for the root node, we always want the "unrestricted" state
What is the best way of checking if an element exists in a sorted array once in terms of time complexity? Select the best that applies.
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!