Facebook Pixel

1993. Operations on Tree

Problem Description

You need to implement a data structure for a tree that supports locking operations. The tree has n nodes numbered from 0 to n - 1, where node 0 is the root. The tree structure is given as a parent array where parent[i] indicates the parent of node i. Since node 0 is the root, parent[0] = -1.

The data structure must support three operations:

1. Lock Operation (lock(num, user))

  • Locks node num for user user
  • Returns true if locking is successful, false otherwise
  • A node can only be locked if it's currently unlocked
  • Once locked by a user, other users cannot lock the same node

2. Unlock Operation (unlock(num, user))

  • Unlocks node num for user user
  • Returns true if unlocking is successful, false otherwise
  • A node can only be unlocked by the same user who locked it

3. Upgrade Operation (upgrade(num, user))

  • Locks node num for user user AND unlocks all its descendants
  • Returns true if upgrade is successful, false otherwise
  • The upgrade can only happen when ALL three conditions are met:
    • The node num itself is unlocked
    • At least one descendant of num is locked (by any user)
    • No ancestor of num is locked

The solution uses:

  • self.locked: An array tracking which user locked each node (-1 means unlocked)
  • self.parent: The given parent array
  • self.children: A list of children for each node (built from the parent array)

The upgrade operation uses DFS to traverse and unlock all descendants, while also checking if any descendants are locked. It traverses up the tree to verify no ancestors are locked before performing the upgrade.

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 edges (parent-child relationships). A tree is a special type of graph.

Is it a tree?

  • Yes: The problem explicitly states we're working with a tree structure. We have n nodes with parent-child relationships defined by the parent array, and node 0 is the root.

DFS

  • Conclusion: According to the flowchart, when we have a tree structure, the appropriate algorithm pattern is DFS (Depth-First Search).

Why DFS is the Right Choice

The flowchart correctly identifies DFS as the solution pattern because:

  1. Tree Traversal: The upgrade operation requires traversing all descendants of a given node to unlock them. DFS is perfect for exploring all nodes in a subtree.

  2. Recursive Nature: The tree structure naturally lends itself to recursive traversal, which is exactly what DFS provides. In the solution, the dfs function recursively visits each child and their descendants.

  3. Complete Subtree Exploration: The upgrade operation needs to:

    • Check if any descendant is locked (requires visiting all descendants)
    • Unlock all locked descendants (requires accessing every node in the subtree)

    DFS ensures we visit every node in the subtree exactly once, making it efficient for this operation.

  4. Parent-Child Relationships: The solution builds a children array to facilitate DFS traversal from any node downward through its descendants, which is essential for the upgrade operation.

The DFS pattern in the solution is implemented in the dfs helper function within the upgrade method, which recursively traverses all children and their descendants to perform the required unlocking operations.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is recognizing that we need to efficiently track lock states and navigate both upward and downward in the tree structure.

For the basic lock and unlock operations, we simply need to track who has locked each node. A simple array where locked[i] stores the user ID (or -1 if unlocked) handles this elegantly.

The challenging part is the upgrade operation, which has three distinct requirements that need different traversal strategies:

  1. Check no ancestors are locked: We need to traverse upward from the node to the root. Since we have the parent array, we can simply follow parent[x] repeatedly until we reach the root (parent[x] = -1). If we find any locked node along this path, the upgrade fails.

  2. Check at least one descendant is locked AND unlock all descendants: These two requirements can be handled together with a single downward traversal. This is where DFS shines - we recursively visit all children and their descendants. During this traversal, we:

    • Track if we found any locked descendant using a flag variable
    • Unlock any locked nodes we encounter

The beauty of this approach is that we combine the checking and unlocking in a single DFS pass, making it efficient.

To enable downward traversal, we preprocess the parent array to build a children list for each node. This one-time preprocessing in the constructor allows us to quickly access all children of any node during DFS.

The solution elegantly uses:

  • Upward traversal (following parent pointers) for ancestor checking
  • Downward traversal (DFS through children) for descendant operations
  • State tracking with the locked array for O(1) lock status checks

