tree_locking_interview_guide.md

🔐 Tree Locking Problem — Complete Interview Guide (Tailored to YOUR Solution)

This guide is written specifically for YOUR solution approach:

  • unordered_map<string, int> wordToIndex for name → index mapping
  • vector<vector<int>> adj for adjacency list (parent → children)
  • vector<int> parent where parent[i] gives parent of node i (-1 for root)
  • vector<int> locked where locked[i] stores uid (-1 = unlocked)
  • Recursive buildTree() to build the M-ary tree in BFS level-order
  • Separate helper functions: checkAncestorLocked, checkDescendantsLocked, sameDescendantLocked, unlockDescendants

Every answer below is written from YOUR perspective. Practice saying these out loud.


📋 Table of Contents

  1. The Problem Statement — Say It in Your Own Words
  2. The Two Implementation Approaches — Always Mention Both
  3. PHASE 1: Explaining Your Data Structures
  4. PHASE 2: Explaining Each Operation
  5. PHASE 3: Deriving the Time and Space Complexity
  6. PHASE 4: Optimization Questions
  7. PHASE 5: Edge Cases
  8. PHASE 6: Tracing Through Your Code
  9. PHASE 7: Thread Safety (If Asked)
  10. Every Possible Question — With Full Answers
  11. Quick-Reference: Things to Say Out Loud
  12. Common Mistakes to Avoid
  13. Appendix: Complexity Summary Card


1. The Problem Statement — Say It in Your Own Words

Before diving into code, always restate the problem confidently. This proves you understood it correctly.

You: "The problem gives us an M-ary tree with N nodes. Every node has a name (string). We need to support three operations:

Lock(x, uid): Lock node x for user uid. This succeeds ONLY if:

  1. Node x is currently not locked by anyone.
  2. No ancestor of x is locked by any user (regardless of uid).
  3. No descendant of x is locked by any user (regardless of uid).

Unlock(x, uid): Unlock node x. This succeeds ONLY if node x is currently locked by the same uid. No one else can unlock someone else's lock.

UpgradeLock(x, uid): This is an atomic 'consolidation' operation. It succeeds ONLY if:

  1. Node x is NOT currently locked (or at least not locked by this uid).
  2. At least one descendant of x is locked.
  3. ALL locked descendants are locked by the SAME uid (the same uid that is calling upgrade).

If all conditions are met, we unlock ALL locked descendants of x, and then lock x itself.

The intuition for UpgradeLock: instead of having 10 locks on child nodes all belonging to the same user, you 'collapse' all of them into one single lock on the parent node. It is a way to consolidate fragmented locks upward in the tree."



2. The Two Implementation Approaches — Always Mention Both

Interviewers love when you proactively compare your approach to alternatives. This shows engineering maturity.

You: "There were two main ways to implement this:

Approach 1: Node-Struct / Class with Pointers

You create a TreeNode struct:

struct TreeNode { string name; int lockedBy; // -1 if unlocked TreeNode* parent; vector<TreeNode*> children; };

Each node stores its name, lock state, a pointer to its parent, and pointers to its children.

Trade-offs of Approach 1:

  • Pro: Object-oriented, intuitive to reason about. Each node encapsulates its own state.
  • Pro: Slightly easier to understand for someone reading the code.
  • Con: Pointer chasing for traversals — not cache-friendly. Each dereference potentially causes a cache miss.
  • Con: Dynamic memory allocation (new TreeNode()) per node — heap fragmentation, slower.

Approach 2: Flat Vectors with Integer Indices (What I Did)

Instead of struct pointers, I use index-based arrays:

unordered_map<string, int> wordToIndex; // name -> index vector<vector<int>> adj; // adj[i] = list of children indices vector<int> parent; // parent[i] = parent index vector<int> locked; // locked[i] = uid or -1

Trade-offs of Approach 2:

  • Pro: O(1) direct array access — locked[i] is just a pointer dereference with no chain.
  • Pro: Contiguous memory — highly cache-friendly for traversals.
  • Pro: No heap allocation per node — vectors are pre-allocated in one block.
  • Pro: Simpler and faster to build — just arithmetic on indices.
  • Con: Less 'readable' at first glance for someone not familiar with this style.

Why I chose Approach 2: Given the BFS level-order input (nodes are given parent-first), the index arithmetic maps naturally. Also, for competitive/systems settings, the flat vector approach is faster in practice. The ancestor walk is just parent[i] — a single array lookup — and the adjacency list traversal is a contiguous memory scan."



3. PHASE 1: Explaining Your Data Structures

Question: "Walk me through your data structures."

You: "I have four main data structures:

1. unordered_map<string, int> wordToIndex This is my name-to-index lookup table. Every node name is mapped to an integer index (0 to N-1). This lets me go from a string like 'World' to an integer like 0 in O(1) amortized time. All subsequent logic works purely with integer indices — no string comparisons needed after the initial lookup.

2. vector<vector<int>> adj This is my adjacency list. adj[i] stores the indices of all children of node i. For example, if World has children Asia and Africa at indices 1 and 2, then adj[0] = {1, 2}. I use this for downward traversals — when I need to check or visit descendants.

3. vector<int> parent This stores the parent of each node. parent[i] gives me the index of node i's parent. The root has parent[0] = -1 (sentinel value meaning 'no parent'). This enables O(1) upward movement — to go from a node to its parent is a single array read.

4. vector<int> locked This stores the lock state of each node. locked[i] is:

