Facebook Pixel

2973. Find Number of Coins to Place in Tree Nodes

Problem Description

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

Each node has an associated cost given in a 0-indexed integer array cost of length n, where cost[i] represents the cost value of node i.

Your task is to determine how many coins to place at each node according to these rules:

  1. If the subtree of node i contains fewer than 3 nodes: Place exactly 1 coin at node i.

  2. If the subtree of node i contains 3 or more nodes:

    • Select 3 distinct nodes from the subtree of node i
    • Calculate the product of their cost values
    • Place coins equal to the maximum possible product from all combinations of 3 nodes
    • If this maximum product is negative, place 0 coins instead

The subtree of a node includes the node itself and all its descendants.

Return an array coin of size n where coin[i] represents the number of coins to be placed at node i.

Example considerations:

  • When calculating the maximum product of 3 nodes, you need to consider that negative costs can exist
  • The maximum product might come from 3 positive values (largest three) or from 2 negative values and 1 positive value (two most negative × one most positive)
  • The tree is undirected but rooted at node 0, meaning we traverse it as a rooted tree structure

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 states we have 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 clearly states we're working with an undirected tree with n nodes and n-1 edges, rooted at node 0.

DFS

  • Yes: Since we identified this as a tree problem, the flowchart directs us to use DFS (Depth-First Search).

Why DFS is appropriate for this problem:

  • We need to process each node's subtree to calculate the number of coins to place
  • For each node, we need information about all nodes in its subtree (their cost values)
  • DFS naturally processes subtrees bottom-up, allowing us to collect information from children before processing the parent
  • The recursive nature of DFS perfectly matches the recursive structure of calculating subtree properties

Conclusion: The flowchart correctly identifies this as a tree problem and suggests using DFS. This aligns perfectly with the solution approach, where we use DFS to traverse the tree, collect cost values from each subtree, and calculate the maximum product of 3 nodes' costs for each subtree to determine the number of coins to place at each node.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to calculate something for each node based on its subtree, DFS is the natural choice because it processes children before parents, allowing us to aggregate information bottom-up.

The key insight is recognizing what information we actually need from each subtree. For any node, we need to find the maximum product of 3 distinct cost values from its subtree.

At first, you might think we need to keep track of all cost values in each subtree, but that would be inefficient. Instead, let's think about which values could possibly contribute to the maximum product:

  1. If all costs are positive: The maximum product comes from the 3 largest values.
  2. If we have negative costs: The maximum could come from either:
    • The 3 largest positive values (if they exist)
    • 2 smallest (most negative) values × 1 largest positive value (negative × negative = positive)

This means we don't need all values - we only need to track the 2 smallest and 3 largest values from each subtree. This is a crucial optimization.

The DFS approach naturally builds up this information:

  1. Start at a node and recursively visit all children
  2. Collect the important cost values (smallest 2 and largest 3) from each child's subtree
  3. Add the current node's cost to this collection
  4. Sort to identify the smallest and largest values
  5. Calculate the maximum product for this node using the formula: max(0, largest3 × largest2 × largest1, smallest1 × smallest2 × largest1)
  6. Pass up only the 2 smallest and 3 largest values to the parent (at most 5 values total)

This pruning strategy (keeping only 5 values) ensures that even as we traverse up the tree, we maintain constant space per recursive call while still having all the information needed to calculate the maximum product at any ancestor node.

Learn more about Tree, Depth-First Search, Dynamic Programming, Sorting and Heap (Priority Queue) patterns.

Solution Approach

The implementation follows a DFS pattern with optimization for maintaining only the necessary cost values from each subtree.

Data Structure Setup:

  • Build an adjacency list g where g[a] contains all neighbor nodes of node a
  • Initialize answer array ans with all values set to 1 (default for subtrees with less than 3 nodes)

