Facebook Pixel

3787. Find Diameter Endpoints of a Tree ๐Ÿ”’

MediumTreeBreadth-First SearchGraph
LeetCode โ†—

Problem Description

You are given an undirected tree with n nodes, numbered from 0 to n - 1. The tree is described by a 2D integer array edges of length n - 1, where each edges[i] = [aแตข, bแตข] means there is an edge connecting node aแตข and node bแตข.

In a tree, a diameter path is the longest simple path between any two nodes. Keep in mind that a tree can have more than one diameter path, since several different pairs of nodes might share the same maximum distance.

An endpoint of a path is simply the first or last node along that path. A node is considered special if it serves as an endpoint of any diameter path in the tree.

Your task is to return a binary string s of length n, defined as follows:

  • s[i] = '1' if node i is a special node (i.e., it is an endpoint of some diameter path).
  • s[i] = '0' otherwise.
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why BFS?

This problem maps to BFS through a short path in the full flowchart.

Graph ortree?yesBFS forlevels/layers?yesBFS

Finding tree diameter endpoints requires BFS from an arbitrary node, then BFS from the farthest node to identify all diameter paths and their endpoints.

Open in Flowchart
Show step-by-step reasoning

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: The problem provides an undirected tree with n nodes and a set of edges, which is a graph structure made up of nodes and edges.

Is it a tree?

  • Yes: The problem explicitly states it is an undirected tree, a connected acyclic graph with n - 1 edges.

DFS?

  • The flowchart points toward DFS for tree problems, but here we need to compute the diameter of the tree and identify all of its endpoints. A natural and clean way to find the farthest node and the layered distances from a source is by exploring the tree level by level.

Is the graph Weighted?

  • No: All edges in the tree are unweighted, so each edge contributes the same unit distance to any path.

BFS

  • Since the edges are unweighted, computing the distance from a source node to every other node can be done efficiently by traversing nodes in order of their distance. Running BFS from an arbitrary node locates one diameter endpoint, a second BFS from that endpoint locates the opposite endpoint and yields the distance array, and a third BFS from that opposite endpoint gives the remaining distances needed to flag every special node.

Conclusion: The flowchart guides us toward the Breadth-First Search (BFS) pattern, using repeated BFS passes to find the tree's diameter length and to mark all nodes that serve as endpoints of any diameter path.

Intuition

The heart of this problem is finding the diameter of the tree and then identifying every node that can serve as one of its two endpoints.

A well-known property of trees gives us a powerful starting point: if we pick any node and find the node a that is farthest from it, then a is guaranteed to be one endpoint of some diameter path. From there, if we find the node b that is farthest from a, the path from a to b is a diameter, and its length d = dist(a, b) is the diameter length of the whole tree. This is the classic "two BFS" trick for computing the diameter of an unweighted tree.

But this problem asks for more than just two endpoints. A tree may have multiple diameter paths, so there can be many nodes that act as endpoints. We need to flag all of them, not just the specific pair a and b we happened to find.

The key insight is this: a node i is a special node (an endpoint of some diameter path) if and only if its distance to one of the diameter ends equals the full diameter length d. Concretely, after locating the two diameter ends a and b, we observe:

  • If dist(b, i) == d, then i is exactly the diameter distance away from b, so the path from i to b is a valid diameter, making i a special node.
  • Similarly, if dist(a, i) == d, then the path from i to a is a valid diameter, making i a special node.

This works because any diameter endpoint must sit at the maximum possible distance from at least one of the two extreme ends of the tree. By measuring distances from both a and b, we capture every node that could be the start or the end of any longest path.

So the plan becomes:

  1. Run BFS from an arbitrary node (say 0) to find one diameter end a.
  2. Run BFS from a to find the other end b and record the distance array dist1 (distances from a).
  3. Run BFS from b to record the distance array dist2 (distances from b).
  4. The diameter length is d = dist1[b]. Any node i with dist1[i] == d or dist2[i] == d is special.

Since the tree is unweighted, BFS is the perfect tool: it naturally explores nodes layer by layer, so the level at which a node is reached is exactly its distance from the source. Three BFS passes are enough to gather all the information we need, giving an efficient O(n) solution.

Pattern Learn more about Tree, Breadth-First Search and Graph patterns.

Solution Approach

We use BFS as the core technique, applied three times to gather all the distance information we need. Here is how the implementation comes together step by step.

Step 1: Build the adjacency list.

We first convert the edges array into an adjacency list g, where g[u] holds all nodes adjacent to node u. Since the tree is undirected, for each edge [a, b] we add b to g[a] and a to g[b]:

g = [[] for _ in range(n)]
for a, b in edges:
    g[a].append(b)
    g[b].append(a)

Step 2: Write a reusable BFS helper.

We define a bfs(start) function that performs a standard breadth-first traversal from a given start node. It uses:

  • A distance array dist, initialized to -1 everywhere to mark unvisited nodes, with dist[start] = 0.
  • A queue (deque) to process nodes in order of their distance from start.
  • A variable far to track the node with the greatest distance seen so far.

As we pop each node u from the queue, we compare dist[u] with dist[far] to update the farthest node, then push all unvisited neighbors with their distance set to dist[u] + 1:

def bfs(start: int):
    dist = [-1] * n
    dist[start] = 0
    q = deque([start])
    far = start
    while q:
        u = q.popleft()
        if dist[u] > dist[far]:
            far = u
        for v in g[u]:
            if dist[v] == -1:
                dist[v] = dist[u] + 1
                q.append(v)
    return far, dist

The helper returns both the farthest node far and the complete distance array dist, so we can reuse it for different purposes.

Step 3: Find the diameter endpoints with three BFS passes.

Following the classic two-BFS diameter trick (extended to three passes here):

  1. Run BFS from node 0 to find one diameter endpoint a. We only need the farthest node from this pass.
  2. Run BFS from a to find the opposite endpoint b, and capture dist1, the distances from a to every node.
  3. Run BFS from b to capture dist2, the distances from b to every node.
a, _ = bfs(0)
b, dist1 = bfs(a)
_, dist2 = bfs(b)

The diameter length is simply d = dist1[b], the distance between the two extreme endpoints.

Step 4: Mark every special node.

We initialize an answer list ans of "0" characters. For each node i, if dist1[i] == d (it lies exactly the diameter distance from a) or dist2[i] == d (it lies exactly the diameter distance from b), then i is an endpoint of some diameter path, so we set ans[i] = "1":

d = dist1[b]
ans = ["0"] * n
for i in range(n):
    if dist1[i] == d or dist2[i] == d:
        ans[i] = "1"
return "".join(ans)

Finally, we join the list into a binary string and return it.

Complexity Analysis:

  • Time Complexity: O(n). Each BFS pass visits every node and edge once, and a tree has n - 1 edges. Running BFS three times plus a final linear scan all stay within O(n).
  • Space Complexity: O(n). The adjacency list, the distance arrays, the BFS queue, and the answer list each use space proportional to the number of nodes.

Example Walkthrough

Let's trace through the solution with a small concrete example.

Input:

n = 6
edges = [[0, 1], [1, 2], [2, 3], [2, 4], [4, 5]]

This builds the following tree:

0 - 1 - 2 - 3
            |
            4
            |
            5

Wait, let me draw it more accurately based on the edges:

0 - 1 - 2 - 3
          
        2 - 4 - 5

So node 2 connects to 1, 3, and 4. Node 4 connects to 2 and 5:

0 - 1 - 2 - 3
        |
        4
        |
        5

Step 1: Build the adjacency list.

NodeNeighbors
0[1]
1[0, 2]
2[1, 3, 4]
3[2]
4[2, 5]
5[4]

Step 2 & 3: Three BFS passes.

BFS Pass 1 โ€” start from node 0 to find one diameter endpoint a.

Distances from 0:

Node012345
dist012334

The farthest node is 5 (distance 4), so a = 5.

BFS Pass 2 โ€” start from node a = 5 to find the opposite endpoint b and capture dist1.

dist1 (distances from 5):

Node012345
dist1432310

The farthest node is 0 (distance 4), so b = 0. (Note: node 3 is also at distance 4? Let me recheck โ€” 5โ†’4โ†’2โ†’3 is distance 3. Node 0 is 5โ†’4โ†’2โ†’1โ†’0, distance 4. So b = 0.)

BFS Pass 3 โ€” start from node b = 0 to capture dist2.

dist2 (distances from 0):

Node012345
dist2012334

Step 4: Compute diameter and mark special nodes.

The diameter length is d = dist1[b] = dist1[0] = 4.

Now we check every node: it is special if dist1[i] == 4 OR dist2[i] == 4.

Node idist1[i]dist2[i]dist1==4?dist2==4?Special?s[i]
040โœ…โŒYes1
131โŒโŒNo0
222โŒโŒNo0
333โŒโŒNo0
413โŒโŒNo0
504โŒโœ…Yes1

Result: s = "100001"