This combination of bidirectional tree navigation and simple state management provides an efficient solution to all three operations.

Learn more about Tree, Depth-First Search and Breadth-First Search patterns.

Solution Approach

Let's walk through the implementation of each component and operation in the LockingTree class:

Data Structure Initialization

The constructor sets up three key data structures:

  1. self.locked: An array of size n initialized with -1, where locked[i] stores the user ID who locked node i, or -1 if unlocked.

  2. self.parent: Stores the given parent array for upward traversal.

  3. self.children: A list of lists built from the parent array. For each node i, children[i] contains all direct children of node i. This is constructed by iterating through the parent array starting from index 1 (skipping root) and appending each node to its parent's children list.

for son, fa in enumerate(parent[1:], 1):
    self.children[fa].append(son)

Lock Operation

The lock method is straightforward:

  • Check if locked[num] == -1 (node is unlocked)
  • If yes, set locked[num] = user and return True
  • Otherwise, return False

Time complexity: O(1)

Unlock Operation

The unlock method verifies ownership:

  • Check if locked[num] == user (same user who locked it)
  • If yes, set locked[num] = -1 and return True
  • Otherwise, return False

Time complexity: O(1)

Upgrade Operation

The upgrade method is the most complex, implementing three checks:

Step 1: Check no ancestors are locked

x = num
while x != -1:
    if self.locked[x] != -1:
        return False
    x = self.parent[x]

Starting from num, traverse upward to the root. If any ancestor is locked, immediately return False.

Step 2: Find and unlock all locked descendants

def dfs(x: int):
    nonlocal find
    for y in self.children[x]:
        if self.locked[y] != -1:
            self.locked[y] = -1
            find = True
        dfs(y)

The DFS helper function:

  • Recursively visits all children and their descendants
  • When a locked node is found, unlocks it (locked[y] = -1) and sets find = True
  • The nonlocal find allows the nested function to modify the outer scope's find variable

Step 3: Finalize the upgrade

find = False
dfs(num)
if not find:
    return False
self.locked[num] = user
return True
  • Initialize find to False before DFS
  • Run DFS to unlock descendants and detect if any were locked
  • If no locked descendants were found (find is still False), return False
  • Otherwise, lock the node for the user and return True

Time Complexity Analysis

  • Lock/Unlock: O(1) - Simple array access and update
  • Upgrade: O(h + d) where h is the height from node to root (for ancestor check) and d is the number of descendants (for DFS traversal)

The solution efficiently handles all operations by maintaining proper data structures for both upward and downward tree traversal, making it practical even for large trees.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with a small tree to illustrate how the locking operations work.

Tree Structure:

parent = [-1, 0, 0, 1, 1, 3]

Tree visualization:
        0
       / \
      1   2
     / \
    3   4
   /
  5

Initial Setup:

  • locked = [-1, -1, -1, -1, -1, -1] (all nodes unlocked)
  • children = [[1,2], [3,4], [], [5], [], []]

