Company Queries I
Problem Overview
| Attribute | Value |
|---|---|
| Source | CSES Problem 1687 |
| Difficulty | Medium |
| Category | Tree Algorithms |
| Time Limit | 1 second |
| Key Technique | Binary Lifting |
Learning Goals
After solving this problem, you will be able to:
- Understand and implement the binary lifting technique for ancestor queries
- Preprocess a tree to answer k-th ancestor queries in O(log k) time
- Recognize when binary lifting is applicable (any problem requiring “jumping” k steps)
- Build sparse tables for tree structures
Problem Statement
Problem: A company has n employees numbered 1 to n, where employee 1 is the general director (root). Each employee except the director has a direct boss. Given q queries, for each query (x, k), find the k-th ancestor of employee x (i.e., the boss k levels above x).
Input:
- Line 1: Two integers n and q (number of employees and queries)
- Line 2: n-1 integers describing the boss of employees 2, 3, …, n
- Next q lines: Two integers x and k (employee and ancestor level)
Output:
- For each query, print the k-th ancestor of x, or -1 if it doesn’t exist
Constraints:
- 1 <= n, q <= 2 x 10^5
- 1 <= x <= n
- 1 <= k <= n
Example
Input:
5 3
1 1 2 2
4 1
4 2
4 3
Output:
2
1
-1
Explanation:
- Employee 4’s parent (1st ancestor) is employee 2
- Employee 4’s grandparent (2nd ancestor) is employee 1
- Employee 4 has no 3rd ancestor (only 2 levels above), so output -1
The company hierarchy:
1 (Director)
/ \
2 3
/ \
4 5
Intuition: How to Think About This Problem
Pattern Recognition
Key Question: How can we efficiently find the k-th ancestor when k can be as large as n?
If we traverse parent-by-parent, each query takes O(k) time. With q queries and k up to n, this gives O(q * n) = O(n^2) which is too slow for n = 2 x 10^5.
The Binary Lifting Insight
Key Idea: Any positive integer k can be represented as a sum of powers of 2.
For example: 13 = 8 + 4 + 1 = 2^3 + 2^2 + 2^0
Instead of jumping 1 step at a time, we can jump in powers of 2:
- Jump 8 steps, then 4 steps, then 1 step to reach the 13th ancestor
This requires precomputing “jump tables” where up[node][j] = the 2^j-th ancestor of node.
Breaking Down the Problem
- What are we looking for? The k-th ancestor of a given node
- What information do we have? The parent of each node
- What’s the relationship? k-th ancestor = (k-1)-th ancestor’s parent
Analogy
Think of binary lifting like an express elevator system:
- Regular stairs: Go up 1 floor at a time (slow)
- Express elevators: Jump 1, 2, 4, 8, 16… floors at once
- To reach floor 13: Take elevator +8, then +4, then +1
Solution 1: Brute Force (Naive Parent Traversal)
Idea
For each query, simply walk up the tree k times by following parent pointers.
Algorithm
- For query (x, k): Start at node x
- Follow parent pointers k times
- If we reach root before k steps, return -1
- Otherwise, return the node we end up at
Code
def solve_brute_force(n, q, bosses, queries):
"""
Brute force: walk up parent pointers k times.
Time: O(q * n) - worst case O(n) per query
Space: O(n)
"""
# parent[i] = direct boss of employee i (1-indexed)
parent = [0] * (n + 1)
parent[1] = 0 # root has no parent
for i in range(2, n + 1):
parent[i] = bosses[i - 2]
results = []
for x, k in queries:
current = x
for _ in range(k):
if current == 0: # reached beyond root
current = -1
break
current = parent[current]
results.append(current if current != 0 else -1)
return results
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(q * n) | Each query may traverse up to n nodes |
| Space | O(n) | Parent array storage |
Why This Works (But Is Slow)
Correctness is obvious - we literally follow the definition. But with n, q up to 2 x 10^5, this gives 4 x 10^10 operations, far too slow.
Solution 2: Binary Lifting (Optimal)
Key Insight
The Trick: Precompute
up[x][j]= the 2^j-th ancestor of x, then decompose any k into powers of 2.
State Definition
| State | Meaning |
|---|---|
up[x][j] |
The (2^j)-th ancestor of node x, or 0 if it doesn’t exist |
In plain English: up[x][j] tells us who is exactly 2^j levels above employee x.
State Transition (Building the Table)
up[x][0] = parent[x] # 2^0 = 1st ancestor (direct parent)
up[x][j] = up[up[x][j-1]][j-1] # 2^j-th ancestor = 2^(j-1)-th ancestor of 2^(j-1)-th ancestor
Why? To go 2^j steps up, first go 2^(j-1) steps, then another 2^(j-1) steps.
- Example: 8th ancestor = 4th ancestor’s 4th ancestor
Base Cases
| Case | Value | Reason |
|---|---|---|
up[x][0] |
parent[x] | Direct parent is 2^0 = 1st ancestor |
up[1][j] |
0 (for all j) | Root has no ancestors |
Algorithm
Preprocessing:
- Initialize
up[x][0] = parent[x]for all nodes - For j from 1 to LOG:
up[x][j] = up[up[x][j-1]][j-1]
Query (x, k):
- For each bit position j where bit j is set in k:
- Jump from current node to its 2^j-th ancestor
- If we ever reach node 0, return -1
- Return final node
Dry Run Example
Let’s trace through finding the 5th ancestor of node 6 in this tree:
Tree structure (parent array for nodes 2-8: [1,1,2,2,3,3,4]):
1
/ \
2 3
/ \ / \
4 5 6 7
/
8
Binary lifting table (LOG = 3, covering up to 2^3 = 8 ancestors):
Node | up[x][0] | up[x][1] | up[x][2]
| (2^0=1) | (2^1=2) | (2^2=4)
-----|----------|----------|----------
1 | 0 | 0 | 0
2 | 1 | 0 | 0
3 | 1 | 0 | 0
4 | 2 | 1 | 0
5 | 2 | 1 | 0
6 | 3 | 1 | 0
7 | 3 | 1 | 0
8 | 4 | 2 | 1
Query: Find 5th ancestor of node 6
k = 5 = 101 in binary = 2^2 + 2^0
Step 1: k = 5, check bit 0 (set)
- Jump 2^0 = 1 step: node 6 -> up[6][0] = 3
- current = 3
Step 2: k = 5, check bit 1 (not set)
- No jump
Step 3: k = 5, check bit 2 (set)
- Jump 2^2 = 4 steps: node 3 -> up[3][2] = 0
- current = 0
Result: 0 means no ancestor exists, return -1
Another example: Find 2nd ancestor of node 8
k = 2 = 10 in binary = 2^1
Step 1: k = 2, check bit 0 (not set)
- No jump, current = 8
Step 2: k = 2, check bit 1 (set)
- Jump 2^1 = 2 steps: node 8 -> up[8][1] = 2
- current = 2
Result: 2 (correct: 8's parent is 4, 4's parent is 2)
Visual Diagram
Binary Lifting Table Construction:
1 (root)
/ \
2 3 up[x][0]: direct parent
/ \ \ up[x][1]: grandparent (parent's parent)
4 5 6 up[x][2]: great-great-grandparent
For node 4:
up[4][0] = 2 (parent)
up[4][1] = up[up[4][0]][0] = up[2][0] = 1 (grandparent)
up[4][2] = up[up[4][1]][1] = up[1][1] = 0 (no 4th ancestor)
Query k=3 from node 4:
3 = 11 binary = 2^1 + 2^0
Start: node 4
Bit 0 set: jump 2^0 -> up[4][0] = 2
Bit 1 set: jump 2^1 -> up[2][1] = 0
Result: -1 (no 3rd ancestor)
Code
Python Implementation:
import sys
from typing import List, Tuple
sys.setrecursionlimit(300000)
def solve_binary_lifting(n: int, q: int, bosses: List[int], queries: List[Tuple[int, int]]) -> List[int]:
"""
Binary lifting solution for k-th ancestor queries.
Time: O((n + q) * log n) - O(n log n) preprocessing, O(log n) per query
Space: O(n * log n) - binary lifting table
"""
LOG = 18 # log2(2 * 10^5) < 18
# up[x][j] = 2^j-th ancestor of node x (0 means no ancestor)
up = [[0] * LOG for _ in range(n + 1)]
# Base case: up[x][0] = parent[x]
up[1][0] = 0 # root has no parent
for i in range(2, n + 1):
up[i][0] = bosses[i - 2]
# Build table: up[x][j] = up[up[x][j-1]][j-1]
for j in range(1, LOG):
for x in range(1, n + 1):
if up[x][j - 1] != 0:
up[x][j] = up[up[x][j - 1]][j - 1]
# Answer queries
results = []
for x, k in queries:
current = x
for j in range(LOG):
if current == 0:
break
if k & (1 << j): # if bit j is set in k
current = up[current][j]
results.append(current if current != 0 else -1)
return results
def main():
"""Main function to read input and output results."""
input_data = sys.stdin.read().split()
idx = 0
n, q = int(input_data[idx]), int(input_data[idx + 1])
idx += 2
bosses = [int(input_data[idx + i]) for i in range(n - 1)]
idx += n - 1
queries = []
for _ in range(q):
x, k = int(input_data[idx]), int(input_data[idx + 1])
queries.append((x, k))
idx += 2
results = solve_binary_lifting(n, q, bosses, queries)
print('\n'.join(map(str, results)))
if __name__ == "__main__":
main()
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O((n + q) log n) | O(n log n) preprocessing + O(log n) per query |
| Space | O(n log n) | Binary lifting table with n nodes and log n levels |
Common Mistakes
Mistake 1: Wrong Table Dimension
# WRONG: LOG too small
LOG = 10 # Only handles k up to 1024
# CORRECT: LOG should cover max possible depth
LOG = 18 # Handles k up to 2^18 > 2 * 10^5
Problem: If LOG is too small, queries with large k will give wrong answers. Fix: Use LOG = ceil(log2(n)) + 1, or safely use LOG = 20 for n <= 10^6.
Mistake 2: Off-by-One in Parent Array
# WRONG: Using 0-indexed parent array with 1-indexed nodes
for i in range(2, n + 1):
up[i][0] = bosses[i - 1] # Index error!
# CORRECT: bosses has n-1 elements for nodes 2 to n
for i in range(2, n + 1):
up[i][0] = bosses[i - 2] # bosses[0] is parent of node 2
Problem: Input gives parents for nodes 2, 3, …, n in order. Fix: Carefully handle the offset between node index and array index.
Mistake 3: Not Handling “No Ancestor” Case
# WRONG: Not checking for 0 during traversal
for j in range(LOG):
if k & (1 << j):
current = up[current][j]
# May access up[0][j] which is undefined
# CORRECT: Stop when we reach 0
for j in range(LOG):
if current == 0:
break
if k & (1 << j):
current = up[current][j]
Problem: Once current becomes 0 (no ancestor), we must stop. Fix: Add early termination check.
Mistake 4: Confusing 0 and -1
# WRONG: Returning 0 as "no ancestor"
return current # 0 is ambiguous if node 0 existed
# CORRECT: Convert 0 to -1 for output
return current if current != 0 else -1
Problem: We use 0 internally to mean “no ancestor”, but output expects -1. Fix: Translate internal representation to expected output format.
Edge Cases
| Case | Input | Expected Output | Why |
|---|---|---|---|
| Root has no k-th ancestor | x=1, k=1 | -1 | Root has no parent |
| k = 0 | x=5, k=0 | 5 | 0th ancestor is the node itself (handle if required) |
| k > depth of node | x=4, k=10 (depth=2) | -1 | Not enough ancestors |
| k exactly equals depth | x=4, k=2 (root at depth 2) | 1 | Returns the root |
| Linear tree (chain) | n nodes in chain, k=n-1 | 1 | Maximum depth case |
| Star tree | All nodes connect to root | x=i, k=2 | -1 for any k >= 2 |
When to Use This Pattern
Use Binary Lifting When:
- You need to find k-th ancestors for multiple queries
- k can be large (close to n), making O(k) per query too slow
- You need to preprocess once and answer many queries
- Combined with LCA algorithms (Lowest Common Ancestor)
- Path queries on trees that require “jumping” to ancestors
Don’t Use When:
- Only a few queries with small k (naive is simpler)
- The tree changes dynamically (need Link-Cut Trees instead)
- You only need parent or immediate ancestors (direct array lookup)
- Memory is extremely constrained (table uses O(n log n) space)
Pattern Recognition Checklist:
- Need to find k-th ancestor? -> Binary Lifting
- Need LCA of two nodes? -> Binary Lifting + depth comparison
- Need to query path between two nodes? -> Binary Lifting to LCA + path processing
- Multiple ancestor queries on static tree? -> Definitely Binary Lifting
Related Problems
Easier (Do These First)
| Problem | Why It Helps |
|---|---|
| Subordinates (CSES 1674) | Basic tree traversal and subtree size |
| Tree Distances I (CSES 1132) | Understanding tree depth and distance |
Similar Difficulty
| Problem | Key Difference |
|---|---|
| Company Queries II (CSES 1688) | Uses binary lifting for LCA queries |
| Distance Queries (CSES 1135) | Combines LCA with path distance |
| Kth Ancestor of Tree Node (LC 1483) | Same problem on LeetCode |
Harder (Do These After)
| Problem | New Concept |
|---|---|
| Path Queries II (CSES 1138) | Binary lifting with segment trees on paths |
| Planets Queries II (CSES 1160) | Binary lifting on functional graphs |
| Cycle Finding (CSES 1751) | Detecting cycles using similar jumping technique |
Key Takeaways
-
The Core Idea: Precompute 2^j-th ancestors to answer any k-th ancestor query by decomposing k into powers of 2.
-
Time Optimization: Reduced from O(k) per query to O(log k) by leveraging binary representation.
-
Space Trade-off: Uses O(n log n) extra space to store the jump table, which enables O(log n) queries.
-
Pattern: Binary lifting is a “sparse table” technique for trees, analogous to how sparse tables work for range queries on arrays.
Practice Checklist
Before moving on, make sure you can:
- Build the binary lifting table correctly with proper base cases
- Query k-th ancestor by iterating through bits of k
- Explain why
up[x][j] = up[up[x][j-1]][j-1]works - Handle edge cases (root queries, k > depth)
- Extend this to LCA computation (Company Queries II)
- Implement in both Python and C++ within 15 minutes