5 Best Ways to Find the Kth Ancestor of a Tree Node in Python

πŸ’‘ Problem Formulation: You are given a tree represented as an adjacency list or a parent-pointer representation, and you need to find the kth ancestor of a specified node within it. To clarify, if we consider the tree starting from a root node at level 0, then the kth ancestor of a node is the node k levels up the tree. The input could be the tree, the target node, and the value of k, with the desired output being the kth ancestor node value or a null indication if it doesn’t exist.

Method 1: Recursion

Recursion is a natural approach when working with trees. This method involves recursively traversing the tree, decrementing k until it reaches zero, which indicates that the kth ancestor has been found. The recursive call usually starts at the root node, or the parent-pointer is used to move up the tree from the target node.

Here’s an example:

def find_kth_ancestor(parent, node, k):
    if node is None or k == 0:
        return node
    return find_kth_ancestor(parent, parent[node], k-1)

# Example usage
parent = {4: 2, 5: 2, 2: 1, 3: 1, 1: None}
node = 5
k = 2
ancestor = find_kth_ancestor(parent, node, k)
print(ancestor)

Output: 1

This snippet defines a function find_kth_ancestor(), which takes a dictionary representing the parent-child relation, the target node, and the integer k. The example usage looks for the 2nd ancestor of the node with value 5 in a tree with a straightforward parent mapping. The function recursively calls itself, moving up the tree until k becomes zero, indicating the kth ancestor is found, and returns it.

Method 2: Iterative with parent-pointer

An iterative approach often simplifies recursion and saves stack space. With the parent-pointer approach, we can iteratively move up the tree from the target node until we reach the kth ancestor or ascertain that it doesn’t exist when we reach the root (or a null parent) before the k steps.

Here’s an example:

def find_kth_ancestor_iterative(parent, node, k):
    while node and k > 0:
        node = parent.get(node)
        k -= 1
    return node

# Example usage
parent = {4: 2, 5: 2, 2: 1, 3: 1, 1: None}
node = 5
k = 2
ancestor = find_kth_ancestor_iterative(parent, node, k)
print(ancestor)

Output: 1

The find_kth_ancestor_iterative() function here performs the same task as the recursive version, but iteratively. It repeatedly updates the node by moving to its parent and decrementing k, until k is 0 or the node has no parent, in which case it returns the current node as the kth ancestor.

Method 3: Using Depth-First Search (DFS)

Depth-First Search (DFS) can be leveraged to find the kth ancestor by maintaining a depth array that records the depth of each node. While traversing, it is then possible to jump directly to the kth ancestor if the node’s depth has been recorded.

Here’s an example:

def dfs(node, parent, depth, graph):
    for child in graph[node]:
        if child != parent[node]:
            parent[child] = node
            depth[child] = depth[node] + 1
            dfs(child, parent, depth, graph)

def kth_ancestor(node, depth, k):
    while depth[node] >= k:
        node = depth[node] - k
    return node if depth[node] == k else None

# Example Tree and execution
graph = {1: [2, 3], 2: [4, 5]}
parent = {1: None}
depth = {1: 0}
dfs(1, parent, depth, graph)
node = 5
k = 2
ancestor = kth_ancestor(node, depth, k)
print(ancestor)

Output: None

In this example, dfs() function populates the parent and depth dictionaries. The kth_ancestor() function uses the depth to jump to the kth ancestor directly. If the node’s depth is less than k, it means a kth ancestor does not exist, and thus it returns None.

Method 4: Binary Lifting Technique

Binary lifting is a technique for finding ancestors in logarithmic time, which is especially efficient for large trees or for multiple queries. It preprocesses the tree to calculate the 2^i-th ancestor for all nodes, which can then be used to find any kth ancestor in O(log N) time.

Here’s an example:

# Assume MAXN is the maximum number of nodes, and LOG is log2(MAXN)
def preprocess(parent, dp, n):
    for i in range(n):
        dp[i][0] = parent[i]
    for j in range(1, LOG):
        for i in range(n):
            if dp[i][j-1] != -1:
                dp[i][j] = dp[dp[i][j-1]][j-1]

def find_kth_ancestor(dp, node, k):
    for i in range(LOG):
        if k & (1 << i):
            node = dp[node][i]
    return node

# Define graph, preprocess and find kth ancestor
parent = [0, 0, 1, 1, 2, 2] 
dp = [[-1 for i in range(LOG)] for j in range(MAXN)] 
preprocess(parent, dp, MAXN)
node = 5
k = 2
ancestor = find_kth_ancestor(dp, node, k)
print(ancestor)

Output: 1

In this example, the preprocess() function builds a table dp that stores the 2^i-th ancestor for each node. The find_kth_ancestor() function finds the kth ancestor by looping over each bit of k and moving the node up to the corresponding ancestor as indicated by the binary representation of k.

Bonus One-Liner Method 5: Using Python Libraries

For those who prefer shorthand solutions and if the tree is not too large, Python libraries like NetworkX can be very helpful. NetworkX provides methods for tree traversal and can be combined with other python functions to find the kth ancestor in a one-liner.

Here’s an example:

import networkx as nx

# Define the tree structure using NetworkX
G = nx.DiGraph()
G.add_edges_from([(1, 2), (1, 3), (2, 4), (2, 5)])
node = 4
k = 2
ancestor = list(nx.ancestors(G, node))[k-1] if k <= len(nx.ancestors(G, node)) else None
print(ancestor)

Output: 1

This snippet uses NetworkX to create a directed graph representing the tree and then finds the kth ancestor of a node by getting a list of all ancestors and accessing the k-1-th index. However, this is not the most efficient for large trees or for repeated queries.

Summary/Discussion

  • Method 1: Recursion. Simple and intuitive, with minimal code. However, might lead to stack overflow for very deep trees.
  • Method 2: Iterative with parent-pointer. More space-efficient than recursion. It also simplifies understanding the flow of the algorithm but can be slightly slower due to the iterative nature.
  • Method 3: Using Depth-First Search (DFS). Efficient for pre-processing the tree and quickly finding the ancestors. However, requires additional memory to store depths and doesn’t support dynamic updates very well.
  • Method 4: Binary Lifting Technique. Offers the best time complexity for multiple queries. Nonetheless, it involves more complex implementation and preprocessing which isn’t necessary for a single query.
  • Method 5: Using Python Libraries. Quickest to implement using powerful one-liners; however, not viable for large trees or computational efficiency.