Problem
There is an undirected tree with n
nodes labeled from 0
to n - 1
, 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.
At every node i
, there is a gate. You are also given an array of even integers amount
, where amount[i]
represents:
- the price needed to open the gate at node
i
, ifamount[i]
is negative, or, - the cash reward obtained on opening the gate at node
i
, otherwise.
The game goes on as follows:
- Initially, Alice is at node
0
and Bob is at nodebob
. - At every second, Alice and Bob each move to an adjacent node. Alice moves towards some leaf node, while Bob moves towards node
0
. - For every node along their path, Alice and Bob either spend money to open the gate at that node, or accept the reward. Note that:
- If the gate is already open, no price will be required, nor will there be any cash reward.
- If Alice and Bob reach the node simultaneously, they share the price/reward for opening the gate there. In other words, if the price to open the gate is
c
, then both Alice and Bob payc / 2
each. Similarly, if the reward at the gate isc
, both of them receivec / 2
each.
- If Alice reaches a leaf node, she stops moving. Similarly, if Bob reaches node
0
, he stops moving. Note that these events are independent of each other.
Return the maximum net income Alice can have if she travels towards the optimal leaf node._
Examples
Example 1:
graph TD 0("0
Amount: -2") 1("1
Amount: 4") 2("2
Amount: 2") 3("3
Amount: -4") 4("4
Amount: 6") 0 --- 1 1 --- 2 1 --- 3 3 --- 4
Input: edges = [[0,1],[1,2],[1,3],[3,4]], bob = 3, amount = [-2,4,2,-4,6]
Output: 6
Explanation:
The above diagram represents the given tree. The game goes as follows:
- Alice is initially on node 0, Bob on node 3. They open the gates of their respective nodes.
Alice's net income is now -2.
- Both Alice and Bob move to node 1.
Since they reach here simultaneously, they open the gate together and share the reward.
Alice's net income becomes -2 + (4 / 2) = 0.
- Alice moves on to node 3. Since Bob already opened its gate, Alice's income remains unchanged.
Bob moves on to node 0, and stops moving.
- Alice moves on to node 4 and opens the gate there. Her net income becomes 0 + 6 = 6.
Now, neither Alice nor Bob can make any further moves, and the game ends.
It is not possible for Alice to get a higher net income.
Example 2:
graph TD 0("0
Amount: -7280") 1("1
Amount: 2350") 0 --- 1
Input: edges = [[0,1]], bob = 1, amount = [-7280,2350]
Output: -7280
Explanation:
Alice follows the path 0->1 whereas Bob follows the path 1->0.
Thus, Alice opens the gate at node 0 only. Hence, her net income is -7280.
Constraints:
2 <= n <= 105
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges
represents a valid tree.1 <= bob < n
amount.length == n
amount[i]
is an even integer in the range[-104, 104]
.
Solution
Method 1 - 2 DFS approach
To solve the problem of determining Alice’s maximum profit while considering Bob’s movement, we used a two-step Depth-First Search (DFS) approach. Here’s the detailed explanation:
Step 1: Bob’s Path Simulation (dfsBob
)
- Goal: Calculate at what time Bob reaches each node.
- Start at node
bob
and recursively determine the time it takes for Bob to reach node0
. - Each node along Bob’s path stores the time Bob reaches it (
bobTime[node]
). - If a node cannot lead to
node 0
, it is removed frombobTime
.
- Start at node
Step 2: Alice’s Path Simulation (dfsAlice
)
- Goal: Simulate Alice’s journey starting from
node 0
and calculate her net profit as she explores each path to a leaf node.- Traverse the tree using DFS. At each node:
- Determine whether Alice or Bob reaches the node first (compare the current time
time
withbobTime[node]
). - Calculate the income for Alice at the node based on the following conditions:
- Alice reaches first: Alice collects the full
amount[node]
. - Alice and Bob reach simultaneously: The
amount[node]
is divided equally. - Bob reaches first: Alice receives no income or pays no fee at the node (it has already been processed by Bob).
- Alice reaches first: Alice collects the full
- Determine whether Alice or Bob reaches the node first (compare the current time
- If the current node is a leaf node (excluding the root
node 0
), update the maximum income seen so far. - Continue to explore all unvisited neighbours.
- Traverse the tree using DFS. At each node:
Why Are bobTime
and Alice’s DFS necessary?
- The interaction between Alice and Bob at each node depends on their relative arrival times. Hence, precomputing when Bob arrives at each node (
bobTime
) simplifies the calculations during Alice’s traversal. - Alice’s traversal ensures all paths to every leaf node are explored, guaranteeing that the optimal path is found.
Handling Special Cases
- Simultaneous Arrival: Income is divided equally between Alice and Bob.
- Bob Reaches First: The node is marked as “processed,” and Alice receives no income there.
- Leaf Nodes: Only pathways ending at leaf nodes count toward maximizing Alice’s net profit.
Video explanation
Here is the video explaining this method in detail. Please check it out:
Code
Java
class Solution {
// Represents the tree as an adjacency list
private List<List<Integer>> tree;
// Tracks when Bob reaches each node
private Map<Integer, Integer> bobTime;
// Tracks visited nodes during DFS
private boolean[] visited;
// Stores the profit or loss at each node
private int[] amount;
// Stores the maximum income Alice can achieve
private int maxIncome = Integer.MIN_VALUE;
public int mostProfitablePath(int[][] edges, int bob, int[] amount) {
int n = amount.length;
// Initialize adjacency list for the tree
tree = new ArrayList<>();
for (int i = 0; i < n; i++) {
tree.add(new ArrayList<>());
}
// Build the tree from the provided edges
for (int[] edge : edges) {
tree.get(edge[0]).add(edge[1]); // Add edge in both directions
tree.get(edge[1]).add(edge[0]);
}
// Initialize bobTime map and visited array
bobTime = new HashMap<>();
visited = new boolean[n];
// Perform DFS to calculate Bob's arrival times
dfsBob(bob, 0);
// Store the `amount` array for Alice's traversal
this.amount = amount;
// Reset visited array for Alice's DFS
Arrays.fill(visited, false);
// Perform DFS for Alice's traversal, starting from the root (node 0)
dfsAlice(0, 0, 0);
// Return the maximum income Alice can achieve
return maxIncome;
}
// DFS to calculate when Bob reaches each node
private boolean dfsBob(int node, int time) {
bobTime.put(node, time); // Record the time Bob reaches this node
visited[node] = true; // Mark node as visited
// If Bob reaches the root, we are done
if (node == 0) {
return true;
}
// Explore neighbours recursively
for (int nei : tree.get(node)) {
if (!visited[nei]) { // Only visit unvisited nodes
if (dfsBob(nei, time + 1)) { // Recur for connected nodes
return true; // If root is reached, stop recursion
}
}
}
// If path to root doesn't exist through this node, remove it from bobTime
bobTime.remove(node);
return false;
}
// DFS to calculate Alice's maximum income
private void dfsAlice(int node, int time, int income) {
visited[node] = true; // Mark node as visited
// Update income based on Alice and Bob's arrival times
if (!bobTime.containsKey(node) || time < bobTime.get(node)) {
// If Alice reaches first or Bob never visits the node
income += amount[node];
} else if (time == bobTime.get(node)) {
// If Alice and Bob arrive simultaneously, they share the amount
income += amount[node] / 2;
}
// If Alice reaches a leaf node, update the maximum income
if (tree.get(node).size() == 1 && node != 0) { // Skip root as leaf node
maxIncome = Math.max(maxIncome, income);
}
// Recur for all unvisited neighbours
for (int nei : tree.get(node)) {
if (!visited[nei]) {
dfsAlice(nei, time + 1, income); // Recur for next node
}
}
}
}
Python
class Solution:
def mostProfitablePath(self, edges: List[List[int]], bob: int, amount: List[int]) -> int:
n = len(amount) # Number of nodes
tree = [[] for _ in range(n)] # Adjacency list representation of the tree
# Build the adjacency list
for a, b in edges:
tree[a].append(b)
tree[b].append(a)
bobTime = {} # Stores the time Bob reaches each node
visited = [False] * n # Tracks visited nodes for DFS
maxIncome = float('-inf') # Stores the maximum income Alice can achieve
# Nested function for Bob's DFS
def dfsBob(node: int, time: int) -> bool:
nonlocal bobTime
bobTime[node] = time # Record Bob's arrival time
visited[node] = True # Mark node as visited
if node == 0: # If Bob reaches the root, stop
return True
for nei in tree[node]:
if not visited[nei]: # Traverse unvisited neighbours
if dfsBob(nei, time + 1):
return True
bobTime.pop(node) # Remove node if it doesn't lead to the root
return False
# Nested function for Alice's DFS
def dfsAlice(node: int, time: int, income: int):
nonlocal maxIncome
visited[node] = True # Mark node as visited
# Calculate Alice's income based on interaction with Bob
if node not in bobTime or time < bobTime[node]:
income += amount[node] # Alice reaches first
elif time == bobTime[node]:
income += amount[node] // 2 # Alice and Bob arrive simultaneously
# Update maximum income if node is a leaf
if len(tree[node]) == 1 and node != 0: # Leaf node check
maxIncome = max(maxIncome, income)
for nei in tree[node]:
if not visited[nei]: # Traverse unvisited neighbours
dfsAlice(nei, time + 1, income)
# Perform DFS for Bob starting at `bob`
dfsBob(bob, 0)
# Reset visited array for Alice's traversal
visited = [False] * n
# Perform DFS for Alice starting at the root (node 0)
dfsAlice(0, 0, 0)
return maxIncome
Complexity
- ⏰ Time complexity:
O(n)
- Building the adjacency list takes
O(n)
since there aren - 1
edges (tree property). - A single DFS traversal is performed, which takes
O(n)
time. - Another DFS traversal is performed, which also takes
O(n)
time.
- Building the adjacency list takes
- 🧺 Space complexity:
O(n)
- Requires
O(n)
space to store all nodes and their edges. - Stores the time Bob reaches each node (
O(n)
in size). - Used in both DFS traversals, requires
O(n)
space. - The recursion depth in a DFS can be at most
O(n)
for a skewed tree.
- Requires