  • -1 if node i is not locked by anyone
  • uid (the user ID) if node i is locked by that user

This design choice — storing the UID directly instead of a boolean — lets me check BOTH 'is this node locked?' and 'who locked it?' in a single array read."


Question: "How do you build the tree?"

You: "I build the tree using the BFS level-order property of the input. Since nodes are given parent-first (root first, then level 1, then level 2...), I know that for a node at index i in an M-ary tree, its children are at indices iM + 1 through iM + M (subject to bounds).

My buildTree function is recursive:

void buildTree(vector<string>& nodes, int i, int m) { if (i >= nodes.size() / 2) return; // leaf node -- no children for (int j = i*m + 1; j <= min((int)(nodes.size()-1), i*m + m); j++) { adj[i].push_back(j); parent[j] = i; } buildTree(nodes, i + 1, m); }

I start from index 0 (root). For each node i, I assign up to m children starting at index i*m + 1. I also record the parent relationship: parent[j] = i. Then I recurse to process the next node.

The stopping condition i >= nodes.size() / 2 ensures I don't try to assign children to leaf nodes — in a full M-ary tree, nodes at index >= N/2 cannot have children within the N-node limit."



4. PHASE 2: Explaining Each Operation

Lock Operation

You: "Lock has three guard checks before actually locking:

bool lock(string x, int uid) { int idx = wordToIndex[x]; if (locked[idx] != -1) return false; // Self check if (checkAncestorLocked(idx, uid)) return false; // Ancestor check if (checkDescendantsLocked(idx, uid)) return false; // Descendant check locked[idx] = uid; return true; }

Check 1 — Self: Is the node itself already locked? locked[idx] != -1. If yes, fail immediately. O(1).

Check 2 — Ancestors: Walk up the tree using the parent array. At each ancestor, check if locked[ancestor] != -1. If ANY ancestor is locked (by ANY user, not just a different user), fail. This walk costs O(height) = O(log_M N).

Check 3 — Descendants: BFS down the tree using the adj adjacency list. At each descendant, check if locked[desc] != -1. If ANY descendant is locked (by ANY user), fail. This BFS costs O(N) worst case.

If all three pass: set locked[idx] = uid and return true."


Unlock Operation

You: "Unlock is the simplest operation:

bool unlock(string x, int uid) { int idx = wordToIndex[x]; if (locked[idx] == uid) { locked[idx] = -1; return true; } return false; }

I check if locked[idx] == uid. This single check handles three cases at once:

  • If node is not locked (locked[idx] == -1), then -1 != uid, return false.
  • If node is locked by a DIFFERENT user (locked[idx] == someone_else), still != uid, return false.
  • If node is locked by THIS user, then == uid, set to -1, return true.

This is why storing the uid in locked[] instead of a boolean is elegant — one comparison covers both the 'is locked?' and 'is locked by me?' checks. Unlock is O(1)."


UpgradeLock Operation

You: "UpgradeLock is the most complex. Let me walk through it step by step:

bool upgradeLock(string x, int uid) { int idx = wordToIndex[x]; if (locked[idx] == uid) return false; // Can't upgrade if already locked by me if (!sameDescendantLocked(idx, uid)) return false; // All locked descs must be same uid unlockDescendants(idx); // Unlock all descendants return lock(x, uid); // Lock the node itself }

Step 1: The first check guards against the case where the node is already locked by this same uid. If you already own the lock, there's nothing to upgrade — return false.

Step 2: sameDescendantLocked is a BFS through all descendants. It tracks two counters:

bool sameDescendantLocked(int x, int uid) { int count = 0; // descendants locked by same uid int count2 = 0; // descendants locked by different uid // BFS through adj... if (node != x && locked[node] == uid) count++; if (node != x && locked[node] != uid && locked[node] != -1) count2++; return count2 == 0 && count != 0; }

count > 0: at least one descendant is locked by this uid. count2 == 0: zero descendants are locked by a DIFFERENT uid. Both must be true for upgrade to proceed.

Step 3: unlockDescendants does another BFS and sets locked[desc] = -1 for every descendant (including unlocked ones — it's safe, just redundant for unlocked nodes).

Step 4: lock(x, uid) is called normally. After unlockDescendants, all descendants are clear, so the lock() call will:

  • Pass the self check (unless the node was locked by someone else — then lock() fails).
  • Pass the descendant check (all clear).
  • Still need to pass the ancestor check."

Important Subtle Point About UpgradeLock

You: "There is one subtle thing to notice: my upgradeLock check at the start is locked[idx] == uid — it only blocks 'already locked by ME'. If the node is locked by SOMEONE ELSE, that check passes (because locked[idx] != uid is false). But then the final lock() call fails at locked[idx] != -1. So the logic is still correct — just the guard is narrower than it might seem at first glance. A cleaner version would be if (locked[idx] != -1) return false at the top of upgradeLock."



5. PHASE 3: Deriving the Time and Space Complexity

This is the most important thing to practice verbally. Know exactly how to derive it without looking at the code.

Lock: O(N) time

You: "Lock has three components:

Ancestor check: I walk from the node up to the root using the parent array. In an M-ary tree with N nodes and branching factor M, the height is log_M(N). This is because:

  • Level 0: 1 node
  • Level 1: M nodes
  • Level 2: M^2 nodes
  • Level h: M^h nodes
  • Total N nodes requires h = log_M(N) levels. So ancestor check is O(log_M N).

Descendant check: I do a BFS starting from the node down through all its descendants. In the worst case — if I'm locking the root — I visit ALL N nodes. So this is O(N) worst case.

The set operation: Once all checks pass, locked[idx] = uid is O(1).

Total for Lock: O(N) — the descendant BFS dominates."


Unlock: O(1) time

You: "Unlock is just:

  1. Map name to index: O(1) amortized
  2. Check locked[idx] == uid: O(1)
  3. Set locked[idx] = -1: O(1)

Total for Unlock: O(1). No ancestor or descendant traversal needed."


UpgradeLock: O(N) time

You: "Upgrade has multiple components:

sameDescendantLocked: BFS through all descendants — O(N) worst case.

unlockDescendants: BFS through all descendants and set each to -1 — O(N) worst case.

Final lock call: O(N) — it does ancestor check + descendant check again. But after unlockDescendants, descendants are clear, so the descendant BFS in lock() runs but finds nothing locked.

Total for UpgradeLock: O(N). Note: we're doing two full BFS passes, so the constant is roughly 2x, but asymptotically still O(N)."


Space Complexity: O(N)

You: "Space complexity is O(N):

  • wordToIndex: O(N) entries
  • adj: O(N) total edges (each node has at most M children, total edges = N-1 in a tree)
  • parent: O(N)
  • locked: O(N)

Total space: O(N)."


Summary Table

You: "So to summarize the complexities:

OperationTimeSpace
LockO(N)O(1) additional
UnlockO(1)O(1) additional
UpgradeLockO(N)O(1) additional
Build TreeO(N)O(N)
Overall for Q queriesO(Q x N)O(N)

Where the O(N) in lock and upgrade comes from the descendant traversal (BFS through the subtree)."



6. PHASE 4: Optimization Questions

Question: "Can you improve these complexities?"

You: "Yes. The bottleneck is the O(N) descendant check in lock(). I have to BFS through the entire subtree every time just to answer 'is any descendant locked?'.

The Optimization: Maintain a lockedDescCount counter per node.

I add an extra array: vector<int> lockedDescCount where lockedDescCount[i] counts how many locked descendants node i has.

On lock(x, uid): After successfully locking node x, walk UP from x to root, incrementing lockedDescCount[ancestor]++ for each ancestor.

On unlock(x, uid): After unlocking node x, walk UP from x to root, decrementing lockedDescCount[ancestor]-- for each ancestor.

Checking descendants: Instead of BFS, just check lockedDescCount[node] > 0. This is O(1)!

With this optimization:

  • Lock: O(log_M N) — ancestor check O(log_M N) + descendant check O(1) + propagate up O(log_M N) = O(log_M N)
  • Unlock: O(log_M N) — need to propagate up to update ancestor counts
  • Upgrade: O(K x log_M N) — unlock K descendants, each unlock needs O(log_M N) propagation

Alternative: vector<unordered_set<int>> lockedDescendants

Instead of a count, store the actual set of locked descendant indices at each ancestor:

  • O(1) check if any descendant is locked: lockedDescendants[node].empty()
  • O(K) iteration to verify all locked descendants have the same uid — useful for upgrade
  • Downside: higher memory usage

I would choose the unordered_set approach for upgrade-heavy workloads (easier to verify same-uid condition) and the integer count for lock/unlock-heavy workloads."


Question: "Why is Lock O(log_M N) and not O(N) after your optimization?"

You: "After optimization, lock does four things:

  1. Check if node itself is locked — O(1) array read
  2. Check if any descendant is locked — O(1) (just check lockedDescCount[node] > 0)
  3. Walk up ancestors checking if any ancestor is locked — O(height) = O(log_M N)
  4. If all pass, walk up again to increment ancestor counts — O(height) = O(log_M N)

The height of a balanced M-ary tree with N nodes is log_M(N). Since no step requires visiting more than O(height) nodes, lock is O(log_M N).

The O(N) came from the BFS subtree scan which we eliminated by precomputing and maintaining the count."


Question: "What about set vs unordered_set?"

You: "set<int> is a red-black tree — O(log K) for insert/erase/find. unordered_set<int> is a hash table — O(1) amortized for insert/erase/find.

The asymptotic Big-O of the full operation doesn't change by switching — the dominant term is still O(log_M N) for the ancestor walk. But unordered_set has a better constant factor — hash table operations are faster on average than tree operations. So in practice the code runs faster with unordered_set, even though the Big-O notation looks identical."



7. PHASE 5: Edge Cases

Question: "What are the edge cases in your solution?"

You: "Let me walk through the important edge cases:

1. Locking an already-locked node — even by the same uid: My check is locked[idx] != -1. So even if uid=5 tries to lock a node that uid=5 already locked, it returns false. You can't double-lock a node.

2. Unlocking a node you don't own: locked[idx] == uid is the check. If uid=7 tries to unlock a node locked by uid=5, locked[idx] is 5, 5 != 7, returns false.

3. Unlocking an already-unlocked node: locked[idx] == -1. Then -1 != uid (valid uids are positive integers), returns false.

4. UpgradeLock on a leaf node: A leaf has no children — adj[leaf] is empty. The BFS in sameDescendantLocked visits only the node itself (which is skipped with node != x), so count = 0. The condition count != 0 fails. Returns false. Correct.

5. UpgradeLock when descendants are locked by MIXED uids: Example: China locked by uid=1, India locked by uid=2. upgradeLock(Asia, uid=1) triggers sameDescendantLocked which finds count2 > 0 (India locked by uid != 1). Returns false. Correct.

6. Locking with ancestor locked by the SAME uid: The ancestor check is locked[x] != -1 — it doesn't check which uid locked the ancestor. Even if YOUR OWN uid locked an ancestor, you CANNOT lock a descendant. This is correct per the problem statement.

7. Sibling nodes — locking one does not block the other: China and India are siblings under Asia. Locking China checks Asia as an ancestor (checks if locked, not India). It does NOT visit India at all. So both China and India can be independently locked by different users simultaneously.

8. Single node tree (just the root): Root has no parent (parent[0] = -1) and no children (adj[0] = {}). Lock works (no ancestors or descendants to check). Unlock works normally. Upgrade always fails (no descendants → count = 0).

9. After upgrade, descendants are unlocked: If Asia is upgraded (which unlocks China and India), then calling unlock(China, uid) returns false because locked[China] = -1, which != uid."



8. PHASE 6: Tracing Through Your Code

"Trace lock('Asia', 3) on: World(0) -> Asia(1) -> {China(2), India(3)}"

You: "Let me trace step by step.

  1. Look up 'Asia' in wordToIndex → index 1.

  2. Check locked[1] != -1. locked[1] = -1, so PASS.

  3. checkAncestorLocked(1, 3):

    • x = parent[1] = 0 (World)
    • Check locked[0] != -1 → -1, not locked. Continue.
    • x = parent[0] = -1 → exit loop.
    • Return false (no ancestor is locked). PASS.
  4. checkDescendantsLocked(1, 3) — BFS starting from node 1:

    • Queue: {1}
    • Pop 1. locked[1] = -1, OK. Push adj[1] = {2, 3}. Queue: {2, 3}
    • Pop 2 (China). locked[2] = -1, OK. adj[2] is empty. Queue: {3}
    • Pop 3 (India). locked[3] = -1, OK. adj[3] is empty. Queue: {}
    • Loop ends. Return false (no descendant locked). PASS.
  5. All checks passed. Set locked[1] = 3. Return true.

Asia is now locked by uid=3."


"Trace upgradeLock('Asia', 3) when China(2) locked by uid=3, India(3) locked by uid=3"

You: "Step by step:

  1. Look up 'Asia' → index 1.

  2. locked[1] == 3? locked[1] = -1, so -1 != 3. Passes first check.

  3. sameDescendantLocked(1, 3) — BFS:

    • Pop 1 (skipped: node == x)
    • Push adj[1] = {2, 3}
    • Pop 2 (China). node != x. locked[2] = 3 = uid → count++ → count=1.
    • Push adj[2] = {} (leaf)
    • Pop 3 (India). node != x. locked[3] = 3 = uid → count++ → count=2.
    • count2 = 0 (no differently-locked descendants)
    • Return: count2==0 && count!=0 → true. PASS.
  4. unlockDescendants(1) — BFS from node 1:

    • locked[1] = -1 (already -1, no change)
    • locked[2] = -1 (China now unlocked)
    • locked[3] = -1 (India now unlocked)
  5. lock('Asia', 3):

    • locked[1] != -1 → -1 != -1 → false, PASS.
    • checkAncestorLocked(1): parent=World, locked[0]=-1. PASS.
    • checkDescendantsLocked(1): China and India are both -1 now. PASS.
    • locked[1] = 3. Return true.
  6. upgradeLock returns true.

Before: China(uid=3) + India(uid=3) locked. After: Asia(uid=3) locked, China and India free."


"What if upgradeLock('Asia', 3) is called when India is locked by uid=7?"

You: "In step 3, sameDescendantLocked(1, 3):

  • China: locked[2] = 3 = uid → count++ → count=1
  • India: locked[3] = 7. 7 != uid AND 7 != -1 → count2++ → count2=1
  • Return: count2==0 is false (count2=1), so return false.

upgradeLock returns false. Correct — you cannot upgrade when a descendant belongs to a different user."



9. PHASE 7: Thread Safety (If Asked)

If the interviewer moves into thread safety, use this section.

"Are there race conditions in your lock() function?"

You: "Yes, absolutely. Before I list them, let me clarify: I'm assuming each individual read or write to a single variable — like locked[i] — is atomic as a single memory operation. But a compound check-then-set — 'read locked[i], compare to -1, write locked[i] = uid' — is NOT atomic as a whole. A context switch can happen between the read and the write. Is that the model we're working with?"

(Interviewer confirms yes.)

"Then there are three key race conditions:

Race 1 — Same Node TOCTOU: Thread T1: lock(Asia, uid=1) Thread T2: lock(Asia, uid=2)

T1: reads locked[1] = -1 → PASS [context switch] T2: reads locked[1] = -1 → PASS T1: writes locked[1] = 1, returns true T2: writes locked[1] = 2, returns true ← OVERWRITES T1!

Both return true. BUG. This is classic TOCTOU (Time-Of-Check to Time-Of-Use).

Race 2 — Ancestor-Descendant Conflict: T1: lock(World, uid=1), T2: lock(Asia, uid=2) — Asia is child of World

T1: checks descendants of World, Asia is unlocked, PASS [context switch] T2: checks ancestors of Asia, World is unlocked, PASS T2: writes locked[Asia] = 2, returns true [context switch] T1: writes locked[World] = 1, returns true

Both return true! World and descendant Asia are both locked. INVARIANT VIOLATED.

Race 3 — Sibling Concurrent Counter Update (with optimization): T1: lock(China), T2: lock(India) — both need to update lockedDescCount[Asia]

T1: reads lockedDescCount[Asia] = 0 T2: reads lockedDescCount[Asia] = 0 T1: writes lockedDescCount[Asia] = 1 T2: writes lockedDescCount[Asia] = 1 ← LOST UPDATE! Should be 2.

Descendant tracking is now wrong — future lock(Asia) would see lockedDescCount=1 but should be 2."


"How do you solve these race conditions?"

You: "I'll start simple and progressively add sophistication:

Level 1: Global Mutex Wrap entire lock() in a global mutex. Simple but kills all parallelism.

Level 2: Per-Node Mutex, leaf-to-root order Each node has its own mutex. When locking node x, acquire mutexes for x and all ancestors from leaf to root. Two threads in independent subtrees don't block each other. Leaf-to-root ordering prevents deadlock by breaking circular wait.

Level 3: CAS with std::atomic

atomic<int> locked[i]; int expected = -1; locked[node].compare_exchange_strong(expected, uid); // atomic!

This is a single hardware instruction (LOCK CMPXCHG). No context switch can happen inside it.

Level 4: Custom Spinlock (if std::mutex is banned) std::mutex uses OS primitives — when there's contention, the OS sleeps the thread (~3,000ns). For our 20-40ns tree operations, that overhead is 100x the work itself.

A spinlock uses __sync_lock_test_and_set (a hardware TAS instruction):

volatile int flag = 0; while (__sync_lock_test_and_set(&flag, 1) == 1) {} // spin // critical section __sync_lock_release(&flag);

Level 5: Pure Software (if even hardware atomics are banned) Peterson's Algorithm (2 threads) or Lamport's Bakery Algorithm (N threads) — guaranteed mutual exclusion using only ordinary single-word memory reads and writes."



10. Every Possible Question — With Full Answers

#QuestionWhere Answered
1"Restate the problem in your own words"Section 1
2"What were the two approaches? Why did you choose yours?"Section 2
3"Walk me through your data structures"Section 3
4"How do you build the tree?"Section 3
5"Explain the lock operation"Section 4 (Lock)
6"Explain the unlock operation"Section 4 (Unlock)
7"Explain the upgradeLock operation"Section 4 (UpgradeLock)
8"What is the time complexity of each operation?"Section 5
9"Derive why ancestor check is O(log_M N)"Section 5 (Lock)
10"Derive why descendant check is O(N)"Section 5 (Lock)
11"What is the space complexity?"Section 5 (Space)
12"Can you optimize these complexities?"Section 6
13"How does the lockedDescCount optimization work?"Section 6
14"Why is optimized lock O(log_M N) and not O(N)?"Section 6
15"What are the edge cases?"Section 7
16"Trace lock(X, uid) step by step"Section 8
17"Trace upgradeLock(X, uid) step by step"Section 8
18"What if lock is called twice on the same node with the same uid?"Section 7, Edge Case 1
19"Can siblings block each other?"Section 7, Edge Case 7
20"Does an ancestor locked by same uid block its descendant?"Section 7, Edge Case 6
21"Are there race conditions in your lock()?"Section 9
22"Name all race conditions"Section 9
23"How do you solve race conditions?"Section 9 (Solutions)
24"What if you can't use mutex?"Section 9 (Spinlock)
25"What if you can't use atomic variables either?"Section 9 (Peterson/Bakery)
26"Why do you store uid instead of a boolean in locked[]?"Section 3 (Data Structures)
27"What happens in upgradeLock if node is locked by someone else?"Section 4 (Subtle Point)
28"Why copy the set before iterating in the optimized upgrade?"Section 6


11. Quick-Reference: Things to Say Out Loud

Opening (Before Any Code)

"I approached this problem using flat integer-indexed arrays for maximum cache efficiency. I have four structures: wordToIndex for O(1) name lookup, adj for child traversal, parent for O(1) upward movement, and locked to store which uid holds each node's lock. There were two approaches — a struct-based approach with pointers, and my index-based approach. I chose index-based for cache friendliness and simplicity."

When Explaining Lock

"Lock has three guard checks: self-check O(1), ancestor check O(log_M N) by walking up the parent array, and descendant check O(N) by BFS through the adjacency list. All three must pass. The descendant check is the bottleneck at O(N)."

When Explaining Unlock

"Unlock is O(1). I just do a single comparison: locked[idx] == uid. This one check handles three cases — node not locked, node locked by someone else, and node locked by this uid. I designed locked[] to store uid directly instead of a boolean precisely so one comparison covers everything."

When Explaining Upgrade

"Upgrade is a consolidation operation. It uses sameDescendantLocked which BFS-scans all descendants and tracks two counters: how many are locked by this uid, and how many by a different uid. Both conditions must be satisfied: at least one locked by same uid AND zero locked by different uid. Then it calls unlockDescendants to clear all descendant locks, then calls lock() on the current node."

When Asked to Derive Complexity

"Lock is O(N). The ancestor walk is O(log_M N) because an M-ary tree of N nodes has height log_M(N). But the descendant BFS is O(N) in the worst case — locking the root means visiting every node. So BFS dominates. Unlock is O(1). Upgrade is O(N) — two full BFS passes."

When Asked to Optimize

"I can add a lockedDescCount[i] counter that I increment/decrement upward on every lock and unlock. Then checking descendants becomes O(1) — just check lockedDescCount[node] > 0. This reduces lock from O(N) to O(log_M N), but unlock now costs O(log_M N) to propagate up."



12. Common Mistakes to Avoid

MistakeWhat To Do Instead
Not mentioning both approachesAlways start with "there were two approaches" — struct-based vs index-based
Saying unlock is O(N) or O(log N)Unlock is O(1) — no traversal needed!
Confusing upgrade's self-check scopeThe check locked[idx] == uid only catches "locked by me". "Locked by someone else" is caught by the final lock() call.
Not explaining WHY uid is stored instead of a booleanSay: "Storing uid means one comparison locked[i] == uid covers both 'is it locked?' and 'is it locked by me?'"
Saying lockedDescCount++ is atomic in multithreaded contextIt's a 3-step read-modify-write. Never claim it's atomic without hardware support.
Forgetting that ancestor check does NOT filter by uidcheckAncestorLocked returns true if ANY ancestor is locked by ANYONE, even the same uid.
Getting confused about sameDescendantLocked skipping node xThe condition node != x skips the starting node — we only check actual descendants, not the node itself.
Not explaining the BFS queue patternMention: "I use a queue, push the start node, then pop each node, check it, and push its children from adj."
Not preparing the upgrade tracePractice the trace: sameDescendantLocked → unlockDescendants → lock, as three clear phases.
Accepting "this is thread-safe" without checkingALWAYS trace through at least two interleavings. Never just agree.
Forgetting to mention the set-copy trapIn optimized upgrade with lockedDescendants set: copy the set first before iterating, because each unlock modifies the set.


13. Appendix: Complexity Summary Card

Your Original Solution

OperationTimeWhy
LockO(N)Descendant BFS = O(N) dominates ancestor walk O(log_M N)
UnlockO(1)Just a comparison and an assignment
UpgradeLockO(N)Two full BFS passes (sameDescendantLocked + unlockDescendants)
Build TreeO(N)One recursive pass assigning children and parents
Space (total)O(N)Four O(N) structures: wordToIndex, adj, parent, locked

After Optimization (lockedDescCount)

OperationTimeWhy
LockO(log_M N)Descendant check O(1) + ancestor walk + propagate up = O(log_M N)
UnlockO(log_M N)Must propagate decrement up to all ancestors
UpgradeLockO(K x log_M N)K = locked descendants; each unlock costs O(log_M N) propagation

Tree Height Formula

M-ary tree with N nodes has height = floor(log_M(N))

  • M=2 (binary): height = log_2(N) ≈ 20 for N=1,000,000
  • M=10: height = log_10(N) ≈ 6 for N=1,000,000

Appendix B: Your Code — Quick Reference for the Interview

YOUR DATA STRUCTURES: +-- unordered_map<string, int> wordToIndex // O(1) name -> index +-- vector<vector<int>> adj // adj[i] = children of node i +-- vector<int> parent // parent[i] = parent index (-1 for root) +-- vector<int> locked // locked[i] = uid or -1 YOUR FUNCTIONS: +-- buildTree(nodes, i, m) // recursive BFS-order tree construction +-- checkAncestorLocked(x, uid) // walk up parent[] checking locked[] != -1 +-- checkDescendantsLocked(x, uid) // BFS through adj[] checking locked[] != -1 +-- unlockDescendants(x) // BFS through adj[], set all locked[] = -1 +-- sameDescendantLocked(x, uid) | +-- BFS through adj[] | +-- count: nodes where locked[node] == uid (skipping x itself) | +-- count2: nodes where locked[node] != uid && != -1 (skipping x) | +-- return count2 == 0 && count != 0 | +-- lock(name, uid) | +-- wordToIndex lookup -> idx | +-- locked[idx] != -1 -> false // self check O(1) | +-- checkAncestorLocked(idx, uid) // ancestor check O(log_M N) | +-- checkDescendantsLocked(idx, uid) // descendant check O(N) | +-- locked[idx] = uid | +-- unlock(name, uid) | +-- locked[idx] == uid -> locked[idx] = -1 // O(1) | +-- upgradeLock(name, uid) +-- locked[idx] == uid -> false // already locked by me guard +-- sameDescendantLocked(idx, uid) // verify all descs are same uid +-- unlockDescendants(idx) // clear all descendant locks +-- lock(name, uid) // lock the node itself

Tailored to YOUR vector/index-based solution for the Tree Locking problem. Practice saying each section out loud — confidence comes from verbalization, not just reading.