Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

1483. Kth Ancestor of a Tree Node - Leetcode Solution

Problem Description

The "Kth Ancestor of a Tree Node" problem asks you to efficiently answer queries about the ancestry of nodes in a rooted tree. You are given a tree with n nodes, labeled from 0 to n-1, where node 0 is the root. The parent of each node is provided in an array parent, where parent[i] is the parent of node i (with parent[0] = -1 for the root).

You must implement a class with two methods:

  • KthAncestor(int n, int[] parent): Constructor to initialize the tree.
  • getKthAncestor(int node, int k): Returns the k-th ancestor of node, or -1 if such ancestor does not exist.
Constraints:
  • The tree is static after initialization.
  • Multiple queries for different node and k will be made.
  • You must answer each query efficiently (ideally in O(log n) time per query).

Thought Process

The naive approach to this problem would be, for each query, to walk up the tree from the given node to its parent, repeating this process k times. However, this can be slow if k is large or if there are many queries, since each step takes O(1) and up to O(k) per query.

To optimize, we look for a way to "jump" up the tree in bigger strides. This is similar to how binary search jumps by powers of two. If we preprocess and store, for each node, its 2j-th ancestor for all valid j, we can decompose any k as a sum of powers of two, and answer each query in O(log k) time. This technique is known as binary lifting.

The challenge is to preprocess the ancestors efficiently and use them for fast queries.

Solution Approach

We use the binary lifting technique to preprocess and answer queries efficiently. Here's how:

  1. Preprocessing:
    • For each node, we calculate and store its 2j-th ancestor for all j such that 2j <= n.
    • This is done using dynamic programming: the 2j-th ancestor of node is the 2j-1-th ancestor of its 2j-1-th ancestor.
    • We build a table up[node][j] where up[node][j] is the 2j-th ancestor of node.
  2. Querying:
    • To find the k-th ancestor, we look at the binary representation of k.
    • For each set bit in k, we "jump" up by the corresponding power of two using our table.
    • If at any point the ancestor does not exist (i.e., is -1), we return -1.
  3. Why Binary Lifting?
    • It allows us to answer each query in O(log n) time, since k can be at most n and we only need to check up to log n bits.
    • Preprocessing takes O(n log n) time and space, which is efficient for large trees and many queries.

Example Walkthrough

Suppose n = 7 and parent = [-1,0,0,1,1,2,2], giving this tree:

  • 0 is the root
  • 1 and 2 are children of 0
  • 3 and 4 are children of 1
  • 5 and 6 are children of 2
Let's answer getKthAncestor(5, 2):
  1. Binary of 2 is 10, so only the 21 bit is set.
  2. From node 5, jump up 21 = 2 steps: up[5][1] is the ancestor 2 steps above 5.
  3. From preprocessing, up[5][0] = 2 (parent), up[5][1] = 0 (grandparent).
  4. So, the answer is 0.
For getKthAncestor(3, 3):
  1. Binary of 3 is 11, so both 20 and 21 bits are set.
  2. First, jump 21=2 steps: up[3][1] = 0.
  3. Then, jump 20=1 step from 0: up[0][0] = -1 (root has no parent).
  4. So, the answer is -1.

Time and Space Complexity

  • Brute-force approach:
    • Time per query: O(k) (walk up the tree k times)
    • Space: O(n) (just the parent array)
  • Binary Lifting approach:
    • Preprocessing: O(n log n) (for each node, compute up to log n ancestors)
    • Query: O(log k) (or O(log n), since k can be at most n)
    • Space: O(n log n) (for the ancestor table)

The optimized approach is much faster for large k and many queries.

Summary

The "Kth Ancestor of a Tree Node" problem is a classic example of using binary lifting to efficiently answer ancestry queries in a tree. By preprocessing each node's ancestors at powers of two, we can quickly "jump" up the tree in logarithmic time per query. This approach is both elegant and practical, making it ideal for problems with many queries and large trees.

Code Implementation

class TreeAncestor:
    def __init__(self, n: int, parent: list[int]):
        LOG = 0
        tmp = n
        while tmp:
            LOG += 1
            tmp //= 2
        self.LOG = LOG
        self.up = [[-1] * LOG for _ in range(n)]
        for v in range(n):
            self.up[v][0] = parent[v]
        for j in range(1, LOG):
            for v in range(n):
                if self.up[v][j-1] != -1:
                    self.up[v][j] = self.up[self.up[v][j-1]][j-1]
    def getKthAncestor(self, node: int, k: int) -> int:
        for j in range(self.LOG):
            if k & (1 << j):
                node = self.up[node][j]
                if node == -1:
                    return -1
        return node
      
class TreeAncestor {
    vector<vector<int>> up;
    int LOG;
public:
    TreeAncestor(int n, vector<int>& parent) {
        LOG = 0;
        int tmp = n;
        while (tmp) {
            LOG++;
            tmp /= 2;
        }
        up.assign(n, vector<int>(LOG, -1));
        for (int v = 0; v < n; ++v)
            up[v][0] = parent[v];
        for (int j = 1; j < LOG; ++j)
            for (int v = 0; v < n; ++v)
                if (up[v][j-1] != -1)
                    up[v][j] = up[up[v][j-1]][j-1];
    }
    int getKthAncestor(int node, int k) {
        for (int j = 0; j < LOG; ++j) {
            if (k & (1 << j)) {
                node = up[node][j];
                if (node == -1)
                    return -1;
            }
        }
        return node;
    }
};
      
class TreeAncestor {
    int[][] up;
    int LOG;
    public TreeAncestor(int n, int[] parent) {
        LOG = 1;
        int tmp = n;
        while (tmp > 1) {
            LOG++;
            tmp /= 2;
        }
        up = new int[n][LOG];
        for (int v = 0; v < n; ++v)
            up[v][0] = parent[v];
        for (int j = 1; j < LOG; ++j)
            for (int v = 0; v < n; ++v)
                up[v][j] = up[v][j-1] == -1 ? -1 : up[up[v][j-1]][j-1];
    }
    public int getKthAncestor(int node, int k) {
        for (int j = 0; j < LOG; ++j) {
            if ((k & (1 << j)) != 0) {
                node = up[node][j];
                if (node == -1)
                    return -1;
            }
        }
        return node;
    }
}
      
var TreeAncestor = function(n, parent) {
    this.LOG = 0;
    let tmp = n;
    while (tmp) {
        this.LOG++;
        tmp = Math.floor(tmp / 2);
    }
    this.up = Array.from({length: n}, () => Array(this.LOG).fill(-1));
    for (let v = 0; v < n; ++v)
        this.up[v][0] = parent[v];
    for (let j = 1; j < this.LOG; ++j)
        for (let v = 0; v < n; ++v)
            if (this.up[v][j-1] !== -1)
                this.up[v][j] = this.up[this.up[v][j-1]][j-1];
};

TreeAncestor.prototype.getKthAncestor = function(node, k) {
    for (let j = 0; j < this.LOG; ++j) {
        if (k & (1 << j)) {
            node = this.up[node][j];
            if (node === -1)
                return -1;
        }
    }
    return node;
};