Why this is correct: The only diameter path here is 0 - 1 - 2 - 4 - 5 (length 4). Its two endpoints are nodes 0 and 5, which are exactly the nodes flagged as 1. Notice node 3 is not special: the longest path ending at 3 is 3 - 2 - 4 - 5 (length 3) or 3 - 2 - 1 - 0 (length 3), neither of which reaches the diameter length 4. This confirms that checking the distance to both extreme ends a and b correctly captures every diameter endpoint while excluding non-endpoints.

Solution Implementation

1from collections import deque
2from typing import List
3
4
5class Solution:
6    def findSpecialNodes(self, n: int, edges: List[List[int]]) -> str:
7        # Build adjacency list for the undirected tree
8        graph: List[List[int]] = [[] for _ in range(n)]
9        for node_a, node_b in edges:
10            graph[node_a].append(node_b)
11            graph[node_b].append(node_a)
12
13        def bfs(start: int) -> tuple[int, List[int]]:
14            """
15            Run BFS from `start`.
16            Return the farthest node from `start` and the distance array.
17            """
18            distance = [-1] * n
19            distance[start] = 0
20            queue = deque([start])
21            farthest = start
22            while queue:
23                current = queue.popleft()
24                # Track the node with the maximum distance seen so far
25                if distance[current] > distance[farthest]:
26                    farthest = current
27                for neighbor in graph[current]:
28                    if distance[neighbor] == -1:
29                        distance[neighbor] = distance[current] + 1
30                        queue.append(neighbor)
31            return farthest, distance
32
33        # First BFS from an arbitrary node (0) to find one endpoint of the diameter
34        endpoint_a, _ = bfs(0)
35        # Second BFS from endpoint_a to find the other endpoint and its distances
36        endpoint_b, dist_from_a = bfs(endpoint_a)
37        # Third BFS from endpoint_b to obtain distances from the other endpoint
38        _, dist_from_b = bfs(endpoint_b)
39
40        # The diameter length is the distance between the two endpoints
41        diameter = dist_from_a[endpoint_b]
42
43        # A node is "special" if it lies at distance == diameter from either endpoint,
44        # meaning it can serve as an endpoint of some diameter path
45        result = ["0"] * n
46        for i in range(n):
47            if dist_from_a[i] == diameter or dist_from_b[i] == diameter:
48                result[i] = "1"
49
50        return "".join(result)
51
1class Solution {
2    public String findSpecialNodes(int n, int[][] edges) {
3        // Build an adjacency list representation of the tree.
4        List<Integer>[] graph = new ArrayList[n];
5        Arrays.setAll(graph, k -> new ArrayList<>());
6        for (int[] edge : edges) {
7            int u = edge[0];
8            int v = edge[1];
9            graph[u].add(v);
10            graph[v].add(u);
11        }
12
13        // Holds the result of a BFS traversal:
14        // - farthest: the node farthest from the start node
15        // - distances: distance from the start node to every other node
16        record BFSResult(int farthest, int[] distances) {
17        }
18
19        // BFS from a given start node.
20        // Returns the farthest reachable node and the distance array.
21        IntFunction<BFSResult> bfs = (int start) -> {
22            int[] distances = new int[n];
23            Arrays.fill(distances, -1);
24            distances[start] = 0;
25
26            ArrayDeque<Integer> queue = new ArrayDeque<>();
27            queue.add(start);
28
29            int farthest = start;
30            while (!queue.isEmpty()) {
31                int u = queue.poll();
32                // Track the node with the maximum distance seen so far.
33                if (distances[u] > distances[farthest]) {
34                    farthest = u;
35                }
36                for (int v : graph[u]) {
37                    // Visit each unvisited neighbor exactly once.
38                    if (distances[v] == -1) {
39                        distances[v] = distances[u] + 1;
40                        queue.add(v);
41                    }
42                }
43            }
44            return new BFSResult(farthest, distances);
45        };
46
47        // Step 1: BFS from an arbitrary node (0) to find one end of the diameter.
48        int endpointA = bfs.apply(0).farthest();
49
50        // Step 2: BFS from endpointA to find the other end of the diameter.
51        BFSResult resultFromA = bfs.apply(endpointA);
52        int endpointB = resultFromA.farthest();
53        int[] distFromA = resultFromA.distances();
54
55        // Step 3: BFS from endpointB to get distances from the other diameter end.
56        int[] distFromB = bfs.apply(endpointB).distances();
57
58        // The length of the diameter.
59        int diameter = distFromA[endpointB];
60
61        // A node is "special" if it lies at the end of some diameter-length path,
62        // i.e. its distance to endpointA or endpointB equals the diameter length.
63        char[] answer = new char[n];
64        Arrays.fill(answer, '0');
65        for (int i = 0; i < n; i++) {
66            if (distFromA[i] == diameter || distFromB[i] == diameter) {
67                answer[i] = '1';
68            }
69        }
70        return new String(answer);
71    }
72}
73
1class Solution {
2public:
3    string findSpecialNodes(int n, vector<vector<int>>& edges) {
4        // Build the adjacency list for the tree.
5        vector<vector<int>> graph(n);
6        for (const auto& edge : edges) {
7            int u = edge[0], v = edge[1];
8            graph[u].push_back(v);
9            graph[v].push_back(u);
10        }
11
12        // BFS from a given source.
13        // Returns the farthest node from the source and the distance array.
14        auto bfs = [&](int source) -> pair<int, vector<int>> {
15            vector<int> distance(n, -1);
16            distance[source] = 0;
17            deque<int> queue;
18            queue.push_back(source);
19            int farthest = source;
20            while (!queue.empty()) {
21                int current = queue.front();
22                queue.pop_front();
23                // Track the node with the maximum distance seen so far.
24                if (distance[current] > distance[farthest]) {
25                    farthest = current;
26                }
27                // Visit all unvisited neighbors.
28                for (int next : graph[current]) {
29                    if (distance[next] == -1) {
30                        distance[next] = distance[current] + 1;
31                        queue.push_back(next);
32                    }
33                }
34            }
35            return {farthest, distance};
36        };
37
38        // Step 1: BFS from an arbitrary node (0) to find one endpoint
39        // of the tree's diameter.
40        auto [endpointA, distFromZero] = bfs(0);
41
42        // Step 2: BFS from endpointA to find the opposite endpoint
43        // and the distance from endpointA to every node.
44        auto [endpointB, distFromA] = bfs(endpointA);
45
46        // Step 3: BFS from endpointB to get the distance from endpointB
47        // to every node.
48        auto [unusedEndpoint, distFromB] = bfs(endpointB);
49
50        // The diameter length is the distance between endpointA and endpointB.
51        int diameter = distFromA[endpointB];
52
53        // A node is "special" if it is a diameter endpoint, i.e. its distance
54        // from one of the two endpoints equals the full diameter length.
55        string answer(n, '0');
56        for (int i = 0; i < n; ++i) {
57            if (distFromA[i] == diameter || distFromB[i] == diameter) {
58                answer[i] = '1';
59            }
60        }
61        return answer;
62    }
63};
64
1/**
2 * Finds the "special" nodes in a tree.
3 *
4 * A node is considered special if it can be one of the two endpoints of some
5 * diameter (a longest path) of the tree. We detect this by:
6 *   1. Running BFS from an arbitrary node to find one endpoint `a` of a diameter.
7 *   2. Running BFS from `a` to find the opposite endpoint `b` and distances `dist1`.
8 *   3. Running BFS from `b` to obtain distances `dist2`.
9 *   4. The diameter length is `d = dist1[b]`. Any node whose distance to either
10 *      endpoint equals `d` is itself a diameter endpoint, hence "special".
11 *
12 * @param n     The number of nodes in the tree (labeled 0..n-1).
13 * @param edges The undirected edges of the tree.
14 * @returns     A binary string where index `i` is '1' if node `i` is special, else '0'.
15 */
16function findSpecialNodes(n: number, edges: number[][]): string {
17    // Build the adjacency list for the undirected tree.
18    const graph: number[][] = Array.from({ length: n }, () => []);
19    for (const [from, to] of edges) {
20        graph[from].push(to);
21        graph[to].push(from);
22    }
23
24    /**
25     * Breadth-first search from `start`.
26     *
27     * @param start The source node.
28     * @returns A tuple `[farthest, distances]` where `farthest` is the node
29     *          with the greatest distance from `start`, and `distances` holds
30     *          the shortest distance from `start` to every node.
31     */
32    const bfs = (start: number): [number, number[]] => {
33        const distances: number[] = new Array<number>(n).fill(-1);
34        distances[start] = 0;
35
36        // Use the queue as a growing array; iterate as we discover new nodes.
37        const queue: number[] = [start];
38        let farthest = start;
39
40        for (const current of queue) {
41            // Track the node lying farthest from `start`.
42            if (distances[current] > distances[farthest]) {
43                farthest = current;
44            }
45            // Visit all unvisited neighbors.
46            for (const neighbor of graph[current]) {
47                if (distances[neighbor] === -1) {
48                    distances[neighbor] = distances[current] + 1;
49                    queue.push(neighbor);
50                }
51            }
52        }
53
54        return [farthest, distances];
55    };
56
57    // Step 1: From node 0, find one endpoint `endpointA` of a diameter.
58    const [endpointA] = bfs(0);
59
60    // Step 2: From `endpointA`, find the opposite endpoint `endpointB`
61    //         and the distances from `endpointA`.
62    const [endpointB, distFromA] = bfs(endpointA);
63
64    // Step 3: From `endpointB`, compute distances to every node.
65    const [, distFromB] = bfs(endpointB);
66
67    // The diameter length is the distance between the two endpoints.
68    const diameter = distFromA[endpointB];
69
70    // Step 4: Mark every node that is itself a diameter endpoint.
71    const answer: string[] = new Array<string>(n).fill('0');
72    for (let i = 0; i < n; i++) {
73        if (distFromA[i] === diameter || distFromB[i] === diameter) {
74            answer[i] = '1';
75        }
76    }
77
78    return answer.join('');
79}
80

