3787. Find Diameter Endpoints of a Tree ๐
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 nodeiis a special node (i.e., it is an endpoint of some diameter path).s[i] = '0'otherwise.
How We Pick the Algorithm
Why BFS?
This problem maps to BFS through a short path in the full flowchart.
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 FlowchartShow 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
nnodes 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 - 1edges.
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, theniis exactly the diameter distance away fromb, so the path fromitobis a valid diameter, makingia special node. - Similarly, if
dist(a, i) == d, then the path fromitoais a valid diameter, makingia 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:
- Run BFS from an arbitrary node (say
0) to find one diameter enda. - Run BFS from
ato find the other endband record the distance arraydist1(distances froma). - Run BFS from
bto record the distance arraydist2(distances fromb). - The diameter length is
d = dist1[b]. Any nodeiwithdist1[i] == dordist2[i] == dis 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-1everywhere to mark unvisited nodes, withdist[start] = 0. - A queue (
deque) to process nodes in order of their distance fromstart. - A variable
farto 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):
- Run BFS from node
0to find one diameter endpointa. We only need the farthest node from this pass. - Run BFS from
ato find the opposite endpointb, and capturedist1, the distances fromato every node. - Run BFS from
bto capturedist2, the distances frombto 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 hasn - 1edges. Running BFS three times plus a final linear scan all stay withinO(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.
| Node | Neighbors |
|---|---|
| 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:
| Node | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| dist | 0 | 1 | 2 | 3 | 3 | 4 |
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):
| Node | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| dist1 | 4 | 3 | 2 | 3 | 1 | 0 |
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):
| Node | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| dist2 | 0 | 1 | 2 | 3 | 3 | 4 |
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 i | dist1[i] | dist2[i] | dist1==4? | dist2==4? | Special? | s[i] |
|---|---|---|---|---|---|---|
| 0 | 4 | 0 | โ | โ | Yes | 1 |
| 1 | 3 | 1 | โ | โ | No | 0 |
| 2 | 2 | 2 | โ | โ | No | 0 |
| 3 | 3 | 3 | โ | โ | No | 0 |
| 4 | 1 | 3 | โ | โ | No | 0 |
| 5 | 0 | 4 | โ | โ | Yes | 1 |
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)
511class 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}
731class 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};
641/**
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}
80Time and Space Complexity
Time Complexity: O(n)
The algorithm performs three BFS traversals over the tree:
bfs(0)to find one endpointaof the diameter.bfs(a)to find the other endpointband the distance arraydist1.bfs(b)to obtain the distance arraydist2.
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
gstores2 * (n - 1)directed entries, requiringO(n)space. - Each BFS uses a
distarray of sizenand a queueqthat can hold up toO(n)nodes, bothO(n). - The
anslist and the returned string each occupyO(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
iis a diameter endpoint if and only if its distance to at least one of the two extreme endpoints (aorb) equals the diameter lengthd.
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, thenipaired withaforms a diameter path โiis special. - If
dist_from_b[i] == d, thenipaired withbforms a diameter path โiis 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 node0): node2,3,4are all at distance2 == dโ marked.dist_from_b(from node2): node0,3,4are all at distance2 == 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 RoadmapWhich algorithm should you use to find a node that is close to the root of the tree?
Recommended Readings
Tree Data Structure Explained 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 assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
https assets algo monster cover_photos graph svg Graph Fundamentals Tree with 0 cycle At this point you should be pretty familiar with trees A tree is a special kind of graph a connected acyclic cycle less graph A graph may contain cycle s and nodes could be disconnected A tree
Want a Structured Path to Master System Design Too? Donโt Miss This!