Maximum Score After Applying Operations on a Tree

MEDIUM

Description

There is an undirected tree with n nodes labeled from 0 to n - 1, and rooted at node 0. You are given a 2D integer array edges of length n - 1, where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the tree.

You are also given a 0-indexed integer array values of length n, where values[i] is the value associated with the ith node.

You start with a score of 0. In one operation, you can:

  • Pick any node i.
  • Add values[i] to your score.
  • Set values[i] to 0.

A tree is healthy if the sum of values on the path from the root to any leaf node is different than zero.

Return the maximum score you can obtain after performing these operations on the tree any number of times so that it remains healthy.

 

Example 1:

Input: edges = [[0,1],[0,2],[0,3],[2,4],[4,5]], values = [5,2,5,2,1,1]
Output: 11
Explanation: We can choose nodes 1, 2, 3, 4, and 5. The value of the root is non-zero. Hence, the sum of values on the path from the root to any leaf is different than zero. Therefore, the tree is healthy and the score is values[1] + values[2] + values[3] + values[4] + values[5] = 11.
It can be shown that 11 is the maximum score obtainable after any number of operations on the tree.

Example 2:

Input: edges = [[0,1],[0,2],[1,3],[1,4],[2,5],[2,6]], values = [20,10,9,7,4,3,5]
Output: 40
Explanation: We can choose nodes 0, 2, 3, and 4.
- The sum of values on the path from 0 to 4 is equal to 10.
- The sum of values on the path from 0 to 3 is equal to 10.
- The sum of values on the path from 0 to 5 is equal to 3.
- The sum of values on the path from 0 to 6 is equal to 5.
Therefore, the tree is healthy and the score is values[0] + values[2] + values[3] + values[4] = 40.
It can be shown that 40 is the maximum score obtainable after any number of operations on the tree.

 

Constraints:

  • 2 <= n <= 2 * 104
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= ai, bi < n
  • values.length == n
  • 1 <= values[i] <= 109
  • The input is generated such that edges represents a valid tree.

Approaches

Checkout 2 different approaches to solve Maximum Score After Applying Operations on a Tree. Click on different approaches to view the approach and algorithm in detail.

Dynamic Programming on Tree

A more efficient approach uses dynamic programming on the tree. The key insight is to reframe the problem: instead of maximizing the score of nodes we take, we can minimize the sum of values of nodes we don't take. The 'healthy' condition requires that on every path from the root to a leaf, at least one node's value must be left untouched.

This structure lends itself to a post-order traversal (DFS) where for each node, we compute the minimum value that must be left in its subtree to satisfy the healthy condition for all paths starting from that node. This avoids re-computation and explores the decision space efficiently.

Algorithm

  • The problem of maximizing the score of taken nodes is equivalent to minimizing the sum of values of nodes that are left untouched, subject to the 'healthy' constraint.
  • The healthy constraint means that for every root-to-leaf path, at least one node must be left untouched.
  • We can define a function, minCost(u), which represents the minimum sum of values we must leave in the subtree rooted at u to ensure all paths from u to leaves within its subtree are 'healthy'.
  • We can compute minCost(u) using a post-order traversal (DFS).
  • Base Case: For a leaf node u, the only path in its subtree is the node itself. To make this path healthy, we must leave values[u]. So, minCost(u) = values[u].
  • Recursive Step: For an internal node u, we have two choices:
    1. Leave values[u]. The cost is values[u]. This satisfies the condition for all paths from u downwards.
    2. Take values[u]. Now, for each child v of u, we must satisfy the condition within v's subtree. The total cost for this choice is the sum of minCost(v) for all children v.
  • The recurrence relation is minCost(u) = min(values[u], sum(minCost(v) for v in children(u))).
  • First, calculate the total sum of all values in the tree.
  • Then, run the DFS from the root (node 0) to compute minCost(0).
  • The final maximum score is totalSum - minCost(0).

