3313. Find the Last Marked Nodes in Tree 🔒
Problem Description
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] = [u_i, v_i]
indicates that there is an edge between nodes u_i
and v_i
in the tree.
Initially, all nodes are unmarked. After every second, you mark all unmarked nodes which have at least one marked node adjacent to them.
Return an array nodes
where nodes[i]
is the last node to get marked in the tree, if you mark node i
at time t = 0
. If nodes[i]
has multiple answers for any node i
, you can choose any one answer.
Intuition
The solution revolves around finding the tree's diameter, which is the longest path between any two nodes in the tree. When you consider marking nodes starting from one point, the nodes that lie on the endpoints of this diameter will be the last to be marked as they have the greatest distance from any starting node.
We delve into this by using the Depth-First Search (DFS) twice:
-
Initial DFS: Commence from any node, say node
0
, and determine the farthest nodea
, which will be one endpoint of the diameter. -
Second DFS: Start another DFS from node
a
to find the farthest nodeb
. This nodeb
serves as the opposite endpoint of the tree's diameter. While doing this DFS, record the distance from nodea
to every other node, storing these distances indist2
. -
Third DFS: Conduct a DFS starting from node
b
, calculating and storing distances from nodeb
to every other node indist3
.
When analyzing distances:
- If
dist2[i] > dist3[i]
for a nodei
, nodea
is the farther endpoint from nodei
, meaning it will be the last marked. - Otherwise, node
b
will be.
By leveraging the endpoints of the diameter, we ensure that each node marks its last node based on the farthest distance using the calculated distances from dist2
and dist3
.
Learn more about Tree and Depth-First Search patterns.
Solution Approach
The solution utilizes a process to find the tree's diameter and employs Depth-First Search (DFS) to assist in determining which nodes are the last to be marked based on their distances from the diameter's endpoints.
Step-by-step Implementation
-
Graph Representation: Initially, represent the tree as an adjacency list using the given
edges
. This graph representation is crucial for DFS traversal.n = len(edges) + 1 g = [[] for _ in range(n)] for u, v in edges: g[u].append(v) g[v].append(u)
-
DFS Function: Define a recursive DFS function that can traverse the tree and update the distance of nodes from the starting point.
def dfs(i: int, fa: int, dist: List[int]): for j in g[i]: if j != fa: dist[j] = dist[i] + 1 dfs(j, i, dist)
-
Identify Endpoint
a
: Start a DFS from an arbitrary node, usually node0
, to find the farthest node. This nodea
serves as one endpoint of the diameter.dist1 = [-1] * n dist1[0] = 0 dfs(0, -1, dist1) a = dist1.index(max(dist1))
-
Determine Endpoint
b
: Conduct a DFS beginning from nodea
to find the other endpoint of the diameter, nodeb
. During this traversal, record distances to all nodes indist2
.dist2 = [-1] * n dist2[a] = 0 dfs(a, -1, dist2) b = dist2.index(max(dist2))
-
Calculate Distances from
b
: Execute a final DFS starting from nodeb
to compute distances from nodeb
to all other nodes, stored indist3
.dist3 = [-1] * n dist3[b] = 0 dfs(b, -1, dist3)
-
Determine Last Marked Node for Each Node: Finally, for each node
i
, compare distances from nodesa
andb
usingdist2
anddist3
. Assign the last marked node based on which distance is greater.return [a if x > y else b for x, y in zip(dist2, dist3)]
This approach efficiently finds which node will be the last marked for each node i
by focusing on the distance from the tree's diameter endpoints, leveraging the properties of an unrooted tree's longest path.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Consider a simple tree with 5 nodes and the following edges: [[0, 1], [1, 2], [1, 3], [3, 4]]
.
Here’s how the solution is implemented step-by-step:
-
Graph Representation: Construct an adjacency list representation of the tree:
0: [1] 1: [0, 2, 3] 2: [1] 3: [1, 4] 4: [3]
-
DFS Function: Define the DFS function for traversing the tree and calculating distances.
-
Identify Endpoint
a
: Start a DFS from node0
to find the farthest node. In this case, we find that node2
is the farthest, soa = 2
.Distances from node 0: [0, 1, 2, 2, 3] Farthest Node: 2
-
Determine Endpoint
b
: Conduct a DFS from nodea = 2
to find the farthest node, which is node4
. This makesb = 4
.Distances from node 2: [2, 1, 0, 2, 3] Farthest Node: 4
-
Calculate Distances from
b
: Start a DFS from nodeb = 4
and record distances to all nodes.Distances from node 4: [3, 2, 3, 1, 0]
-
Determine Last Marked Node for Each Node: Compare the distances
dist2
anddist3
for each node:Node | dist2 | dist3 | Last Marked Node -------|---------|---------|----------------- 0 | 2 | 3 | 4 1 | 1 | 2 | 4 2 | 0 | 3 | 4 3 | 2 | 1 | 2 4 | 3 | 0 | 2
Therefore, the result array
[4, 4, 4, 2, 2]
indicates the last nodes to be marked for each node when starting the marking from any node at timet = 0
.
Solution Implementation
1from typing import List
2
3class Solution:
4 def lastMarkedNodes(self, edges: List[List[int]]) -> List[int]:
5 # Depth-First Search helper function
6 def dfs(current: int, parent: int, distances: List[int]):
7 # Explore all connected nodes (neighbors)
8 for neighbor in graph[current]:
9 if neighbor != parent: # Avoid traversing back to the parent
10 distances[neighbor] = distances[current] + 1 # Calculate distance
11 dfs(neighbor, current, distances) # Continue DFS
12
13 num_nodes = len(edges) + 1 # Total number of nodes
14 graph = [[] for _ in range(num_nodes)] # Initialize adjacency list
15
16 # Construct the undirected graph from edge list
17 for u, v in edges:
18 graph[u].append(v)
19 graph[v].append(u)
20
21 # First DFS to find the farthest node from node 0
22 distances_from_start = [-1] * num_nodes
23 distances_from_start[0] = 0
24 dfs(0, -1, distances_from_start)
25
26 # 'a' is the farthest node from the starting node 0
27 node_a = distances_from_start.index(max(distances_from_start))
28
29 # Second DFS to find the farthest node from 'node_a'
30 distances_from_a = [-1] * num_nodes
31 distances_from_a[node_a] = 0
32 dfs(node_a, -1, distances_from_a)
33
34 # 'b' is the farthest node from 'node_a'
35 node_b = distances_from_a.index(max(distances_from_a))
36
37 # Third DFS to find distances from 'node_b'
38 distances_from_b = [-1] * num_nodes
39 distances_from_b[node_b] = 0
40 dfs(node_b, -1, distances_from_b)
41
42 # Compare distances from 'node_a' and 'node_b'
43 # Return the node closer to each respective point
44 return [node_a if dist_a > dist_b else node_b for dist_a, dist_b in zip(distances_from_a, distances_from_b)]
45
1import java.util.ArrayList;
2import java.util.Arrays;
3import java.util.List;
4
5class Solution {
6 // Adjacency list representation of the graph
7 private List<Integer>[] graph;
8
9 /**
10 * Finds the last marked nodes based on given edges.
11 *
12 * @param edges The edges of the tree.
13 * @return An array where each element at index `i` is the last marked node
14 * on the longest path from node `i`.
15 */
16 public int[] lastMarkedNodes(int[][] edges) {
17 int n = edges.length + 1; // Calculate number of nodes
18 graph = new List[n];
19
20 // Initialize adjacency list for the graph
21 Arrays.setAll(graph, k -> new ArrayList<>());
22
23 // Populate the adjacency list from edges
24 for (var edge : edges) {
25 int u = edge[0], v = edge[1];
26 graph[u].add(v);
27 graph[v].add(u);
28 }
29
30 // First DFS to find farthest node from node 0
31 int[] dist1 = new int[n];
32 dist1[0] = 0;
33 dfs(0, -1, dist1);
34 int a = maxNode(dist1); // Node a is the farthest from 0
35
36 // Second DFS to find farthest node from node a
37 int[] dist2 = new int[n];
38 dist2[a] = 0;
39 dfs(a, -1, dist2);
40 int b = maxNode(dist2); // Node b is the farthest from a
41
42 // Third DFS to get distances from node b
43 int[] dist3 = new int[n];
44 dist3[b] = 0;
45 dfs(b, -1, dist3);
46
47 // Determine the last marked node on the longest path from each node
48 int[] result = new int[n];
49 for (int i = 0; i < n; i++) {
50 result[i] = dist2[i] > dist3[i] ? a : b;
51 }
52 return result;
53 }
54
55 /**
56 * Depth First Search to calculate distances from a given start node.
57 *
58 * @param currentNode The current node being visited.
59 * @param parentNode The parent node of the current node.
60 * @param distances Distance array to the start node.
61 */
62 private void dfs(int currentNode, int parentNode, int[] distances) {
63 for (int neighbor : graph[currentNode]) {
64 if (neighbor != parentNode) {
65 distances[neighbor] = distances[currentNode] + 1; // Increase distance
66 dfs(neighbor, currentNode, distances);
67 }
68 }
69 }
70
71 /**
72 * Finds the index of the node with the maximum distance.
73 *
74 * @param distances Distance array.
75 * @return Index of the node with the largest distance.
76 */
77 private int maxNode(int[] distances) {
78 int maxIndex = 0;
79 for (int i = 0; i < distances.length; i++) {
80 if (distances[maxIndex] < distances[i]) {
81 maxIndex = i; // Update maxIndex if a larger distance is found
82 }
83 }
84 return maxIndex;
85 }
86}
87
1#include <vector>
2#include <algorithm>
3
4class Solution {
5public:
6 // This function returns a vector of integers representing the last marked nodes.
7 std::vector<int> lastMarkedNodes(std::vector<std::vector<int>>& edges) {
8 int n = edges.size() + 1; // Calculate the number of nodes
9 graph.resize(n);
10 // Build the adjacency list for the graph
11 for (const auto& edge : edges) {
12 int u = edge[0], v = edge[1];
13 graph[u].push_back(v);
14 graph[v].push_back(u);
15 }
16 std::vector<int> distFromRoot(n);
17 dfs(0, -1, distFromRoot); // Perform DFS to find distances from the root node
18 int nodeA = std::max_element(distFromRoot.begin(), distFromRoot.end()) - distFromRoot.begin();
19
20 std::vector<int> distFromNodeA(n);
21 dfs(nodeA, -1, distFromNodeA); // Perform DFS from node A to find distances
22 int nodeB = std::max_element(distFromNodeA.begin(), distFromNodeA.end()) - distFromNodeA.begin();
23
24 std::vector<int> distFromNodeB(n);
25 dfs(nodeB, -1, distFromNodeB); // Perform DFS from node B to find distances
26
27 std::vector<int> resultNodes;
28 // Determine the result for each node based on distances
29 for (int i = 0; i < n; ++i) {
30 // If the distance from node A is greater choose A, otherwise choose B
31 resultNodes.push_back(distFromNodeA[i] > distFromNodeB[i] ? nodeA : nodeB);
32 }
33 return resultNodes;
34 }
35
36private:
37 std::vector<std::vector<int>> graph; // Adjacency list of the graph
38
39 // Depth First Search to compute distances from a given starting node
40 void dfs(int currentNode, int parentNode, std::vector<int>& distances) {
41 for (int neighbor : graph[currentNode]) {
42 if (neighbor != parentNode) {
43 distances[neighbor] = distances[currentNode] + 1;
44 dfs(neighbor, currentNode, distances);
45 }
46 }
47 }
48};
49
1// Function to find the last marked nodes
2function lastMarkedNodes(edges: number[][]): number[] {
3 // Total number of nodes is one more than the number of edges
4 const n = edges.length + 1;
5
6 // Graph adjacency list initialization
7 const g: number[][] = Array.from({ length: n }, () => []);
8
9 // Building the graph from given edges
10 for (const [u, v] of edges) {
11 g[u].push(v);
12 g[v].push(u);
13 }
14
15 // Depth First Search (DFS) to calculate distances
16 const dfs = (i: number, fa: number, dist: number[]) => {
17 for (const j of g[i]) {
18 if (j !== fa) {
19 dist[j] = dist[i] + 1;
20 dfs(j, i, dist);
21 }
22 }
23 };
24
25 // First DFS to find the furthest node from node 0
26 const dist1: number[] = Array(n).fill(0);
27 dfs(0, -1, dist1);
28
29 // Node 'a' is the furthest from node 0
30 const a = dist1.indexOf(Math.max(...dist1));
31
32 // Second DFS starting from node 'a' to find its furthest node 'b'
33 const dist2: number[] = Array(n).fill(0);
34 dfs(a, -1, dist2);
35
36 // Node 'b' is the furthest from node 'a'
37 const b = dist2.indexOf(Math.max(...dist2));
38
39 // Third DFS starting from node 'b' to compute distances for use in the final decision
40 const dist3: number[] = Array(n).fill(0);
41 dfs(b, -1, dist3);
42
43 // Determine the last marked nodes
44 const ans: number[] = [];
45 for (let i = 0; i < n; ++i) {
46 // Compare distances to decide whether to mark node 'a' or 'b'
47 ans.push(dist2[i] > dist3[i] ? a : b);
48 }
49
50 // Return the array of last marked nodes
51 return ans;
52}
53
Time and Space Complexity
The time complexity of the code is O(n)
. This is because the depth-first search (DFS) is executed three times, and each DFS call visits every node exactly once. Each time, the function updates the distance arrays (dist1
, dist2
, and dist3
), which have a size proportional to n
, the number of nodes.
The space complexity of the code is also O(n)
. The primary space usage comes from the adjacency list g
and the distance arrays (dist1
, dist2
, and dist3
), which all scale linearly with the number of nodes n
.
Learn more about how to find time and space complexity quickly.
What's the output of running the following function using input 56
?
1KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13 def dfs(path, res):
14 if len(path) == len(digits):
15 res.append(''.join(path))
16 return
17
18 next_number = digits[len(path)]
19 for letter in KEYBOARD[next_number]:
20 path.append(letter)
21 dfs(path, res)
22 path.pop()
23
24 res = []
25 dfs([], res)
26 return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2 '2', "abc".toCharArray(),
3 '3', "def".toCharArray(),
4 '4', "ghi".toCharArray(),
5 '5', "jkl".toCharArray(),
6 '6', "mno".toCharArray(),
7 '7', "pqrs".toCharArray(),
8 '8', "tuv".toCharArray(),
9 '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13 List<String> res = new ArrayList<>();
14 dfs(new StringBuilder(), res, digits.toCharArray());
15 return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19 if (path.length() == digits.length) {
20 res.add(path.toString());
21 return;
22 }
23 char next_digit = digits[path.length()];
24 for (char letter : KEYBOARD.get(next_digit)) {
25 path.append(letter);
26 dfs(path, res, digits);
27 path.deleteCharAt(path.length() - 1);
28 }
29}
30
1const KEYBOARD = {
2 '2': 'abc',
3 '3': 'def',
4 '4': 'ghi',
5 '5': 'jkl',
6 '6': 'mno',
7 '7': 'pqrs',
8 '8': 'tuv',
9 '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13 let res = [];
14 dfs(digits, [], res);
15 return res;
16}
17
18function dfs(digits, path, res) {
19 if (path.length === digits.length) {
20 res.push(path.join(''));
21 return;
22 }
23 let next_number = digits.charAt(path.length);
24 for (let letter of KEYBOARD[next_number]) {
25 path.push(letter);
26 dfs(digits, path, res);
27 path.pop();
28 }
29}
30
Recommended Readings
Everything About Trees A tree is a type of graph data structure composed of nodes and edges Its main properties are It is acyclic doesn't contain any cycles There exists a path from the root to any node Has N 1 edges where N is the number of nodes in the tree and
https algomonster s3 us east 2 amazonaws com cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!