Coding questions · Amazon
Design a hierarchical locking tree
This interview question evaluates algorithm design, data structures, correctness, complexity, edge cases, and implementation details in a realistic interview setting. A strong answer for Design a hierarchical locking tree states assumptions, handles edge cases, explains trade-offs, and shows how to validate the result clearly.
Design a LockingHierarchy data structure over a rooted tree of N nodes labeled 0..N-1, initialized with an integer array parent[] where parent[0] = -1 and parent[i] is the parent of i. Support operations: (1) lock(x): lock node x only if x is currently unlocked AND all its ancestors and descendants are unlocked; return true if the lock succeeds, else false. (2) unlock(x): if x is locked, unlock it and return true; otherwise return false. (3) isLocked(x): return whether x is locked. Constraints: up to 2e5 nodes and 2e5 operations; each operation should be O(log N) or better (amortized). Specify and justify the data structures and algorithms you will use (e.g., parent array initialization, subtree indexing, ancestor/descendant conflict checks, and maintaining counts of locked descendants), the time and space complexity, and key edge cases. Provide a brief set of unit tests covering boundary conditions (root, leaf, repeated locks/unlocks, conflicting locks, deep trees).
What a Strong Answer Covers
- A clear algorithm with the right data structures and enough pseudocode or code-level detail to implement it.
- A correctness argument that explains why the algorithm covers all required cases.
- Time and space complexity, plus at least one alternative approach when relevant.
- Focused tests for normal cases, edge cases, and failure modes.
Follow-up Questions
- How would the approach change if the input were streaming or too large for memory?
- What invariants would you assert in production code?
- Which tests would catch off-by-one, duplicate, or tie-breaking bugs?
Design a Hierarchical Locking Tree
Implement a LockingHierarchy over a rooted tree of N nodes labeled 0..N-1. The tree is given by an integer array parent[] where parent[0] = -1 (the root) and parent[i] is the parent of node i. Support three operations:
- lock(x) — lock node
xonly ifxis currently unlocked and none of its ancestors are locked and none of its descendants are locked. Returntrueif the lock succeeds, elsefalse. - unlock(x) — if
xis locked, unlock it and returntrue; otherwise returnfalse. - isLocked(x) — return whether
xis currently locked.
To make this executable, your solution consumes the parent array plus a list of operations (name, x) and returns the list of boolean results, one per operation (for isLocked the result is the current lock state).
Approach. Keep a locked[] boolean per node. The descendant check is the trap: a naive DFS over the subtree on every lock is O(N) per call. Instead maintain locked_desc[], a count of how many nodes in each node's subtree are currently locked. On lock(x), walk up the ancestor chain once (a) to check no ancestor is locked and (b) to increment each ancestor's locked_desc; the descendant check is then just locked_desc[x] == 0. On unlock(x), walk up again to decrement. Each op is O(depth) — O(log N) on balanced trees, O(N) worst case on a degenerate chain; the ancestor walk is unavoidable without heavier machinery (e.g. Euler-tour + segment tree / BIT for O(log N) guaranteed).
Constraints
- 1 <= N <= 210^5 nodes; up to 210^5 operations.
- parent[0] == -1 (node 0 is the root); for i > 0, 0 <= parent[i] < i is not required but parent[i] must reference a valid node forming a rooted tree.
- 0 <= x < N for every operation.
- name is one of 'lock', 'unlock', 'isLocked'.
Examples
Example 1
Input: ([-1, 0, 0, 1, 1, 2], [('lock', 0), ('isLocked', 0), ('lock', 3), ('unlock', 0), ('lock', 3), ('isLocked', 3)])
Output: [True, True, False, True, True, True]
Explanation: Lock root 0 (ok). isLocked(0) is True. lock(3) fails because ancestor 0 is locked. unlock(0) succeeds. Now lock(3) succeeds (no locked ancestor/descendant). isLocked(3) is True.
Example 2
Input: ([-1, 0, 1, 2, 3], [('lock', 2), ('lock', 0), ('lock', 4), ('unlock', 2), ('lock', 0), ('lock', 4)])
Output: [True, False, False, True, True, False]
Explanation: Chain 0->1->2->3->4. lock(2) ok. lock(0) fails: descendant 2 is locked. lock(4) fails: ancestor 2 is locked. unlock(2) ok. lock(0) now ok. lock(4) fails: ancestor 0 is locked.
Hints
- The descendant check is the bottleneck. Instead of DFS-ing a node's subtree on every lock(), maintain locked_desc[v] = number of currently-locked nodes in v's subtree. Then 'no locked descendant' is simply locked_desc[x] == 0 in O(1).
- On lock(x), walk up the ancestor chain exactly once: it both verifies no ancestor is locked AND increments locked_desc on each ancestor. On unlock(x), walk up again to decrement.
- Edge cases: locking an already-locked node returns False; unlocking an unlocked node returns False; the root has no ancestors; a leaf has no descendants. Re-locking after an unlock must succeed once the conflicts clear.
Solution
Solution Alignment
The prompt asks for an implementation-level answer. The safest way to present it is to define the state, maintain clear invariants, then walk through complexity and tests.
Problem Restatement
Design a LockingHierarchy data structure over a rooted tree of N nodes labeled 0..N-1, initialized with an integer array parent[] where parent[0] = -1 and parent[i] is the parent of i. Support operations: ( 1) lock(x): lock node x only if x is currently unlocked AND all its ancestors and descendants are unlocked; return true if the lock succeeds, else false. ( 2) unlock(x): if x is locked, unlock it and return true; otherwise return false. ( 3) isLocked(x): return whether x is locked. Constraints: up to 2e5 nodes and 2e5 operations; each operation should be O(log N) or better (amortized). Specify and justify the data structures and algorithms you will use (e.g., parent array initialization, ...
Recommended Approach
Choose traversal based on the required output. DFS is natural for subtree computations, reconstruction, and range pruning; BFS is natural for level order or side views. Keep per-depth or per-position state when the output depends on columns, rows, or depths.
Correctness
The implementation should maintain an invariant after each loop or operation that directly matches the problem statement. At termination, that invariant implies the returned value has considered every valid candidate exactly once, or has preserved the required data-structure state after every API call.
Complexity
Most tree traversals are O(n) time and O(h) recursion stack for DFS or O(w) queue space for BFS, where h is height and w is maximum width.
Edge Cases and Tests
Empty tree, one node, skewed tree, duplicate values when reconstruction assumes uniqueness, deep recursion, and tie-breaking for same row/column nodes.