We can solve this problem optimally in linear time using a single DFS traversal. Let's define dfs(u, p) as a function that computes the minimum sum of values that must be left in the subtree rooted at u (with p being its parent) to ensure that any path from u to a leaf in its subtree has a non-zero sum.

  • For a leaf node u: The only path is the node itself. We must leave its value. So, dfs(u, p) returns values[u].
  • For an internal node u: We have two options:
    1. Leave values[u]: The cost incurred is values[u]. Since u is on every path starting from itself, this single choice makes all paths from u to its descendant leaves healthy.
    2. Take values[u]: We don't leave values[u]. To maintain the healthy property, we must now ensure it's satisfied in the subtrees of its children. Since the subtrees are independent, we must pay the minimum cost for each child's subtree. The total cost is the sum of the results of dfs(v, u) for all children v of u.

We choose the option with the minimum cost. Thus, for an internal node u, dfs(u, p) = min(values[u], sum(dfs(v, u))) for all children v.

The final answer is the total sum of all values in the tree minus the minimum cost for the entire tree, which is dfs(0, -1).

import java.util.ArrayList; 
import java.util.List;

class Solution {
    private List<List<Integer>> adj;
    private int[] values;

    public long maximumScoreAfterOperations(int[][] edges, int[] values) {
        int n = values.length;
        this.adj = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            adj.add(new ArrayList<>());
        }
        for (int[] edge : edges) {
            adj.get(edge[0]).add(edge[1]);
            adj.get(edge[1]).add(edge[0]);
        }
        this.values = values;

        long totalSum = 0;
        for (int value : values) {
            totalSum += value;
        }

        long minUnpickedSum = dfs(0, -1);
        return totalSum - minUnpickedSum;
    }

    /**
     * Computes the minimum sum of values to leave in the subtree of u
     * to make all paths from u to its leaves healthy.
     */
    private long dfs(int u, int p) {
        long childrenSubtreeMinSum = 0;
        boolean isLeaf = true;

        for (int v : adj.get(u)) {
            if (v != p) {
                isLeaf = false;
                childrenSubtreeMinSum += dfs(v, u);
            }
        }

        // Base case: if the node is a leaf in the rooted tree traversal.
        if (isLeaf) {
            return (long)values[u];
        }

        // Recursive step: for an internal node, choose the minimum of two options.
        return Math.min((long)values[u], childrenSubtreeMinSum);
    }
}

Complexity Analysis

Time Complexity: O(n). We visit each node and edge once to build the adjacency list, and once during the DFS traversal. Summing the initial values also takes O(n).Space Complexity: O(n) for storing the adjacency list and for the recursion stack in the worst-case scenario (a skewed tree).

Pros and Cons

Pros:
  • Optimal time complexity of O(n).
  • Efficiently solves the problem for large inputs.
  • Elegant solution based on a clear recurrence relation.
Cons:
  • Requires recognizing the problem can be transformed and solved with tree DP.
  • Slightly more complex to implement than a naive brute-force approach.

Code Solutions

Checking out 3 solutions in different languages for Maximum Score After Applying Operations on a Tree. Click on different languages to view the code.

class Solution {
private
  List<Integer>[] g;
private
  int[] values;
public
  long maximumScoreAfterOperations(int[][] edges, int[] values) {
    int n = values.length;
    g = new List[n];
    this.values = values;
    Arrays.setAll(g, k->new ArrayList<>());
    for (var e : edges) {
      int a = e[0], b = e[1];
      g[a].add(b);
      g[b].add(a);
    }
    return dfs(0, -1)[1];
  }
private
  long[] dfs(int i, int fa) {
    long a = 0, b = 0;
    boolean leaf = true;
    for (int j : g[i]) {
      if (j != fa) {
        leaf = false;
        var t = dfs(j, i);
        a += t[0];
        b += t[1];
      }
    }
    if (leaf) {
      return new long[]{values[i], 0};
    }
    return new long[]{values[i] + a, Math.max(values[i] + b, a)};
  }
}

Video Solution

Watch the video walkthrough for Maximum Score After Applying Operations on a Tree



Algorithms:

Depth-First Search

Patterns:

Dynamic Programming

Data Structures:

Tree

Subscribe to Scale Engineer newsletter

Learn about System Design, Software Engineering, and interview experiences every week.

No spam, unsubscribe at any time.