There exists an undirected tree rooted at node 0
with n
nodes labeled from 0
to n - 1
. 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 array coins
of size n
where coins[i]
indicates the number of coins in the vertex i
, and an integer k
.
Starting from the root, you have to collect all the coins such that the coins at a node can only be collected if the coins of its ancestors have been already collected.
Coins at nodei
can be collected in one of the following ways:
coins[i] - k
points. If coins[i] - k
is negative then you will lose abs(coins[i] - k)
points.floor(coins[i] / 2)
points. If this way is used, then for all the nodej
present in the subtree of nodei
, coins[j]
will get reduced to floor(coins[j] / 2)
.Return the maximum points you can get after collecting the coins from all the tree nodes.
Example 1:
Input: edges = [[0,1],[1,2],[2,3]], coins = [10,10,3,3], k = 5 Output: 11 Explanation: Collect all the coins from node 0 using the first way. Total points = 10 - 5 = 5. Collect all the coins from node 1 using the first way. Total points = 5 + (10 - 5) = 10. Collect all the coins from node 2 using the second way so coins left at node 3 will be floor(3 / 2) = 1. Total points = 10 + floor(3 / 2) = 11. Collect all the coins from node 3 using the second way. Total points = 11 + floor(1 / 2) = 11. It can be shown that the maximum points we can get after collecting coins from all the nodes is 11.
Example 2:
Input: edges = [[0,1],[0,2]], coins = [8,4,4], k = 0 Output: 16 Explanation: Coins will be collected from all the nodes using the first way. Therefore, total points = (8 - 0) + (4 - 0) + (4 - 0) = 16.
Constraints:
n == coins.length
2 <= n <= 105
0 <= coins[i] <= 104
edges.length == n - 1
0 <= edges[i][0], edges[i][1] < n
0 <= k <= 104
When you get asked this question in a real-life environment, it will often be ambiguous (especially at FAANG). Make sure to ask these questions in that case:
The brute force approach to this coin collection problem explores absolutely every possible path through the tree. We consider whether or not to collect coins at each node and then explore all the resulting options from that decision.
Here's how the algorithm would work step-by-step:
def maximum_points_brute_force(
node_index,
coins,
graph
):
# If node_index is out of bounds, return 0
if node_index is None:
return 0
# Option 1: Collect coins at the current node
collect_points = coins[node_index]
max_collect_path = 0
for neighbor in graph[node_index]:
max_collect_path += maximum_points_brute_force(
neighbor,
coins,
graph
)
collect_points += max_collect_path
# Option 2: Skip coins at the current node
skip_points = 0
max_skip_path = 0
for neighbor in graph[node_index]:
max_skip_path += maximum_points_brute_force(
neighbor,
coins,
graph
)
skip_points += max_skip_path
# Choose the path that gives maximum points.
return max(collect_points, skip_points)
def solve():
coins = [1, 2, 3, 4, 5]
graph = {
0: [1, 2],
1: [3],
2: [4],
3: [],
4: []
}
#Start traversal at root
return maximum_points_brute_force(
0,
coins,
graph
)
This problem involves figuring out the best way to collect coins from a tree-like structure. The smart approach avoids checking every possibility by working from the ground up and making decisions based on the best immediate reward, which will lead to the best overall score.
Here's how the algorithm would work step-by-step:
def maximum_points_after_collecting_coins(coins, edges):
number_of_nodes = len(coins)
graph = [[] for _ in range(number_of_nodes)]
for edge_start, edge_end in edges:
graph[edge_start].append(edge_end)
graph[edge_end].append(edge_start)
memo = {}
def solve(node, parent):
if (node, parent) in memo:
return memo[(node, parent)]
# Option 1: Take the coins from the current node
take_coins = coins[node]
for child in graph[node]:
if child != parent:
# Cannot take coins from children, so skip them
take_coins += solve(child, node)[1]
# Option 2: Skip the coins from the current node
skip_coins = 0
for child in graph[node]:
if child != parent:
# Can take or skip coins from children, choose the best
skip_coins += max(solve(child, node))
memo[(node, parent)] = (take_coins, skip_coins)
return take_coins, skip_coins
# Start DFS from node 0 (arbitrary root)
# with no parent (-1)
result = max(solve(0, -1))
return result
Case | How to Handle |
---|---|
Null or empty tree (no nodes) | Return 0 if the tree is null or empty as there are no coins to collect. |
Single node tree | Return the value of coins[0] as we can only collect from the single node. |
Tree with all coins values as 0 | Return 0 as no matter how we traverse, we collect 0 coins. |
All coins values are the same | The algorithm should still function correctly, potentially demonstrating symmetric optimal paths. |
Coins values are very large, potentially leading to integer overflow after multiple halvings | Use long data types or consider modulo operations if applicable to prevent overflow. |
Highly skewed tree (e.g., a linked list) | The algorithm's performance might degrade in highly skewed trees, requiring potential optimization for such cases. |
Deeply nested tree, potentially causing stack overflow with naive recursion | Consider using iterative approach with explicit stack or memoization to avoid stack overflow. |
Tree with negative coin values (although problem description implies non-negative) | The algorithm should handle this gracefully, possibly by modifying the base cases or the DP transitions to correctly handle negative coin values potentially subtracting from the total. |