Core DFS Function: The dfs(a, fa) function traverses the tree and returns an array containing the most relevant cost values from the subtree rooted at node a:

  1. Initialize result array: Start with res = [cost[a]] containing the current node's cost

  2. Traverse children: For each neighbor b of node a:

    • Skip if b is the parent node fa (to avoid going back up the tree)
    • Recursively call dfs(b, a) and extend res with the returned values
  3. Sort the collected costs: After collecting all costs from the subtree, sort res to easily identify smallest and largest values

  4. Calculate coins for current node:

    • If len(res) >= 3: Calculate the maximum product using two possible combinations:
      • res[-3] × res[-2] × res[-1]: Product of 3 largest values
      • res[0] × res[1] × res[-1]: Product of 2 smallest and 1 largest value
      • Take max(0, combination1, combination2) to handle negative products
    • Otherwise: Keep the default value of 1 coin
  5. Optimize the return values: If len(res) > 5, prune the array to keep only:

    • res[:2]: The 2 smallest values
    • res[-3:]: The 3 largest values

    This ensures we pass at most 5 values up the tree while maintaining all necessary information for parent calculations

Algorithm Execution:

  • Start DFS from root node 0 with parent -1: dfs(0, -1)
  • The function populates the ans array during traversal
  • Return the completed ans array

Time Complexity: O(n × log n) where n is the number of nodes, due to sorting at each node (but each value participates in at most O(log n) sorts as it moves up the tree)

Space Complexity: O(n) for the adjacency list and recursion stack, with each recursive call maintaining at most 5 cost values

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a small example to illustrate the solution approach.

Example Tree:

edges = [[0,1], [0,2], [2,3], [2,4]]
cost = [2, -1, 3, 5, -2]

Tree structure:
      0(2)
     / \
   1(-1) 2(3)
        / \
      3(5) 4(-2)