Operation Sequence:

  1. lock(1, 10) → Returns true

    • Check: locked[1] == -1? Yes, node 1 is unlocked
    • Action: Set locked[1] = 10
    • State: locked = [-1, 10, -1, -1, -1, -1]
  2. lock(3, 20) → Returns true

    • Check: locked[3] == -1? Yes, node 3 is unlocked
    • Action: Set locked[3] = 20
    • State: locked = [-1, 10, -1, 20, -1, -1]
  3. unlock(3, 10) → Returns false

    • Check: locked[3] == 10? No, it's locked by user 20
    • Action: No change
    • State: locked = [-1, 10, -1, 20, -1, -1]
  4. upgrade(0, 30) → Returns false

    • Check ancestors of node 0: Node 0 has no ancestors (it's the root)
    • Check descendants: Found locked nodes 1 and 3
    • But node 0's direct child (node 1) is locked, violating the "node itself must be unlocked" condition
    • Action: No change
  5. unlock(1, 10) → Returns true

    • Check: locked[1] == 10? Yes
    • Action: Set locked[1] = -1
    • State: locked = [-1, -1, -1, 20, -1, -1]
  6. upgrade(1, 40) → Returns true

    • Check ancestors: Traverse 1→0, check if any are locked
      • locked[1] == -1? Yes (unlocked)
      • locked[0] == -1? Yes (unlocked)
    • Check descendants via DFS from node 1:
      • Visit child 3: locked[3] == 20 (locked!) → Unlock it, set find = true
      • Visit child 4: locked[4] == -1 (already unlocked)
      • Visit grandchild 5: locked[5] == -1 (already unlocked)
    • Since find == true (found at least one locked descendant), proceed
    • Action: Lock node 1 for user 40
    • Final state: locked = [-1, 40, -1, -1, -1, -1]
  7. upgrade(3, 50) → Returns false

    • Check ancestors: Traverse 3→1→0
      • locked[3] == -1? Yes (unlocked)
      • locked[1] == 40? No! Node 1 is locked
    • Since an ancestor is locked, immediately return false
    • Action: No change

This example demonstrates all three operations and shows how the upgrade operation carefully checks both ancestors (upward traversal) and descendants (DFS traversal) while managing the lock states.

Solution Implementation

1from typing import List
2
3class LockingTree:
4    def __init__(self, parent: List[int]):
5        """
6        Initialize the locking tree with parent relationships.
7      
8        Args:
9            parent: List where parent[i] is the parent of node i, parent[0] = -1 (root)
10        """
11        n = len(parent)
12        # Store which user locked each node (-1 means unlocked)
13        self.locked = [-1] * n
14        # Store parent of each node
15        self.parent = parent
16        # Build adjacency list for children of each node
17        self.children = [[] for _ in range(n)]
18      
19        # Populate children list (skip root at index 0)
20        for child_node, parent_node in enumerate(parent[1:], 1):
21            self.children[parent_node].append(child_node)
22
23    def lock(self, num: int, user: int) -> bool:
24        """
25        Lock a node if it's currently unlocked.
26      
27        Args:
28            num: Node to lock
29            user: User attempting to lock
30          
31        Returns:
32            True if lock successful, False otherwise
33        """
34        if self.locked[num] == -1:  # Node is unlocked
35            self.locked[num] = user
36            return True
37        return False
38
39    def unlock(self, num: int, user: int) -> bool:
40        """
41        Unlock a node if it's locked by the same user.
42      
43        Args:
44            num: Node to unlock
45            user: User attempting to unlock
46          
47        Returns:
48            True if unlock successful, False otherwise
49        """
50        if self.locked[num] == user:  # Node is locked by this user
51            self.locked[num] = -1
52            return True
53        return False
54
55    def upgrade(self, num: int, user: int) -> bool:
56        """
57        Upgrade a node by locking it and unlocking all descendants if:
58        1. The node and all ancestors are unlocked
59        2. At least one descendant is locked
60      
61        Args:
62            num: Node to upgrade
63            user: User attempting to upgrade
64          
65        Returns:
66            True if upgrade successful, False otherwise
67        """
68        # Check if any ancestor (including self) is locked
69        current_node = num
70        while current_node != -1:
71            if self.locked[current_node] != -1:  # Found a locked ancestor
72                return False
73            current_node = self.parent[current_node]
74      
75        # Check and unlock all locked descendants
76        has_locked_descendant = False
77      
78        def unlock_descendants(node: int) -> None:
79            """DFS to unlock all descendants and track if any were locked."""
80            nonlocal has_locked_descendant
81          
82            for child in self.children[node]:
83                if self.locked[child] != -1:  # Child is locked
84                    self.locked[child] = -1  # Unlock it
85                    has_locked_descendant = True
86                unlock_descendants(child)  # Continue to child's descendants
87      
88        unlock_descendants(num)
89      
90        # If no locked descendants found, upgrade fails
91        if not has_locked_descendant:
92            return False
93      
94        # Lock the current node with the user
95        self.locked[num] = user
96        return True
97
98
99# Your LockingTree object will be instantiated and called as such:
100# obj = LockingTree(parent)
101# param_1 = obj.lock(num, user)
102# param_2 = obj.unlock(num, user)
103# param_3 = obj.upgrade(num, user)
104
1class LockingTree {
2    // Array to store which user has locked each node (-1 means unlocked)
3    private int[] lockedBy;
4    // Array to store parent of each node
5    private int[] parent;
6    // Array of lists to store children of each node
7    private List<Integer>[] children;
8
9    /**
10     * Initialize the locking tree with parent relationships
11     * @param parent Array where parent[i] is the parent of node i
12     */
13    public LockingTree(int[] parent) {
14        int n = parent.length;
15      
16        // Initialize arrays
17        this.lockedBy = new int[n];
18        this.parent = parent;
19        this.children = new List[n];
20      
21        // Initially all nodes are unlocked
22        Arrays.fill(this.lockedBy, -1);
23      
24        // Initialize children lists for each node
25        Arrays.setAll(this.children, i -> new ArrayList<>());
26      
27        // Build children relationships from parent array
28        for (int i = 1; i < n; i++) {
29            this.children[parent[i]].add(i);
30        }
31    }
32
33    /**
34     * Lock a node by a user
35     * @param num The node to lock
36     * @param user The user attempting to lock
37     * @return true if lock successful, false if already locked
38     */
39    public boolean lock(int num, int user) {
40        // Check if node is unlocked
41        if (lockedBy[num] == -1) {
42            lockedBy[num] = user;
43            return true;
44        }
45        return false;
46    }
47
48    /**
49     * Unlock a node by a user
50     * @param num The node to unlock
51     * @param user The user attempting to unlock
52     * @return true if unlock successful, false if not locked by this user
53     */
54    public boolean unlock(int num, int user) {
55        // Check if node is locked by the requesting user
56        if (lockedBy[num] == user) {
57            lockedBy[num] = -1;
58            return true;
59        }
60        return false;
61    }
62
63    /**
64     * Upgrade a node: lock it and unlock all descendants
65     * Conditions: node and ancestors must be unlocked, at least one descendant must be locked
66     * @param num The node to upgrade
67     * @param user The user attempting to upgrade
68     * @return true if upgrade successful, false otherwise
69     */
70    public boolean upgrade(int num, int user) {
71        // Check if the node and all its ancestors are unlocked
72        int currentNode = num;
73        while (currentNode != -1) {
74            if (lockedBy[currentNode] != -1) {
75                return false;
76            }
77            currentNode = parent[currentNode];
78        }
79      
80        // Check for locked descendants and unlock them
81        boolean[] hasLockedDescendant = new boolean[1];
82        unlockDescendants(num, hasLockedDescendant);
83      
84        // If no locked descendants found, upgrade fails
85        if (!hasLockedDescendant[0]) {
86            return false;
87        }
88      
89        // Lock the current node
90        lockedBy[num] = user;
91        return true;
92    }
93
94    /**
95     * DFS to unlock all descendants and check if any were locked
96     * @param node Current node in traversal
97     * @param hasLockedDescendant Flag to track if any descendant was locked
98     */
99    private void unlockDescendants(int node, boolean[] hasLockedDescendant) {
100        // Process all children
101        for (int child : children[node]) {
102            // If child is locked, unlock it and set flag
103            if (lockedBy[child] != -1) {
104                lockedBy[child] = -1;
105                hasLockedDescendant[0] = true;
106            }
107            // Recursively process descendants
108            unlockDescendants(child, hasLockedDescendant);
109        }
110    }
111}
112
113/**
114 * Your LockingTree object will be instantiated and called as such:
115 * LockingTree obj = new LockingTree(parent);
116 * boolean param_1 = obj.lock(num,user);
117 * boolean param_2 = obj.unlock(num,user);
118 * boolean param_3 = obj.upgrade(num,user);
119 */
120
1class LockingTree {
2private:
3    vector<int> lockedBy;           // lockedBy[i] = user who locked node i (-1 if unlocked)
4    vector<int> parentNode;         // parentNode[i] = parent of node i
5    vector<vector<int>> childNodes; // childNodes[i] = list of children of node i
6  
7public:
8    LockingTree(vector<int>& parent) {
9        int nodeCount = parent.size();
10      
11        // Initialize data structures
12        lockedBy = vector<int>(nodeCount, -1);
13        parentNode = parent;
14        childNodes.resize(nodeCount);
15      
16        // Build adjacency list for children
17        for (int i = 1; i < nodeCount; ++i) {
18            childNodes[parent[i]].push_back(i);
19        }
20    }
21  
22    bool lock(int num, int user) {
23        // Can only lock if node is currently unlocked
24        if (lockedBy[num] == -1) {
25            lockedBy[num] = user;
26            return true;
27        }
28        return false;
29    }
30  
31    bool unlock(int num, int user) {
32        // Can only unlock if node is locked by the same user
33        if (lockedBy[num] == user) {
34            lockedBy[num] = -1;
35            return true;
36        }
37        return false;
38    }
39  
40    bool upgrade(int num, int user) {
41        // Check condition 1: node must be unlocked
42        if (lockedBy[num] != -1) {
43            return false;
44        }
45      
46        // Check condition 2: all ancestors must be unlocked
47        int currentNode = num;
48        while (currentNode != -1) {
49            if (lockedBy[currentNode] != -1) {
50                return false;
51            }
52            currentNode = parentNode[currentNode];
53        }
54      
55        // Check condition 3: at least one descendant must be locked
56        // Also unlock all locked descendants
57        bool hasLockedDescendant = false;
58      
59        function<void(int)> unlockDescendants = [&](int node) {
60            // Process all children of current node
61            for (int child : childNodes[node]) {
62                // If child is locked, unlock it and mark that we found a locked descendant
63                if (lockedBy[child] != -1) {
64                    hasLockedDescendant = true;
65                    lockedBy[child] = -1;
66                }
67                // Recursively process child's descendants
68                unlockDescendants(child);
69            }
70        };
71      
72        unlockDescendants(num);
73      
74        // If no locked descendant found, upgrade fails
75        if (!hasLockedDescendant) {
76            return false;
77        }
78      
79        // All conditions met: lock the node with the given user
80        lockedBy[num] = user;
81        return true;
82    }
83};
84
85/**
86 * Your LockingTree object will be instantiated and called as such:
87 * LockingTree* obj = new LockingTree(parent);
88 * bool param_1 = obj->lock(num,user);
89 * bool param_2 = obj->unlock(num,user);
90 * bool param_3 = obj->upgrade(num,user);
91 */
92
1// Array to store which user has locked each node (-1 means unlocked)
2let locked: number[];
3// Array to store parent of each node
4let parentArray: number[];
5// Array to store children of each node
6let childrenArray: number[][];
7
8/**
9 * Initializes the locking tree with parent relationships
10 * @param parent - Array where parent[i] is the parent of node i
11 */
12function initialize(parent: number[]): void {
13    const nodeCount = parent.length;
14  
15    // Initialize locked array with -1 (unlocked state)
16    locked = Array(nodeCount).fill(-1);
17  
18    // Store parent relationships
19    parentArray = parent;
20  
21    // Initialize children array as empty arrays
22    childrenArray = Array(nodeCount)
23        .fill(0)
24        .map(() => []);
25  
26    // Build children relationships from parent array
27    for (let i = 1; i < nodeCount; i++) {
28        childrenArray[parent[i]].push(i);
29    }
30}
31
32/**
33 * Attempts to lock a node for a specific user
34 * @param num - The node to lock
35 * @param user - The user attempting to lock
36 * @returns true if lock successful, false otherwise
37 */
38function lock(num: number, user: number): boolean {
39    // Check if node is currently unlocked
40    if (locked[num] === -1) {
41        locked[num] = user;
42        return true;
43    }
44    return false;
45}
46
47/**
48 * Attempts to unlock a node for a specific user
49 * @param num - The node to unlock
50 * @param user - The user attempting to unlock
51 * @returns true if unlock successful, false otherwise
52 */
53function unlock(num: number, user: number): boolean {
54    // Check if node is locked by the same user
55    if (locked[num] === user) {
56        locked[num] = -1;
57        return true;
58    }
59    return false;
60}
61
62/**
63 * Attempts to upgrade a node's lock status
64 * Upgrade succeeds if:
65 * 1. The node and all ancestors are unlocked
66 * 2. At least one descendant is locked
67 * 3. All locked descendants get unlocked and the node gets locked
68 * @param num - The node to upgrade
69 * @param user - The user attempting to upgrade
70 * @returns true if upgrade successful, false otherwise
71 */
72function upgrade(num: number, user: number): boolean {
73    // Check if node and all its ancestors are unlocked
74    let currentNode = num;
75    while (currentNode !== -1) {
76        if (locked[currentNode] !== -1) {
77            return false;
78        }
79        currentNode = parentArray[currentNode];
80    }
81  
82    // Track if any descendant is locked
83    let hasLockedDescendant = false;
84  
85    /**
86     * DFS to unlock all descendants and check if any were locked
87     * @param nodeIndex - Current node being processed
88     */
89    const unlockDescendants = (nodeIndex: number): void => {
90        for (const childNode of childrenArray[nodeIndex]) {
91            // If child is locked, unlock it and mark that we found a locked descendant
92            if (locked[childNode] !== -1) {
93                locked[childNode] = -1;
94                hasLockedDescendant = true;
95            }
96            // Recursively process all descendants
97            unlockDescendants(childNode);
98        }
99    };
100  
101    // Unlock all descendants and check if any were locked
102    unlockDescendants(num);
103  
104    // If no descendants were locked, upgrade fails
105    if (!hasLockedDescendant) {
106        return false;
107    }
108  
109    // Lock the node for the user
110    locked[num] = user;
111    return true;
112}
113

Time and Space Complexity

Time Complexity:

  • __init__(parent): O(n) where n is the length of the parent array. We iterate through the parent array once to build the children adjacency list.

  • lock(num, user): O(1). We only check and update a single element in the locked array.

  • unlock(num, user): O(1). We only check and update a single element in the locked array.

  • upgrade(num, user): O(n) in the worst case.

    • Checking ancestors: O(h) where h is the height of the tree. In the worst case (skewed tree), h = n - 1.
    • DFS to unlock descendants: O(m) where m is the number of nodes in the subtree rooted at num. In the worst case, this could be O(n) if most nodes are descendants.
    • Overall: O(h + m) = O(n) in the worst case.

Space Complexity:

  • __init__(parent): O(n) for storing:

    • self.locked: O(n) array
    • self.parent: O(n) array
    • self.children: O(n) for the adjacency list (total edges = n - 1)
  • lock(num, user): O(1). No additional space used.

  • unlock(num, user): O(1). No additional space used.

  • upgrade(num, user): O(h) where h is the height of the tree, due to the recursive call stack in the DFS function. In the worst case (skewed tree), this could be O(n).

Overall Space Complexity of the class: O(n) for the data structures maintained throughout the object's lifetime.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Incorrect Ancestor Check in Upgrade Operation

A critical pitfall is checking ancestors incorrectly during the upgrade operation. Many implementations mistakenly start checking from the parent of the target node instead of the node itself.

Incorrect Implementation:

def upgrade(self, num: int, user: int) -> bool:
    # WRONG: Starting from parent, missing the check for num itself
    current_node = self.parent[num]  # Skips checking if num is locked!
    while current_node != -1:
        if self.locked[current_node] != -1:
            return False
        current_node = self.parent[current_node]

Why This Fails: According to the problem requirements, the upgrade operation requires that "the node num itself is unlocked." If we start checking from the parent, we miss verifying whether the target node is already locked, which would violate the upgrade conditions.

Correct Implementation:

def upgrade(self, num: int, user: int) -> bool:
    # CORRECT: Start from the node itself
    current_node = num  # Include num in the ancestor check
    while current_node != -1:
        if self.locked[current_node] != -1:
            return False
        current_node = self.parent[current_node]

2. Forgetting to Use nonlocal in Nested DFS Function

When using a nested function to track whether any descendants are locked, forgetting the nonlocal keyword causes the variable update to be local to the nested function only.

Incorrect Implementation:

def upgrade(self, num: int, user: int) -> bool:
    has_locked_descendant = False
  
    def unlock_descendants(node: int) -> None:
        # WRONG: Missing nonlocal, creates a new local variable
        for child in self.children[node]:
            if self.locked[child] != -1:
                self.locked[child] = -1
                has_locked_descendant = True  # This creates a LOCAL variable!
            unlock_descendants(child)
  
    unlock_descendants(num)
    # has_locked_descendant is still False here, even if descendants were locked
    if not has_locked_descendant:
        return False

Correct Implementation:

def upgrade(self, num: int, user: int) -> bool:
    has_locked_descendant = False
  
    def unlock_descendants(node: int) -> None:
        nonlocal has_locked_descendant  # CORRECT: Modifies outer scope variable
        for child in self.children[node]:
            if self.locked[child] != -1:
                self.locked[child] = -1
                has_locked_descendant = True
            unlock_descendants(child)

3. Inefficient Children List Construction

Building the children adjacency list incorrectly can lead to index errors or missing relationships.

Incorrect Implementation:

def __init__(self, parent: List[int]):
    self.children = [[] for _ in range(len(parent))]
    # WRONG: Starting from 0 includes root, which has parent -1
    for child_node, parent_node in enumerate(parent):
        if parent_node != -1:  # Extra check needed
            self.children[parent_node].append(child_node)

Better Implementation:

def __init__(self, parent: List[int]):
    self.children = [[] for _ in range(len(parent))]
    # CORRECT: Start from index 1, skip root automatically
    for child_node, parent_node in enumerate(parent[1:], 1):
        self.children[parent_node].append(child_node)

The second approach is cleaner as it naturally skips the root node (which has no parent) and avoids the need for conditional checks.

4. Not Handling Edge Cases Properly

Failing to consider edge cases like single-node trees or operations on the root node.

Example Edge Case:

  • Tree with only root node (n=1): The upgrade operation should always fail since root has no descendants
  • Attempting to upgrade the root when it's the only locked node in the tree

Solution: The implementation should naturally handle these cases through the DFS traversal, which will find no descendants for a leaf node, causing the upgrade to correctly return False.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What does the following code do?

1def f(arr1, arr2):
2  i, j = 0, 0
3  new_arr = []
4  while i < len(arr1) and j < len(arr2):
5      if arr1[i] < arr2[j]:
6          new_arr.append(arr1[i])
7          i += 1
8      else:
9          new_arr.append(arr2[j])
10          j += 1
11  new_arr.extend(arr1[i:])
12  new_arr.extend(arr2[j:])
13  return new_arr
14
1public static List<Integer> f(int[] arr1, int[] arr2) {
2  int i = 0, j = 0;
3  List<Integer> newArr = new ArrayList<>();
4
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.add(arr1[i]);
8          i++;
9      } else {
10          newArr.add(arr2[j]);
11          j++;
12      }
13  }
14
15  while (i < arr1.length) {
16      newArr.add(arr1[i]);
17      i++;
18  }
19
20  while (j < arr2.length) {
21      newArr.add(arr2[j]);
22      j++;
23  }
24
25  return newArr;
26}
27
1function f(arr1, arr2) {
2  let i = 0, j = 0;
3  let newArr = [];
4  
5  while (i < arr1.length && j < arr2.length) {
6      if (arr1[i] < arr2[j]) {
7          newArr.push(arr1[i]);
8          i++;
9      } else {
10          newArr.push(arr2[j]);
11          j++;
12      }
13  }
14  
15  while (i < arr1.length) {
16      newArr.push(arr1[i]);
17      i++;
18  }
19  
20  while (j < arr2.length) {
21      newArr.push(arr2[j]);
22      j++;
23  }
24  
25  return newArr;
26}
27

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More