Time and Space Complexity

Time Complexity: O(n)

The algorithm performs three BFS traversals over the tree:

  1. bfs(0) to find one endpoint a of the diameter.
  2. bfs(a) to find the other endpoint b and the distance array dist1.
  3. bfs(b) to obtain the distance array dist2.

For a tree with n nodes, there are exactly n - 1 edges. Each BFS visits every node once and traverses each edge a constant number of times (twice, since the graph is stored as an undirected adjacency list). Therefore, a single BFS runs in O(n + (n - 1)) = O(n) time. Running BFS three times still yields O(3n) = O(n).

Building the adjacency list from edges takes O(n) time (since there are n - 1 edges). The final loop over all n nodes to construct the answer string also costs O(n).

Combining all parts, the overall time complexity is O(n).

Space Complexity: O(n)

  • The adjacency list g stores 2 * (n - 1) directed entries, requiring O(n) space.
  • Each BFS uses a dist array of size n and a queue q that can hold up to O(n) nodes, both O(n).
  • The ans list and the returned string each occupy O(n) space.

Hence, the total space complexity is O(n). Here n is the number of nodes.

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall: Assuming all diameter endpoints can be found from just one pair of endpoints (a, b)

The most tempting mistake is to run only the standard two-BFS diameter algorithm, find a single endpoint pair (a, b), and conclude that only a and b are special. This fails because a tree can contain multiple distinct diameter paths, and their endpoints form a larger set than just one pair.

