Time Taken to Mark All Nodes
HardPrompt
There exists an undirected tree with n nodes numbered 0 to n - 1. You are given a 2D integer array edges of length n - 1, where edges[i] = [ui, vi] indicates that there is an edge between nodes ui and vi in the tree.
Initially, all nodes are unmarked. For each node i:
- If
iis odd, the node will get marked at timexif there is at least one node adjacent to it which was marked at timex - 1. - If
iis even, the node will get marked at timexif there is at least one node adjacent to it which was marked at timex - 2.
Return an array times where times[i] is the time when all nodes get marked in the tree, if you mark node i at time t = 0.
Note that the answer for each times[i] is independent, i.e. when you mark node i all other nodes are unmarked.
Example 1:
Input: edges = [[0,1],[0,2]]
Output: [2,4,3]
Explanation:

- For
i = 0:- Node 1 is marked at
t = 1, and Node 2 att = 2.
- Node 1 is marked at
- For
i = 1:- Node 0 is marked at
t = 2, and Node 2 att = 4.
- Node 0 is marked at
- For
i = 2:- Node 0 is marked at
t = 2, and Node 1 att = 3.
- Node 0 is marked at
Example 2:
Input: edges = [[0,1]]
Output: [1,2]
Explanation:

- For
i = 0:- Node 1 is marked at
t = 1.
- Node 1 is marked at
- For
i = 1:- Node 0 is marked at
t = 2.
- Node 0 is marked at
Example 3:
Input: edges = [[2,4],[0,1],[2,3],[0,2]]
Output: [4,6,3,5,5]
Explanation:

Constraints:
2 <= n <= 105edges.length == n - 1edges[i].length == 20 <= edges[i][0], edges[i][1] <= n - 1- The input is generated such that
edgesrepresents a valid tree.
Approaches
2 approaches with complexity analysis and trade-offs.
The most straightforward way to solve this problem is to simulate the marking process for each possible starting node. For each node i from 0 to n-1, we treat it as the initial node marked at time t=0 and calculate the time it takes for every other node in the tree to get marked. The final answer for i is the maximum of these times.
Algorithm
- For each node
ifrom0ton-1:- Initialize
distarray of sizenwith infinity,dist[i] = 0. - Create a deque and add
ito it. - While deque is not empty:
- Pop node
ufrom the front. - For each neighbor
vofu:- Calculate
cost = (v % 2 == 0) ? 2 : 1. - If
dist[u] + cost < dist[v]:- Update
dist[v] = dist[u] + cost. - If
cost == 1, addvto the front of the deque. - Else, add
vto the back.
- Update
- Calculate
- Pop node
- Find the maximum value in the
distarray and store it as the result for starting nodei.
- Initialize
Walkthrough
The problem of finding the time each node gets marked, given a starting node, can be modeled as a shortest path problem on a graph. The nodes of the graph are the nodes of the tree. The time it takes for a mark to propagate from a node u to an adjacent node v can be seen as the weight of a directed edge from u to v. This weight depends on the destination node v:
- If
vis odd, the time delay is 1. - If
vis even, the time delay is 2.
Since the edge weights are small positive integers (1 and 2), we can find the shortest path from a starting node s to all other nodes efficiently using a variation of Breadth-First Search (BFS), often called 0-1 BFS, which uses a deque instead of a standard queue.
The overall algorithm is as follows:
- Build an adjacency list representation of the tree from the
edgesinput. - Initialize an answer array
timesof sizen. - Loop through each node
ifrom0ton-1: a. Perform a 0-1 BFS starting from nodeito calculate the shortest time (dist[j]) for every other nodejto be marked. b. Thedistarray is initialized with infinity, anddist[i]is set to 0. c. A deque is used to store nodes to visit. Initially, it containsi. d. While the deque is not empty, extract a nodeu. For each neighborv, calculate the new timedist[u] + cost(v). If this is a shorter path tov, updatedist[v]and addvto the deque: to the front if the cost was 1, and to the back if the cost was 2. e. After the BFS completes, find the maximum value in thedistarray. This is the time when all nodes are marked. f. Store this maximum time intimes[i]. - Return the
timesarray.
This approach is correct but inefficient because it repeatedly calculates shortest paths over the same tree structure.
import java.util.*; class Solution { public int[] timeTaken(int[][] edges) { int n = edges.length + 1; List<List<Integer>> 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]); } int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = calculateMaxTime(i, n, adj); } return result; } private int calculateMaxTime(int startNode, int n, List<List<Integer>> adj) { int[] dist = new int[n]; Arrays.fill(dist, Integer.MAX_VALUE); dist[startNode] = 0; Deque<Integer> deque = new ArrayDeque<>(); deque.addFirst(startNode); int maxTime = 0; while (!deque.isEmpty()) { int u = deque.pollFirst(); maxTime = Math.max(maxTime, dist[u]); for (int v : adj.get(u)) { int cost = (v % 2 == 0) ? 2 : 1; if (dist[u] != Integer.MAX_VALUE && dist[u] + cost < dist[v]) { dist[v] = dist[u] + cost; if (cost == 1) { deque.addFirst(v); } else { deque.addLast(v); } } } } return maxTime; }}Complexity
Time
O(N^2). For each of the `N` starting nodes, we perform a 0-1 BFS which takes `O(N + E)` time, where `E` is the number of edges. Since it's a tree, `E = N-1`, so one BFS is `O(N)`. The total time is `N * O(N) = O(N^2)`.
Space
O(N). We need `O(N)` space for the adjacency list, `O(N)` for the `dist` array, and `O(N)` for the deque in the worst case.
Trade-offs
Pros
Relatively simple to understand as it directly models the problem.
Correctly solves the problem for smaller inputs.
Cons
The
O(N^2)complexity is too slow for the given constraints (Nup to 10^5), leading to a "Time Limit Exceeded" error.
A much more efficient approach is to use dynamic programming on the tree, often known as the "rerooting" technique. Instead of re-calculating from scratch for each starting node, we can compute the answer for all nodes in just two passes (DFS traversals) over the tree.
Algorithm
- Build an adjacency list for the tree.
- Define
cost[i]as 1 for oddiand 2 for eveni. - DFS 1 (post-order, e.g.,
dfs1(u, p)):- For each child
vofu, recursively calldfs1(v, u). - After recursive calls, calculate
down[u] = max(down[u], cost[v] + down[v])over all childrenv. Base case for leaves isdown[u] = 0.
- For each child
- DFS 2 (pre-order, e.g.,
dfs2(u, p)):- For node
u, calculate the longest path frompinto a sibling subtree ofu. This can be done efficiently by pre-calculating prefix and suffix maximums ofcost[c] + down[c]for all childrencofu. - Calculate
up[v]for each childv:up[v] = cost[u] + max(up[u], sibling_max_path). - Recursively call
dfs2(v, u)for all childrenv. Base case for the root isup[root] = 0.
- For node
- Combine Results: For each node
i, the answer ismax(down[i], up[i]).
Walkthrough
The core idea is to first root the tree arbitrarily (say, at node 0). Then, for each node u, we calculate two values:
down[u]: The longest path (in terms of marking time) starting fromuand ending at a node within its subtree.up[u]: The longest path starting fromuand ending at a node outside its subtree.
The final answer for a starting node u is simply the maximum of down[u] and up[u].
The algorithm proceeds in two main steps:
Step 1: First DFS (Post-order Traversal)
- We perform a DFS from the root (e.g., node 0). In the post-order traversal phase (after visiting all children), we compute
down[u]for each nodeu. - The longest path from
uinto its subtree will either be touitself (time 0) or will go through one of its childrenc. The time to reach a nodevinc's subtree fromuiscost(c) + dist(c, v). - The recurrence for
down[u]is:down[u] = max(0, max_{c ∈ children(u)} {cost(c) + down[c]}). cost(c)is 1 ifcis odd, 2 ifcis even.
Step 2: Second DFS (Pre-order Traversal)
- We perform another DFS from the root. In the pre-order traversal phase (before visiting children), we compute
up[u]for each nodeu. up[root]is 0, as there are no nodes outside its "subtree" (the whole tree).- For any other node
uwith parentp, a path starting atuand going outside its subtree must first go top. The time to reachpiscost(p). Fromp, the path can either continue "upwards" (away fromp's subtree) or go into one ofu's sibling subtrees. - The recurrence for
up[u]is:up[u] = cost(p) + max(up[p], max_{s ∈ siblings(u)} {cost(s) + down[s]}). - To efficiently calculate the maximum over siblings for each child, we can use prefix and suffix maximums on the children's
cost + downvalues.
After both DFS traversals, ans[i] = max(down[i], up[i]) for all i.
import java.util.*; class Solution { private List<List<Integer>> adj; private int[] cost; private int[] down; private int[] up; private int n; public int[] timeTaken(int[][] edges) { n = edges.length + 1; 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]); } cost = new int[n]; for (int i = 0; i < n; i++) { cost[i] = (i % 2 == 0) ? 2 : 1; } down = new int[n]; up = new int[n]; // First DFS to compute `down` values dfs1(0, -1); // Second DFS to compute `up` values dfs2(0, -1); int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = Math.max(down[i], up[i]); } return result; } private void dfs1(int u, int p) { down[u] = 0; for (int v : adj.get(u)) { if (v == p) continue; dfs1(v, u); down[u] = Math.max(down[u], cost[v] + down[v]); } } private void dfs2(int u, int p) { List<Integer> children = new ArrayList<>(); for (int v : adj.get(u)) { if (v != p) { children.add(v); } } int k = children.size(); if (k == 0) return; int[] prefixMax = new int[k]; int[] suffixMax = new int[k]; prefixMax[0] = cost[children.get(0)] + down[children.get(0)]; for (int i = 1; i < k; i++) { prefixMax[i] = Math.max(prefixMax[i - 1], cost[children.get(i)] + down[children.get(i)]); } suffixMax[k - 1] = cost[children.get(k - 1)] + down[children.get(k - 1)]; for (int i = k - 2; i >= 0; i--) { suffixMax[i] = Math.max(suffixMax[i + 1], cost[children.get(i)] + down[children.get(i)]); } for (int i = 0; i < k; i++) { int v = children.get(i); int siblingMax = 0; if (i > 0) { siblingMax = Math.max(siblingMax, prefixMax[i - 1]); } if (i < k - 1) { siblingMax = Math.max(siblingMax, suffixMax[i + 1]); } up[v] = cost[u] + Math.max(up[u], siblingMax); dfs2(v, u); } }}Complexity
Time
O(N). Both DFS traversals visit each node and edge once. The prefix/suffix maximum calculations for children of a node `u` take time proportional to its degree. The sum of degrees in a tree is `2(N-1)`, so the total time for these calculations across all nodes is `O(N)`.
Space
O(N). We need `O(N)` space for the adjacency list, `down`, `up` arrays, and the recursion stack in the worst case (for a skewed tree). The prefix/suffix arrays are local to the function call and their maximum size is bounded by the maximum degree, which can be up to `N-1`.
Trade-offs
Pros
Highly efficient with linear time complexity.
The optimal solution for the given constraints.
Cons
The logic is more complex and less intuitive than the brute-force approach.
Implementation requires careful handling of tree traversals and DP state transitions.
Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.