Step-by-step DFS traversal:

  1. Start at node 0, visit child 1:

    • Node 1 is a leaf, subtree has only 1 node
    • res = [-1], length < 3, so ans[1] = 1
    • Return [-1] to parent
  2. From node 0, visit child 2:

    2a. From node 2, visit child 3:

    • Node 3 is a leaf
    • res = [5], length < 3, so ans[3] = 1
    • Return [5] to parent

    2b. From node 2, visit child 4:

    • Node 4 is a leaf
    • res = [-2], length < 3, so ans[4] = 1
    • Return [-2] to parent

    2c. Back at node 2:

    • Collect costs from subtree: res = [3] (node 2's cost)
    • Add child 3's values: res = [3, 5]
    • Add child 4's values: res = [3, 5, -2]
    • Sort: res = [-2, 3, 5]
    • Length = 3, calculate products:
      • Three largest: 3 × 5 × (-2) = -30
      • Two smallest × one largest: (-2) × 3 × 5 = -30
      • ans[2] = max(0, -30, -30) = 0
    • Return [-2, 3, 5] to parent
  3. Back at node 0:

    • Collect costs from subtree: res = [2] (node 0's cost)
    • Add child 1's values: res = [2, -1]
    • Add child 2's values: res = [2, -1, -2, 3, 5]
    • Sort: res = [-2, -1, 2, 3, 5]
    • Length = 5, calculate products:
      • Three largest: 2 × 3 × 5 = 30
      • Two smallest × one largest: (-2) × (-1) × 5 = 10
      • ans[0] = max(0, 30, 10) = 30
    • Since this is the root, we don't need to return values

Final Result: ans = [30, 1, 0, 1, 1]

Key Observations:

  • Leaf nodes (1, 3, 4) have subtrees with < 3 nodes, so they get 1 coin
  • Node 2's subtree has exactly 3 nodes, but the maximum product is negative, so it gets 0 coins
  • Node 0's subtree has 5 nodes, and the maximum product (2 × 3 × 5 = 30) comes from the three largest positive values
  • The optimization of keeping only 5 values (2 smallest + 3 largest) when returning from node 2 would still preserve all necessary information for calculating node 0's result

Solution Implementation

1class Solution:
2    def placedCoins(self, edges: List[List[int]], cost: List[int]) -> List[int]:
3        """
4        Calculate the maximum product of 3 costs for each subtree in a tree.
5      
6        Args:
7            edges: List of edges representing the tree structure
8            cost: List of costs for each node
9          
10        Returns:
11            List where each element is either 1 (if subtree has < 3 nodes) 
12            or the maximum product of 3 costs in that subtree
13        """
14      
15        def dfs(node: int, parent: int) -> List[int]:
16            """
17            Perform DFS to collect costs from subtree and calculate maximum product.
18          
19            Args:
20                node: Current node being visited
21                parent: Parent of current node (to avoid revisiting)
22              
23            Returns:
24                List of important costs from this subtree (at most 5 values)
25            """
26            # Start with current node's cost
27            subtree_costs = [cost[node]]
28          
29            # Traverse all children (excluding parent)
30            for child in adjacency_list[node]:
31                if child != parent:
32                    # Recursively get costs from child's subtree
33                    child_costs = dfs(child, node)
34                    subtree_costs.extend(child_costs)
35          
36            # Sort costs to easily find minimum and maximum values
37            subtree_costs.sort()
38          
39            # Calculate maximum product if we have at least 3 nodes
40            if len(subtree_costs) >= 3:
41                # Maximum product could be:
42                # 1. Product of 3 largest values (all positive)
43                # 2. Product of 2 smallest (negative) and 1 largest (positive)
44                # 3. Zero (if no positive product possible)
45                max_product = max(
46                    subtree_costs[-3] * subtree_costs[-2] * subtree_costs[-1],  # 3 largest
47                    subtree_costs[0] * subtree_costs[1] * subtree_costs[-1],    # 2 smallest, 1 largest
48                    0  # Non-negative constraint
49                )
50                result[node] = max_product
51          
52            # Optimization: Keep only the most relevant values for parent calculations
53            # We only need 2 smallest and 3 largest values for future products
54            if len(subtree_costs) > 5:
55                subtree_costs = subtree_costs[:2] + subtree_costs[-3:]
56          
57            return subtree_costs
58      
59        # Initialize variables
60        n = len(cost)
61        adjacency_list = [[] for _ in range(n)]
62      
63        # Build adjacency list from edges
64        for node_a, node_b in edges:
65            adjacency_list[node_a].append(node_b)
66            adjacency_list[node_b].append(node_a)
67      
68        # Initialize result array with 1 (default for subtrees with < 3 nodes)
69        result = [1] * n
70      
71        # Start DFS from node 0 with no parent (-1)
72        dfs(0, -1)
73      
74        return result
75
1class Solution {
2    // Store the cost array for each node
3    private int[] cost;
4    // Adjacency list representation of the tree
5    private List<Integer>[] adjacencyList;
6    // Result array to store maximum product for each subtree
7    private long[] result;
8
9    public long[] placedCoins(int[][] edges, int[] cost) {
10        int n = cost.length;
11        this.cost = cost;
12      
13        // Initialize result array with all 1s (default for subtrees with < 3 nodes)
14        result = new long[n];
15        Arrays.fill(result, 1);
16      
17        // Build adjacency list for the tree
18        adjacencyList = new List[n];
19        Arrays.setAll(adjacencyList, i -> new ArrayList<>());
20      
21        // Add edges to adjacency list (undirected graph)
22        for (int[] edge : edges) {
23            int nodeA = edge[0];
24            int nodeB = edge[1];
25            adjacencyList[nodeA].add(nodeB);
26            adjacencyList[nodeB].add(nodeA);
27        }
28      
29        // Start DFS from root node 0 with no parent (-1)
30        dfs(0, -1);
31      
32        return result;
33    }
34
35    /**
36     * Performs DFS traversal and calculates maximum product for each subtree
37     * @param currentNode - current node being processed
38     * @param parent - parent of current node to avoid revisiting
39     * @return list of costs in the subtree (optimized to keep only necessary values)
40     */
41    private List<Integer> dfs(int currentNode, int parent) {
42        // List to collect all costs in current subtree
43        List<Integer> subtreeCosts = new ArrayList<>();
44      
45        // Add current node's cost
46        subtreeCosts.add(cost[currentNode]);
47      
48        // Traverse all children (excluding parent)
49        for (int child : adjacencyList[currentNode]) {
50            if (child != parent) {
51                // Recursively get costs from child's subtree and add to current list
52                subtreeCosts.addAll(dfs(child, currentNode));
53            }
54        }
55      
56        // Sort costs to easily find maximum and minimum values
57        Collections.sort(subtreeCosts);
58        int subtreeSize = subtreeCosts.size();
59      
60        // Calculate maximum product if subtree has at least 3 nodes
61        if (subtreeSize >= 3) {
62            // Option 1: Product of three largest values
63            long productOfLargest = (long) subtreeCosts.get(subtreeSize - 1) * 
64                                    subtreeCosts.get(subtreeSize - 2) * 
65                                    subtreeCosts.get(subtreeSize - 3);
66          
67            // Option 2: Product of two smallest (negative) and one largest value
68            long productWithNegatives = (long) subtreeCosts.get(0) * 
69                                        subtreeCosts.get(1) * 
70                                        subtreeCosts.get(subtreeSize - 1);
71          
72            // Store maximum of 0 and the two product options
73            result[currentNode] = Math.max(0, Math.max(productOfLargest, productWithNegatives));
74        }
75      
76        // Optimization: Keep only the values needed for parent calculations
77        // We need at most 2 smallest and 3 largest values
78        if (subtreeSize >= 5) {
79            subtreeCosts = List.of(
80                subtreeCosts.get(0),                // smallest
81                subtreeCosts.get(1),                // second smallest
82                subtreeCosts.get(subtreeSize - 3),  // third largest
83                subtreeCosts.get(subtreeSize - 2),  // second largest
84                subtreeCosts.get(subtreeSize - 1)   // largest
85            );
86        }
87      
88        return subtreeCosts;
89    }
90}
91
1class Solution {
2public:
3    vector<long long> placedCoins(vector<vector<int>>& edges, vector<int>& cost) {
4        int n = cost.size();
5        // Initialize result array with 1 for each node (default value for subtrees with < 3 nodes)
6        vector<long long> result(n, 1);
7      
8        // Build adjacency list representation of the tree
9        vector<vector<int>> adjacencyList(n);
10        for (const auto& edge : edges) {
11            int nodeA = edge[0];
12            int nodeB = edge[1];
13            adjacencyList[nodeA].push_back(nodeB);
14            adjacencyList[nodeB].push_back(nodeA);
15        }
16      
17        // DFS function to process each subtree and return important values
18        function<vector<int>(int, int)> dfs = [&](int currentNode, int parentNode) -> vector<int> {
19            // Start with current node's cost
20            vector<int> subtreeCosts = {cost[currentNode]};
21          
22            // Collect costs from all child subtrees
23            for (int childNode : adjacencyList[currentNode]) {
24                if (childNode != parentNode) {
25                    vector<int> childSubtreeCosts = dfs(childNode, currentNode);
26                    subtreeCosts.insert(subtreeCosts.end(), 
27                                       childSubtreeCosts.begin(), 
28                                       childSubtreeCosts.end());
29                }
30            }
31          
32            // Sort costs to easily find maximum and minimum values
33            sort(subtreeCosts.begin(), subtreeCosts.end());
34            int subtreeSize = subtreeCosts.size();
35          
36            // Calculate maximum product if we have at least 3 nodes
37            if (subtreeSize >= 3) {
38                // Option 1: Product of three largest values
39                long long productOfThreeLargest = 1LL * subtreeCosts[subtreeSize - 1] * 
40                                                        subtreeCosts[subtreeSize - 2] * 
41                                                        subtreeCosts[subtreeSize - 3];
42              
43                // Option 2: Product of two smallest (negative) and one largest value
44                long long productOfTwoSmallestOneLargest = 1LL * subtreeCosts[0] * 
45                                                                 subtreeCosts[1] * 
46                                                                 subtreeCosts[subtreeSize - 1];
47              
48                // Take maximum of 0, option 1, and option 2
49                result[currentNode] = max({0LL, productOfThreeLargest, productOfTwoSmallestOneLargest});
50            }
51          
52            // Optimization: Keep only the 5 most important values for parent calculations
53            // (2 smallest and 3 largest values are sufficient for any product calculation)
54            if (subtreeSize >= 5) {
55                subtreeCosts = {subtreeCosts[0], 
56                               subtreeCosts[1], 
57                               subtreeCosts[subtreeSize - 1], 
58                               subtreeCosts[subtreeSize - 2], 
59                               subtreeCosts[subtreeSize - 3]};
60            }
61          
62            return subtreeCosts;
63        };
64      
65        // Start DFS from node 0 with no parent (-1)
66        dfs(0, -1);
67      
68        return result;
69    }
70};
71
1/**
2 * Calculates the maximum product of three costs for each node in a tree
3 * @param edges - Array of edges representing tree connections
4 * @param cost - Array of costs for each node
5 * @returns Array containing maximum product of three costs for each subtree
6 */
7function placedCoins(edges: number[][], cost: number[]): number[] {
8    const nodeCount = cost.length;
9    const result: number[] = Array(nodeCount).fill(1);
10  
11    // Build adjacency list representation of the tree
12    const adjacencyList: number[][] = Array.from({ length: nodeCount }, () => []);
13    for (const [nodeA, nodeB] of edges) {
14        adjacencyList[nodeA].push(nodeB);
15        adjacencyList[nodeB].push(nodeA);
16    }
17  
18    /**
19     * Performs DFS traversal to collect costs from subtree and calculate maximum products
20     * @param currentNode - Current node being processed
21     * @param parentNode - Parent of current node to avoid revisiting
22     * @returns Array of selected costs from the subtree (at most 5 values)
23     */
24    const dfs = (currentNode: number, parentNode: number): number[] => {
25        // Start with current node's cost
26        const subtreeCosts: number[] = [cost[currentNode]];
27      
28        // Traverse all children (excluding parent)
29        for (const childNode of adjacencyList[currentNode]) {
30            if (childNode !== parentNode) {
31                subtreeCosts.push(...dfs(childNode, currentNode));
32            }
33        }
34      
35        // Sort costs in ascending order
36        subtreeCosts.sort((a, b) => a - b);
37        const costCount = subtreeCosts.length;
38      
39        // Calculate maximum product if we have at least 3 costs
40        if (costCount >= 3) {
41            // Option 1: Product of three largest values
42            const threeLargest = subtreeCosts[costCount - 1] * 
43                                subtreeCosts[costCount - 2] * 
44                                subtreeCosts[costCount - 3];
45          
46            // Option 2: Product of two smallest (negative) and one largest
47            const twoSmallestOneLargest = subtreeCosts[0] * 
48                                          subtreeCosts[1] * 
49                                          subtreeCosts[costCount - 1];
50          
51            // Store maximum of 0 and both options
52            result[currentNode] = Math.max(0, threeLargest, twoSmallestOneLargest);
53        }
54      
55        // Keep only the most relevant values (2 smallest and 3 largest)
56        // This optimization prevents unnecessary memory usage
57        if (costCount > 5) {
58            subtreeCosts.splice(2, costCount - 5);
59        }
60      
61        return subtreeCosts;
62    };
63  
64    // Start DFS from node 0 with no parent (-1)
65    dfs(0, -1);
66  
67    return result;
68}
69

Time and Space Complexity

Time Complexity: O(n × log n)

The algorithm performs a depth-first search (DFS) traversal of the tree starting from node 0. During the traversal:

  • Each node is visited exactly once, contributing O(n) to the traversal itself
  • At each node, we collect costs from all descendants in its subtree and sort them
  • The sorting operation at each node takes O(k × log k) where k is the size of the subtree rooted at that node
  • In the worst case (a linear tree), the root node would need to sort all n elements, giving O(n × log n)
  • While multiple sorts occur across different nodes, the amortized analysis shows that each element participates in sorting operations along its path to the root
  • The dominant operation is the sorting at nodes with large subtrees, particularly near the root

Therefore, the overall time complexity is O(n × log n).

Space Complexity: O(n)

The space usage consists of:

  • The adjacency list g which stores all edges: O(n) (since it's a tree with n-1 edges)
  • The answer array ans: O(n)
  • The recursion call stack depth: O(n) in the worst case (linear tree)
  • The res list at each recursive call: Although we collect costs from subtrees, we trim the list to at most 5 elements (res = res[:2] + res[-3:]), so each call maintains at most O(1) additional space for the trimmed result
  • The total space for all res lists across the recursion is bounded by O(n)

Therefore, the overall space complexity is O(n).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Forgetting to Handle Negative Products

Issue: A common mistake is only considering the product of the three largest values when calculating the maximum product. This fails when negative values are present in the cost array.

Why it happens: With negative costs, the maximum product might come from multiplying two negative values (which gives a positive) with one positive value. For example, if costs are [-10, -5, 1, 2, 3], the maximum product is (-10) × (-5) × 3 = 150, not 1 × 2 × 3 = 6.

Solution: Always check both combinations:

# Incorrect approach
max_product = subtree_costs[-3] * subtree_costs[-2] * subtree_costs[-1]

# Correct approach
max_product = max(
    subtree_costs[-3] * subtree_costs[-2] * subtree_costs[-1],  # 3 largest
    subtree_costs[0] * subtree_costs[1] * subtree_costs[-1],    # 2 smallest, 1 largest
    0  # Handle negative products
)

Pitfall 2: Not Optimizing the Returned Cost Array

Issue: Returning all collected costs from each subtree without pruning leads to exponential growth in the size of arrays being passed up the tree, causing time and memory limit exceeded errors.

Why it happens: In a deep tree, without pruning, each node would accumulate all costs from its entire subtree. A parent node would then need to sort and process increasingly large arrays.

Solution: After sorting, keep only the values that matter for future calculations:

# Inefficient - returns entire subtree
return subtree_costs

# Efficient - returns only necessary values
if len(subtree_costs) > 5:
    subtree_costs = subtree_costs[:2] + subtree_costs[-3:]
return subtree_costs

Pitfall 3: Incorrect Tree Traversal

Issue: Treating the undirected edges as a directed tree without properly tracking the parent node, leading to infinite recursion or incorrect subtree calculations.

Why it happens: The edges represent an undirected graph, but we need to traverse it as a rooted tree. Without tracking the parent, DFS might revisit nodes.

Solution: Pass the parent node in DFS and skip it during traversal:

def dfs(node, parent):
    for child in adjacency_list[node]:
        if child != parent:  # Critical check to avoid going back to parent
            child_costs = dfs(child, node)

Pitfall 4: Off-by-One Errors in Array Indexing

Issue: Using incorrect indices when accessing the smallest or largest values after sorting, especially when the array has exactly 3 elements.

Why it happens: Confusion about zero-based indexing and negative indexing in Python.

Solution: Remember that for a sorted array:

  • arr[0] and arr[1] are the two smallest values
  • arr[-1], arr[-2], and arr[-3] are the three largest values (in ascending order)
# For array [1, 2, 3, 4, 5]:
# arr[0] = 1 (smallest)
# arr[1] = 2 (second smallest)
# arr[-3] = 3 (third largest)
# arr[-2] = 4 (second largest)
# arr[-1] = 5 (largest)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of these properties could exist for a graph but not a tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More