Why this is wrong โ€” a concrete example:

Consider a "star-like" tree where a central node connects to three branches of equal length:

        0
        |
        1
       /|\
      2 3 4

Here edges = [[0,1],[1,2],[1,3],[1,4]]. The diameter length is 2 (e.g., 0 โ†’ 1 โ†’ 2). But the diameter paths include 0-1-2, 0-1-3, 0-1-4, 2-1-3, 2-1-4, 3-1-4. The endpoints are {0, 2, 3, 4} โ€” four special nodes, not two.

A naive two-BFS approach that returns only the pair it happened to land on (say 0 and 2) would incorrectly mark only those two, missing 3 and 4.


The Solution: Check distances from both diameter endpoints

The key insight that the provided code exploits:

A node i is a diameter endpoint if and only if its distance to at least one of the two extreme endpoints (a or b) equals the diameter length d.

This works because of a well-known tree property: for any node, its farthest node is always one of the two endpoints a or b discovered by the diameter BFS. Therefore:

  • If dist_from_a[i] == d, then i paired with a forms a diameter path โ†’ i is special.
  • If dist_from_b[i] == d, then i paired with b forms a diameter path โ†’ i is special.

By running BFS from both a and b and taking the union of nodes at distance d, we capture every possible diameter endpoint:

for i in range(n):
    if dist_from_a[i] == diameter or dist_from_b[i] == diameter:
        result[i] = "1"

In the example above:

  • dist_from_a (from node 0): node 2, 3, 4 are all at distance 2 == d โ†’ marked.
  • dist_from_b (from node 2): node 0, 3, 4 are all at distance 2 == d โ†’ marked.
  • Union โ†’ {0, 2, 3, 4} โœ“ Correct.

Secondary Pitfall: Forgetting the or (using only one distance array)

Even when developers run three BFS passes, some mistakenly check only dist_from_a[i] == d. This still misses endpoints. Revisiting the star example: checking only dist_from_a (from 0) would mark {2, 3, 4} but miss node 0 itself, which is also a valid endpoint (e.g., path 0-1-2). Both conditions joined by or are required to form the complete union.

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which algorithm should you use to find a node that is close to the root of the tree?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More