{x}
blog image

Choose Edges to Maximize Score in a Tree

Solution Explanation

This problem asks to find the maximum sum of weights of edges in a tree such that no two chosen edges are adjacent. This can be efficiently solved using a dynamic programming approach with Depth-First Search (DFS).

Approach:

The core idea is to perform a post-order DFS traversal of the tree. For each node, we calculate two values:

  • a: The maximum score achievable by only selecting edges below the current node (in its subtree).
  • b: The maximum score achievable by selecting edges below the current node and potentially an edge connecting the current node to its parent.

The DFS function works as follows:

  1. Base Case: For leaf nodes, a and b are both 0 because there are no edges below them.

  2. Recursive Step: For internal nodes, the function iterates through its children. For each child:

    • It recursively calls dfs to get the a and b values from the child's subtree.
    • It updates a by adding the b value from the child (we can't choose the edge to the parent and the edge to the child simultaneously).
    • It updates b by adding the b value from the child.
  3. Edge Selection: The key step is choosing whether to include the edge connecting the current node to its parent. We calculate t, the maximum score including this edge. If the edge's weight added to a from the subtrees is greater than b, it means selecting this edge is beneficial. This is represented by max(t, x - y + w).

  4. Return Values: The function returns a and b for the current node. The final answer is the b value of the root node.

Time Complexity Analysis:

The DFS function visits each node and edge exactly once. Therefore, the time complexity is O(N), where N is the number of nodes in the tree. This is linear time complexity.

Space Complexity Analysis:

The space complexity is determined by the recursion depth of the DFS, which is at most the height of the tree. In the worst case (a skewed tree), the height could be N. Therefore, the space complexity is O(N) in the worst case, due to the recursive call stack. In practice, it's often much less than O(N).

Code Explanation (Python):

class Solution:
    def maxScore(self, edges: List[List[int]]) -> int:
        def dfs(i):
            a = b = t = 0
            for j, w in g[i]:
                x, y = dfs(j)
                a += y  # Add b from child (can't choose both parent and child edges)
                b += y  # Add b from child
                t = max(t, x - y + w) # Max score including edge to parent
            b += t # Update b to include parent edge if beneficial
            return a, b
 
        g = defaultdict(list)  # Adjacency list for the tree
        for i, (p, w) in enumerate(edges[1:], 1): #Build graph (skip root)
            g[p].append((i, w))
        return dfs(0)[1] # Return b value for root

The other languages (Java, C++, Go) follow a similar structure, implementing the same algorithm with their respective syntax and data structures. The defaultdict in Python and the ArrayList in Java efficiently handle variable-sized children for each node. The use of pairs in C++ and Go simplifies storing (child node, weight) pairs in the adjacency list.