1483. Kth Ancestor of a Tree Node
Problem Description
You are given a tree with n
nodes numbered from 0
to n - 1
. The tree is represented using a parent array parent
, where parent[i]
is the parent of the i
-th node. The root of the tree is node 0
(it has no parent).
Your task is to implement a data structure that can efficiently find the k
-th ancestor of any given node. The k
-th ancestor of a node is the node that is k
steps up in the path from that node to the root.
You need to implement the TreeAncestor
class with the following methods:
TreeAncestor(int n, int[] parent)
: Initializes the object withn
nodes and their parent relationships given in theparent
array.int getKthAncestor(int node, int k)
: Returns thek
-th ancestor of the givennode
. If no such ancestor exists (i.e.,k
is greater than the depth of the node), return-1
.
For example, if we have a tree where node 1's parent is 0, node 2's parent is 1, and node 3's parent is 2, then:
- The 1st ancestor of node 3 is node 2
- The 2nd ancestor of node 3 is node 1
- The 3rd ancestor of node 3 is node 0
- The 4th ancestor of node 3 doesn't exist, so we return -1
The solution uses binary lifting technique with dynamic programming. It precomputes a table p[i][j]
that stores the 2^j
-th ancestor of node i
. This allows us to find any k
-th ancestor by decomposing k
into powers of 2 and making jumps accordingly, achieving O(log n)
query time instead of the naive O(k)
approach.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: The problem involves a tree structure with nodes and parent-child relationships. A tree is a special type of graph (connected acyclic graph).
Is it a tree?
- Yes: The problem explicitly states we're working with a tree structure where each node has exactly one parent (except the root), and there are no cycles.
DFS
- Yes: According to the flowchart, when dealing with tree problems, DFS is the recommended approach.
Conclusion: The flowchart suggests using DFS (Depth-First Search) for this tree problem.
While the solution actually uses binary lifting with dynamic programming for optimization, the core concept still relies on traversing the tree structure (finding ancestors by following parent pointers). The binary lifting technique is essentially a optimized way to perform what would otherwise be a DFS traversal up the tree. Instead of traversing node by node (which DFS would do), binary lifting allows us to "jump" by powers of 2, but the fundamental operation is still exploring the tree's parent-child relationships, which aligns with the DFS pattern for tree problems.
Intuition
The naive approach to find the k
-th ancestor would be to start from the given node and traverse upward k
times through parent pointers. However, this takes O(k)
time per query, which becomes inefficient when k
is large or when we have many queries.
The key insight is to ask: "Can we make larger jumps instead of going one parent at a time?"
Think about how we represent numbers in binary. Any number k
can be decomposed into powers of 2. For example, 13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0
. This means to go up 13 ancestors, we can make three jumps: go up 8 ancestors, then 4 ancestors, then 1 ancestor.
This leads us to the binary lifting idea: what if we precompute for each node where we'd end up after jumping 2^0, 2^1, 2^2, ...
ancestors? With this information, we can reach any k
-th ancestor by combining these power-of-2 jumps.
Why does this work? Because:
- Any positive integer can be represented as a sum of distinct powers of 2 (binary representation)
- We only need
log(n)
different jump sizes to cover any possible distance in a tree ofn
nodes - Each jump can be computed from the previous one: the
2^j
-th ancestor is just the2^(j-1)
-th ancestor of the2^(j-1)
-th ancestor
This transforms our O(k)
traversal into O(log k)
jumps, making queries much faster. The tradeoff is that we need O(n log n)
preprocessing time and space to build our jump table, but this is worthwhile when we have multiple queries to answer.
Learn more about Tree, Depth-First Search, Breadth-First Search, Binary Search and Dynamic Programming patterns.
Solution Approach
The solution implements binary lifting using dynamic programming. Let's walk through the implementation:
Data Structure Setup:
We create a 2D table p[i][j]
where:
i
represents the node (from 0 to n-1)j
represents the power of 2 (we use j from 0 to 17, since2^17 = 131,072
is sufficient for most tree heights)p[i][j]
stores the2^j
-th ancestor of nodei
, or -1 if no such ancestor exists
Initialization Phase:
-
First, we initialize the immediate parents (j=0):
for i, fa in enumerate(parent): self.p[i][0] = fa
Here,
p[i][0]
represents the2^0 = 1
st ancestor, which is just the direct parent. -
Then, we build the rest of the table using the recurrence relation:
for j in range(1, 18): for i in range(n): if self.p[i][j - 1] == -1: continue self.p[i][j] = self.p[self.p[i][j - 1]][j - 1]
This implements the formula:
p[i][j] = p[p[i][j-1]][j-1]
The intuition is: to find the
2^j
-th ancestor of nodei
, we:- First find the
2^(j-1)
-th ancestor of nodei
- Then find the
2^(j-1)
-th ancestor of that intermediate node - Since
2^(j-1) + 2^(j-1) = 2^j
, this gives us the2^j
-th ancestor
- First find the
Query Phase (getKthAncestor):
To find the k-th ancestor, we decompose k
into its binary representation and make jumps accordingly:
for i in range(17, -1, -1):
if k >> i & 1:
node = self.p[node][i]
if node == -1:
break
The algorithm:
- Iterates through bit positions from high to low (17 down to 0)
- Checks if the i-th bit of
k
is set usingk >> i & 1
- If the bit is set, we jump
2^i
ancestors up by updatingnode = self.p[node][i]
- If we reach -1 (no ancestor), we stop early
For example, if k = 13 = 1101
in binary:
- Bit 3 is set: jump
2^3 = 8
ancestors - Bit 2 is set: jump
2^2 = 4
ancestors - Bit 0 is set: jump
2^0 = 1
ancestor - Total: 8 + 4 + 1 = 13 ancestors
Time and Space Complexity:
- Preprocessing:
O(n × log n)
time to fill the table - Query:
O(log n)
time to decompose k and make jumps - Space:
O(n × log n)
for the 2D table
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to understand how binary lifting works.
Tree Structure:
Consider a tree with 7 nodes where the parent array is [-1, 0, 1, 2, 3, 4, 5]
.
This creates a linear tree (essentially a path):
0 (root) └── 1 └── 2 └── 3 └── 4 └── 5 └── 6
Building the Binary Lifting Table:
We create a table p[i][j]
where p[i][j]
= the 2^j
-th ancestor of node i
.
Step 1: Initialize direct parents (j = 0
, meaning 2^0 = 1
st ancestor):
p[0][0] = -1 (root has no parent) p[1][0] = 0 p[2][0] = 1 p[3][0] = 2 p[4][0] = 3 p[5][0] = 4 p[6][0] = 5
Step 2: Build for j = 1
(meaning 2^1 = 2
nd ancestor):
Using the formula p[i][1] = p[p[i][0]][0]
:
p[0][1] = p[-1][0] = -1 (no 2nd ancestor for root) p[1][1] = p[0][0] = -1 (only 1 ancestor above node 1) p[2][1] = p[1][0] = 0 (2nd ancestor of node 2 is node 0) p[3][1] = p[2][0] = 1 p[4][1] = p[3][0] = 2 p[5][1] = p[4][0] = 3 p[6][1] = p[5][0] = 4
Step 3: Build for j = 2
(meaning 2^2 = 4
th ancestor):
Using the formula p[i][2] = p[p[i][1]][1]
:
p[0][2] = -1 p[1][2] = -1 p[2][2] = -1 (node 0 has no 2nd ancestor) p[3][2] = -1 (node 1 has no 2nd ancestor) p[4][2] = p[2][1] = 0 (4th ancestor of node 4 is node 0) p[5][2] = p[3][1] = 1 p[6][2] = p[4][1] = 2
Query Example: Find the 5th ancestor of node 6
We need to find getKthAncestor(6, 5)
.
First, decompose 5 into binary: 5 = 101₂ = 4 + 1 = 2^2 + 2^0
Now we make jumps based on the set bits:
-
Check bit 2 (value 4): It's set, so jump
2^2 = 4
ancestorsnode = p[6][2] = 2
- We're now at node 2
-
Check bit 1 (value 2): It's not set, skip
-
Check bit 0 (value 1): It's set, so jump
2^0 = 1
ancestornode = p[2][0] = 1
- We're now at node 1
Result: The 5th ancestor of node 6 is node 1.
Verification: Walking up manually from node 6: 6→5→4→3→2→1. Indeed, node 1 is 5 steps up!
Another Query: Find the 3rd ancestor of node 4
Decompose 3 into binary: 3 = 11₂ = 2 + 1 = 2^1 + 2^0
Make jumps:
-
Check bit 1 (value 2): It's set, so jump
2^1 = 2
ancestorsnode = p[4][1] = 2
- We're now at node 2
-
Check bit 0 (value 1): It's set, so jump
2^0 = 1
ancestornode = p[2][0] = 1
- We're now at node 1
Result: The 3rd ancestor of node 4 is node 1.
This example demonstrates how binary lifting allows us to find any k-th ancestor in O(log k) jumps instead of making k individual parent traversals.
Solution Implementation
1from typing import List
2
3
4class TreeAncestor:
5 def __init__(self, n: int, parent: List[int]):
6 """
7 Initialize the TreeAncestor data structure.
8
9 Args:
10 n: Number of nodes in the tree (nodes are numbered from 0 to n-1)
11 parent: List where parent[i] is the parent of node i (-1 for root)
12 """
13 # Maximum power of 2 we need (2^17 > 50000, which covers most tree sizes)
14 max_power = 18
15
16 # Binary lifting table: ancestors[i][j] = 2^j-th ancestor of node i
17 # -1 means the ancestor doesn't exist (we've gone above the root)
18 self.ancestors = [[-1] * max_power for _ in range(n)]
19
20 # Initialize the first column: direct parent (2^0 = 1st ancestor)
21 for node, parent_node in enumerate(parent):
22 self.ancestors[node][0] = parent_node
23
24 # Build the binary lifting table using dynamic programming
25 # ancestors[i][j] = ancestors[ancestors[i][j-1]][j-1]
26 # This means: 2^j-th ancestor = 2^(j-1)-th ancestor of 2^(j-1)-th ancestor
27 for power in range(1, max_power):
28 for node in range(n):
29 # If the 2^(power-1)-th ancestor doesn't exist, skip
30 if self.ancestors[node][power - 1] == -1:
31 continue
32
33 # Find the 2^(power-1)-th ancestor of the 2^(power-1)-th ancestor
34 intermediate_ancestor = self.ancestors[node][power - 1]
35 self.ancestors[node][power] = self.ancestors[intermediate_ancestor][power - 1]
36
37 def getKthAncestor(self, node: int, k: int) -> int:
38 """
39 Find the k-th ancestor of the given node.
40
41 Args:
42 node: The node whose ancestor we want to find
43 k: The number of steps to go up in the tree
44
45 Returns:
46 The k-th ancestor of node, or -1 if it doesn't exist
47 """
48 # Decompose k into sum of powers of 2 and jump accordingly
49 # We check bits from most significant to least significant
50 for bit_position in range(17, -1, -1):
51 # Check if the bit at position 'bit_position' is set in k
52 if (k >> bit_position) & 1:
53 # Jump 2^bit_position steps up
54 node = self.ancestors[node][bit_position]
55
56 # If we've gone above the root, stop early
57 if node == -1:
58 break
59
60 return node
61
62
63# Your TreeAncestor object will be instantiated and called as such:
64# obj = TreeAncestor(n, parent)
65# param_1 = obj.getKthAncestor(node, k)
66
1class TreeAncestor {
2 // Binary lifting table: dp[i][j] represents the 2^j-th ancestor of node i
3 private int[][] dp;
4
5 public TreeAncestor(int n, int[] parent) {
6 // Initialize binary lifting table with 18 levels (2^17 > 50000, sufficient for constraints)
7 // dp[node][level] = the ancestor at distance 2^level from node
8 dp = new int[n][18];
9
10 // Initialize all entries to -1 (indicating no ancestor exists)
11 for (int[] row : dp) {
12 Arrays.fill(row, -1);
13 }
14
15 // Base case: the 2^0 = 1st ancestor is the direct parent
16 for (int i = 0; i < n; i++) {
17 dp[i][0] = parent[i];
18 }
19
20 // Build the binary lifting table using dynamic programming
21 // For each level j, compute the 2^j-th ancestor
22 for (int j = 1; j < 18; j++) {
23 for (int i = 0; i < n; i++) {
24 // If the 2^(j-1)-th ancestor doesn't exist, skip
25 if (dp[i][j - 1] == -1) {
26 continue;
27 }
28 // The 2^j-th ancestor is the 2^(j-1)-th ancestor of the 2^(j-1)-th ancestor
29 // In other words: jump 2^(j-1) twice to get 2^j distance
30 dp[i][j] = dp[dp[i][j - 1]][j - 1];
31 }
32 }
33 }
34
35 public int getKthAncestor(int node, int k) {
36 // Use binary representation of k to find the k-th ancestor
37 // Example: if k = 13 = 1101 in binary, we jump 2^3 + 2^2 + 2^0 = 8 + 4 + 1
38
39 // Check each bit position from most significant to least significant
40 for (int i = 17; i >= 0; i--) {
41 // Check if the i-th bit is set in k
42 if (((k >> i) & 1) == 1) {
43 // Jump 2^i ancestors up
44 node = dp[node][i];
45
46 // If we've reached beyond the root, stop early
47 if (node == -1) {
48 break;
49 }
50 }
51 }
52
53 // Return the k-th ancestor (or -1 if it doesn't exist)
54 return node;
55 }
56}
57
58/**
59 * Your TreeAncestor object will be instantiated and called as such:
60 * TreeAncestor obj = new TreeAncestor(n, parent);
61 * int param_1 = obj.getKthAncestor(node,k);
62 */
63
1class TreeAncestor {
2public:
3 TreeAncestor(int n, vector<int>& parent) {
4 // Initialize binary lifting table
5 // dp[i][j] represents the 2^j-th ancestor of node i
6 // Using 18 levels since 2^17 > 50000 (max constraint)
7 dp = vector<vector<int>>(n, vector<int>(18, -1));
8
9 // Base case: first parent (2^0 = 1st ancestor)
10 for (int i = 0; i < n; ++i) {
11 dp[i][0] = parent[i];
12 }
13
14 // Build the binary lifting table using dynamic programming
15 // dp[i][j] = dp[dp[i][j-1]][j-1]
16 // The 2^j-th ancestor is the 2^(j-1)-th ancestor of the 2^(j-1)-th ancestor
17 for (int j = 1; j < 18; ++j) {
18 for (int i = 0; i < n; ++i) {
19 // If the 2^(j-1)-th ancestor doesn't exist, skip
20 if (dp[i][j - 1] == -1) {
21 continue;
22 }
23 // Find the 2^j-th ancestor
24 dp[i][j] = dp[dp[i][j - 1]][j - 1];
25 }
26 }
27 }
28
29 int getKthAncestor(int node, int k) {
30 // Use binary representation of k to find the k-th ancestor
31 // For example, if k = 13 = 1101 in binary = 8 + 4 + 1
32 // We jump 2^3, 2^2, and 2^0 steps
33 for (int i = 17; i >= 0; --i) {
34 // Check if the i-th bit is set in k
35 if ((k >> i) & 1) {
36 // Jump 2^i steps up
37 node = dp[node][i];
38 // If we've reached beyond the root, stop
39 if (node == -1) {
40 break;
41 }
42 }
43 }
44 return node;
45 }
46
47private:
48 vector<vector<int>> dp; // Binary lifting table
49};
50
51/**
52 * Your TreeAncestor object will be instantiated and called as such:
53 * TreeAncestor* obj = new TreeAncestor(n, parent);
54 * int param_1 = obj->getKthAncestor(node,k);
55 */
56
1// Binary lifting table to store ancestors at powers of 2
2// ancestorTable[i][j] represents the 2^j-th ancestor of node i
3let ancestorTable: number[][];
4
5/**
6 * Initializes the tree ancestor data structure using binary lifting
7 * @param n - Number of nodes in the tree
8 * @param parent - Array where parent[i] is the parent of node i
9 */
10function TreeAncestor(n: number, parent: number[]): void {
11 // Initialize the ancestor table with dimensions n x 18
12 // 18 is sufficient since 2^17 > 100000 (max possible k value)
13 ancestorTable = Array.from({ length: n }, () => Array(18).fill(-1));
14
15 // Fill the first column with direct parents
16 for (let node = 0; node < n; node++) {
17 ancestorTable[node][0] = parent[node];
18 }
19
20 // Build the binary lifting table using dynamic programming
21 // ancestorTable[node][power] = ancestorTable[ancestorTable[node][power-1]][power-1]
22 for (let power = 1; power < 18; power++) {
23 for (let node = 0; node < n; node++) {
24 // Skip if the previous ancestor doesn't exist
25 if (ancestorTable[node][power - 1] === -1) {
26 continue;
27 }
28
29 // The 2^power-th ancestor is the 2^(power-1)-th ancestor
30 // of the 2^(power-1)-th ancestor
31 const intermediateAncestor = ancestorTable[node][power - 1];
32 ancestorTable[node][power] = ancestorTable[intermediateAncestor][power - 1];
33 }
34 }
35}
36
37/**
38 * Finds the k-th ancestor of the given node
39 * @param node - The starting node
40 * @param k - The number of ancestors to traverse up
41 * @returns The k-th ancestor of the node, or -1 if it doesn't exist
42 */
43function getKthAncestor(node: number, k: number): number {
44 // Use binary representation of k to find the k-th ancestor
45 // by jumping through powers of 2
46 for (let bitPosition = 17; bitPosition >= 0; bitPosition--) {
47 // Check if the current bit is set in k
48 if (((k >> bitPosition) & 1) === 1) {
49 // Jump 2^bitPosition ancestors up
50 node = ancestorTable[node][bitPosition];
51
52 // If we've reached an invalid ancestor, stop
53 if (node === -1) {
54 break;
55 }
56 }
57 }
58
59 return node;
60}
61
Time and Space Complexity
Time Complexity:
-
Initialization (
__init__
):O(n * log n)
wheren
is the number of nodes. The algorithm builds a binary lifting table with18
levels (since2^17 > 50000
, which covers the constraint). For each of thelog n
levels, we iterate through alln
nodes to compute the2^j
-th ancestor. -
Query (
getKthAncestor
):O(log k)
wherek
is the ancestor distance. The method iterates through at most18
bits (constant) to find the k-th ancestor by decomposingk
into powers of 2. Each bit check and node update operation takesO(1)
time.
Space Complexity:
O(n * log n)
for storing the binary lifting tableself.p
. The table has dimensionsn × 18
, wheren
is the number of nodes and18
represents⌈log₂(50000)⌉ + 1
levels needed to handle the maximum possible tree height. Each cell stores an integer representing an ancestor node index.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Binary Representation Iteration
Pitfall: A common mistake is iterating through the bits of k
in the wrong order or using an incorrect bit-checking mechanism. Some developers might try:
# Wrong approach 1: Using string conversion
binary_k = bin(k)[2:] # Convert to binary string
for i, bit in enumerate(binary_k):
if bit == '1':
node = self.ancestors[node][len(binary_k) - i - 1]
# Wrong approach 2: Iterating from low to high bits incorrectly
for i in range(18):
if k & (1 << i): # This checks bits from low to high
node = self.ancestors[node][i]
Why it's problematic: While both approaches might seem logical, they can lead to:
- The first approach has unnecessary string conversion overhead and complex indexing
- The second approach works but processes bits in a different order than the standard implementation
Solution: Use the standard bit-checking pattern that processes bits from high to low:
for i in range(17, -1, -1):
if (k >> i) & 1: # Check if i-th bit is set
node = self.ancestors[node][i]
if node == -1:
break
2. Forgetting to Handle -1 During Table Construction
Pitfall: Not checking for -1 (non-existent ancestors) when building the binary lifting table:
# Wrong: This will cause index out of bounds errors
for j in range(1, 18):
for i in range(n):
self.ancestors[i][j] = self.ancestors[self.ancestors[i][j-1]][j-1]
Why it's problematic: If self.ancestors[i][j-1]
is -1 (meaning the ancestor doesn't exist), trying to access self.ancestors[-1][j-1]
will either:
- Return an unexpected value (Python allows negative indexing)
- Cause errors in other languages
Solution: Always check for -1 before using it as an index:
for j in range(1, 18):
for i in range(n):
if self.ancestors[i][j - 1] == -1:
continue # Or set self.ancestors[i][j] = -1
self.ancestors[i][j] = self.ancestors[self.ancestors[i][j-1]][j-1]
3. Insufficient Table Size for Large Trees
Pitfall: Using a fixed table size that's too small:
# Wrong: Only supports trees up to depth 15 max_power = 16 # 2^15 = 32,768
Why it's problematic: If the tree has a depth greater than 2^(max_power-1), some queries will fail to find ancestors that actually exist.
Solution: Calculate the appropriate table size based on the maximum possible tree depth:
import math
def __init__(self, n: int, parent: List[int]):
# Calculate the maximum power needed
max_power = math.ceil(math.log2(n)) + 1 if n > 1 else 1
# Or use a safe upper bound like 18 (2^17 = 131,072)
max_power = min(18, max_power)
4. Not Breaking Early When Node Becomes -1
Pitfall: Continuing to process bits even after the node becomes -1:
# Wrong: Doesn't stop when ancestor doesn't exist
def getKthAncestor(self, node: int, k: int) -> int:
for i in range(17, -1, -1):
if (k >> i) & 1:
node = self.ancestors[node][i]
return node
Why it's problematic: Once node
becomes -1, accessing self.ancestors[-1][i]
might return unexpected values due to Python's negative indexing, potentially returning a wrong ancestor instead of -1.
Solution: Break immediately when node becomes -1:
def getKthAncestor(self, node: int, k: int) -> int:
for i in range(17, -1, -1):
if (k >> i) & 1:
node = self.ancestors[node][i]
if node == -1:
break # Stop early
return node
5. Misunderstanding the Parent Array Format
Pitfall: Assuming parent[0] should be 0 (self-loop) instead of -1:
# Wrong assumption about input format if parent[0] != -1: parent[0] = -1 # "Fixing" the root
Why it's problematic: The problem specification states that the root node (node 0) has no parent, represented as -1 in the parent array. Modifying this breaks the tree structure.
Solution: Accept the parent array as-is, where parent[0] = -1 indicates the root:
# Correct: Use parent array directly
for node, parent_node in enumerate(parent):
self.ancestors[node][0] = parent_node # parent[0] will be -1
Which technique can we use to find the middle of a linked list?
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 assets algo monster 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 As the name suggests
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
Want a Structured Path to Master System Design Too? Don’t Miss This!