Facebook Pixel

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:

  1. Initial DFS: Commence from any node, say node 0, and determine the farthest node a, which will be one endpoint of the diameter.

  2. Second DFS: Start another DFS from node a to find the farthest node b. This node b serves as the opposite endpoint of the tree's diameter. While doing this DFS, record the distance from node a to every other node, storing these distances in dist2.

  3. Third DFS: Conduct a DFS starting from node b, calculating and storing distances from node b to every other node in dist3.

When analyzing distances:

  • If dist2[i] > dist3[i] for a node i, node a is the farther endpoint from node i, 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

  1. 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)
  2. 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)
  3. Identify Endpoint a: Start a DFS from an arbitrary node, usually node 0, to find the farthest node. This node a serves as one endpoint of the diameter.

    dist1 = [-1] * n
    dist1[0] = 0
    dfs(0, -1, dist1)
    a = dist1.index(max(dist1))
  4. Determine Endpoint b: Conduct a DFS beginning from node a to find the other endpoint of the diameter, node b. During this traversal, record distances to all nodes in dist2.

    dist2 = [-1] * n
    dist2[a] = 0
    dfs(a, -1, dist2)
    b = dist2.index(max(dist2))
  5. Calculate Distances from b: Execute a final DFS starting from node b to compute distances from node b to all other nodes, stored in dist3.

    dist3 = [-1] * n
    dist3[b] = 0
    dfs(b, -1, dist3)
  6. Determine Last Marked Node for Each Node: Finally, for each node i, compare distances from nodes a and b using dist2 and dist3. 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 Evaluator

Example 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:

  1. Graph Representation: Construct an adjacency list representation of the tree:

    0: [1]
    1: [0, 2, 3]
    2: [1]
    3: [1, 4]
    4: [3]
  2. DFS Function: Define the DFS function for traversing the tree and calculating distances.

  3. Identify Endpoint a: Start a DFS from node 0 to find the farthest node. In this case, we find that node 2 is the farthest, so a = 2.

    Distances from node 0: [0, 1, 2, 2, 3]
    Farthest Node: 2
  4. Determine Endpoint b: Conduct a DFS from node a = 2 to find the farthest node, which is node 4. This makes b = 4.

    Distances from node 2: [2, 1, 0, 2, 3]
    Farthest Node: 4
  5. Calculate Distances from b: Start a DFS from node b = 4 and record distances to all nodes.

    Distances from node 4: [3, 2, 3, 1, 0]
  6. Determine Last Marked Node for Each Node: Compare the distances dist2 and dist3 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 time t = 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.


Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:
Question 1 out of 10

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

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


Load More