๐ฅ Hackathon Part B โ Complete Guide (Tailored to YOUR Solution)
This guide is written specifically for YOUR Part A approach:
unordered_map<string, int>for name โ index mappingvector<vector<int>> adjfor adjacency list (parent โ children)vector<int> parentwhereparent[i]gives parent of nodeivector<int> lockedwherelocked[i]storesuid(-1 = unlocked)- Separate helper functions for ancestor check, descendant check, etc.
Every answer below is written from YOUR perspective. Memorize these explanations.
๐ Table of Contents
- What to Expect โ The Full Day Flow
- PHASE 1: Explaining Your Part A Solution
- PHASE 2: Optimization Questions
- PHASE 3: The Thread-Safety Core Task
- PHASE 4: Identifying Race Conditions
- PHASE 5: Solutions โ Coarse to Fine-Grained
- PHASE 6: Building Your Own Mutex
- PHASE 7: The "No Primitives" Solution โ Software Algorithms & Set-and-Test
- PHASE 8: Finding Bugs in "Thread-Safe" Solutions
- PHASE 9: Thread Interleaving Traces
- PHASE 10: OS / CN / Fundamentals Questions
- PHASE 11: Resume & Project Deep-Dive
- Every Possible Question โ With Full Answers
- Quick-Reference: Things to Say Out Loud
- Common Mistakes to Avoid
- Mental Preparation & Endurance Tips
1. ๐ What to Expect โ The Full Day Flow
Format
- You are assigned a mentor on Slack, sometimes paired with a Google Meet call
- Duration is wildly inconsistent โ reports range from ~6 hours to 9amโ9pm to spanning two days
- One account describes a 17-hour stretch
- Budget for a full day. Eat well beforehand. Keep water nearby.
The Step-by-Step Flow
Step 1: Interviewer shares YOUR Part A submission on screen
"Walk me through your solution line by line"
|
Step 2: Deep complexity analysis
"What's the exact time/space complexity? Derive it."
|
Step 3: Optimization push
"Can you improve these complexities?"
|
Step 4: THE CORE TASK -> Thread-Safety
"Now imagine lock() runs on a multi-core machine with
multiple threads. Will there be a race condition?"
|
Step 5: Name the EXACT race conditions YOURSELF
(Don't wait for them to tell you -- say it first!)
|
Step 6: Solve it -- multiple levels of constraint
Level 1: Global Mutex / Synchronized -> "Kills parallelism"
Level 2: Per-Node Mutex -> "OS sleep / Futex overhead (~3,000ns)"
Level 3: Lock-Free with Atomic Variables (std::atomic) -> "Now take away atomics"
Level 4: Custom Spinlock / Ownership Mutex (__sync_* / CAS)
Level 5: Pure Software Algorithms (Peterson's / Lamport's Bakery / OCC Set-and-Test)
|
Step 7: Interviewer presents a "thread-safe" solution and claims it works
YOUR JOB: Push back and find the bugs!
|
Step 8: Trace specific thread interleavings on demand
"Thread A does X, context switch, Thread B does Y -- what happens?"
|
Step 9: OS / CN / Fundamentals questions (lighter, toward the end)
DNS, Idempotency, SSL/TLS, Process vs Thread, HashMap internals
|
Step 10: Resume / Project / Behavioral discussionImportant Clarification You Should Ask Early
Based on real experiences, ask this question early:
You: "Just to clarify -- in Part B, are we making only the
lock()method thread-safe, or all three methods (lock, unlock, upgrade)?"
In most cases the answer is: "We just care about lock method in Part B hackathon."
But be mentally prepared if they say all three.
2. ๐ค PHASE 1: Explaining Your Part A Solution
This is the FIRST thing that happens. The interviewer opens your submitted code. You need to walk through it confidently.
Question 1: "Explain the logic that you used to solve the Hackathon Part A question and its time and space complexity."
How You Should Answer (Full Script):
You: "I approached this problem by first converting every node name to an integer index using an
unordered_map<string, int>. This way, all subsequent operations work purely with integer indices, giving me O(1) lookup from name to index.Then I built three core data structures:
vector<vector<int>> adj-- An adjacency list whereadj[i]gives me all the children of nodei. I built this by processing the input nodes in BFS (level-order) fashion, assigning up to M children to each node.
vector<int> parent-- Whereparent[i]gives me the parent of nodei. The root hasparent[0] = -1. This gives me O(1) access to any node's parent.
vector<int> locked-- Wherelocked[i]stores theuidthat locked nodei. Initially all values are-1, meaning no node is locked. When a node is locked, I store the uid there.For the three operations:
Lock(node, uid):
- First, I check if the node itself is already locked:
if (locked[node] != -1) return false;- Then I check all ancestors by walking up using the parent array:
cur = parent[node]and keep going untilcur == -1(root's parent). If any ancestor haslocked[ancestor] != -1, return false.- Then I check all descendants by doing a BFS/DFS through
adj[node]. If any descendant haslocked[desc] != -1, return false.- If all checks pass, I set
locked[node] = uidand return true.Unlock(node, uid):
- Check if
locked[node] == uid. If not, return false.- If yes, set
locked[node] = -1and return true.UpgradeLock(node, uid):
- Check that the node itself is NOT locked:
locked[node] == -1- Check that at least one descendant IS locked
- Check that ALL locked descendants are locked by the same
uid- Check that no ancestor is locked
- If all pass: unlock all locked descendants (set their
locked[desc] = -1), then lock the node itself (locked[node] = uid)"
Time Complexity (Derive Verbally -- Practice This):
You: "For the time complexity:
Lock: O(log_m N) for the ancestor check -- because I walk from the node up to the root, and the height of a balanced M-ary tree with N nodes is log base M of N. But the descendant check is O(N) in the worst case because I'm doing a BFS/DFS through the subtree rooted at the node, and in the worst case that subtree could be the entire tree.
So overall, lock is O(N) because the descendant check dominates.
Unlock: O(1) -- I just check
locked[node] == uidand setlocked[node] = -1. There's no ancestor/descendant walk needed for unlock.Upgrade: O(N) -- because I need to traverse all descendants to check if they're all locked by the same uid, then unlock them. Plus the ancestor check which is O(log_m N), so overall O(N).
Space Complexity: O(N) -- the adjacency list, parent array, and locked array each take O(N) space."
Question 2: "Why did you use adjacency list with integer indices instead of a Node struct with pointers?"
How You Should Answer:
You: "I chose this approach because:
Simplicity and speed -- Integer indices on flat vectors give O(1) direct array access. There's no pointer chasing overhead. The memory is contiguous, which is cache-friendly.
Easy to build -- Given the BFS-order input, I can build the parent and adjacency list arrays with a simple loop and a queue. The index arithmetic maps naturally to the level-order input.
O(1) parent access --
parent[i]is a direct array lookup. No hash map needed for the parent relationship.Familiar and less error-prone -- I'm comfortable with index-based graph representations from competitive programming. It reduces the chance of bugs with pointer management.
The trade-off is that I don't have a
lockedDescendantsset in each node, so my descendant check is O(N) instead of O(1). If I had maintained alockedDescendantsset per node, I could achieve O(1) for that check. I can discuss how to optimize that."
Question 3: "Walk me through what happens when Lock(China, 9) is called."
How You Should Answer (Trace Through Your Code):
You: "Let me trace through my code step by step.
First, I look up 'China' in my
nameToIndexmap. Say China maps to index 4.Step 1: Check
locked[4]. It's -1, so China is not already locked. Pass.Step 2: Walk up ancestors.
parent[4]gives me, say, index 1 (Asia). Checklocked[1]-- it's -1, Asia is not locked. Pass. Thenparent[1]gives me index 0 (World). Checklocked[0]-- it's -1, World is not locked. Pass. Thenparent[0]is -1, meaning we've reached above the root. All ancestors are clear. Pass.Step 3: Check descendants. I do BFS starting from node 4 (China). I look at
adj[4]-- if China is a leaf,adj[4]is empty. No descendants to check. Pass.Step 4: All checks passed. Set
locked[4] = 9. Return true.Now China is locked by uid 9."
Question 4: "What are the edge cases in your solution?"
How You Should Answer:
You: "The key edge cases I handle are:
Locking an already-locked node -- Even if the same uid tries to lock it again, it returns false because
locked[node] != -1.Unlocking with wrong uid --
locked[node] != uid, so it returns false.Unlocking an already-unlocked node --
locked[node] == -1, which is not equal to any valid uid, so it returns false.Upgrade on a leaf node -- A leaf has no descendants, so the 'at least one locked descendant' check fails. Returns false.
Upgrade when the node itself is already locked -- First check catches this. Returns false.
Upgrade with mixed uids among descendants -- If China is locked by uid 1 and India by uid 2, upgrade on Asia fails for both uid 1 and uid 2.
Sibling nodes -- China and India are siblings (both children of Asia). Locking China does NOT prevent locking India, because neither is an ancestor/descendant of the other.
Single node tree -- The root has no parent and no children. Lock works (no ancestors or descendants to check). Upgrade always fails (no descendants).
After upgrade, descendants are unlocked -- If I upgrade Asia (which unlocks China and India), then trying to unlock China returns false because it's already unlocked."
Question 5: "What happens if you receive 10,000 operations in sequence?"
How You Should Answer:
You: "Each lock operation is at most O(N) because of the descendant check. Each unlock is O(1). Each upgrade is O(N). So 10,000 operations would be at most O(10,000 x N) in the worst case.
For reasonable values of N (say N = 10,000 nodes), that's 10^8 operations -- borderline but should run within the time limit on modern hardware.
If performance is critical, I could optimize the descendant check from O(N) to O(1) by maintaining a
lockedDescendantscount or set in each node -- but that would require updating ancestors on every lock/unlock, which is O(log_m N) per operation. The trade-off is: faster descendant checks but slightly more work on each lock/unlock. Net result would be O(log_m N) per lock/unlock, which is much better."
3. ๐ง PHASE 2: Optimization Questions
The interviewer will now push you to improve your solution.
Question 6: "Can you improve these complexities?"
How You Should Answer:
You: "Yes. The bottleneck in my current solution is the descendant check in
lock(), which is O(N) because I do a BFS/DFS through the entire subtree.Optimization 1: Maintain a locked descendants count/set per node.
Instead of searching the subtree every time, I can maintain additional information. Two options:
Option A:
vector<int> lockedDescCount-- WherelockedDescCount[i]counts how many locked descendants nodeihas. When I lock node X:
- Walk up from X to root, incrementing
lockedDescCount[ancestor]for each ancestor. When I unlock node X:- Walk up from X to root, decrementing
lockedDescCount[ancestor]for each ancestor. Then to check if any descendant is locked: just checklockedDescCount[node] > 0. This is O(1).Option B:
vector<unordered_set<int>> lockedDescendants-- WherelockedDescendants[i]is a set of indices of all locked descendants of node i. When I lock X, I walk up and insert X into each ancestor's set. When I unlock, I walk up and erase X.
- Checking descendants locked:
lockedDescendants[node].empty()-- O(1)- For upgrade, I can iterate the set to check all locked descendants are same uid -- O(K) where K = number of locked descendants
With either option:
- Lock: O(log_m N) -- ancestor check O(log_m N) + descendant check O(1) + propagate up O(log_m N)
- Unlock: O(log_m N) -- propagate up to update ancestor counts/sets
- Upgrade: O(K x log_m N) -- unlock K descendants, each requiring O(log_m N) propagation
Optimization 2: If I'm using
set, I can switch tounordered_setfor O(1) amortized insert/erase instead of O(log K) forset. The asymptotic complexity stays the same but the constant factor improves -- the code runs faster in practice."
Question 7: "So what will be the final complexity after this change? Same or different?"
How You Should Answer:
You: "The asymptotic Big-O complexity stays the same -- O(log_m N) for lock and unlock, O(K x log_m N) for upgrade. But
unordered_sethas O(1) average-case insert/erase compared toset's O(log K), so the actual runtime is faster in practice. The time complexity notation doesn't change because the dominant term is the ancestor walk (O(log_m N)), not the set operations."
Question 8: "Why is Lock O(log_m N) and not O(N)?"
How You Should Answer:
You: "With the optimization, the lock operation does three things:
- Check if the node itself is locked -- O(1)
- Check if any descendant is locked -- O(1) (just check
lockedDescCount[node] > 0)- Walk up from the node to the root checking ancestors -- O(height)
- If all checks pass, walk up again to propagate (update ancestor counts/sets) -- O(height)
The height of a balanced M-ary tree with N nodes is log_M(N). This is because at each level, the number of nodes multiplies by M. Level 0 has 1 node, level 1 has M nodes, level 2 has M^2 nodes... So to accommodate N total nodes, we need log_M(N) levels.
Since the only operation that scales with input size is the ancestor walk, and the ancestor walk is O(height) = O(log_m N), the overall lock operation is O(log_m N).
Without the optimization, I have to BFS the entire subtree for descendant checking, which is O(subtree size) = O(N) in worst case. That's why my original solution was O(N)."
Question 9: "Implement the optimization now."
How You Should Answer (Write This Code):
If they ask you to modify your existing code, here's what to add:
// BEFORE (your original approach):
// vector<int> locked; // locked[i] = uid or -1
// AFTER (optimized -- add this):
vector<unordered_set<int>> lockedDescendants; // lockedDescendants[i] = set of locked desc indices
// Initialize in tree building:
lockedDescendants.resize(N);
// Helper: propagate lock info up to ancestors
void propagateUp(int node, int lockedNode, bool isLocking) {
int cur = parent[node];
while (cur != -1) {
if (isLocking) {
lockedDescendants[cur].insert(lockedNode);
} else {
lockedDescendants[cur].erase(lockedNode);
}
cur = parent[cur];
}
}
bool lock(int node, int uid) {
// Check 1: node itself locked?
if (locked[node] != -1) return false;
// Check 2: any descendants locked? (NOW O(1)!)
if (!lockedDescendants[node].empty()) return false;
// Check 3: any ancestor locked?
int cur = parent[node];
while (cur != -1) {
if (locked[cur] != -1) return false;
cur = parent[cur];
}
// All checks pass -- lock the node
locked[node] = uid;
propagateUp(node, node, true); // inform ancestors
return true;
}
bool unlock(int node, int uid) {
if (locked[node] != uid) return false;
locked[node] = -1;
propagateUp(node, node, false); // remove from ancestors
return true;
}
bool upgrade(int node, int uid) {
// Check 1: node itself must NOT be locked
if (locked[node] != -1) return false;
// Check 2: at least one locked descendant
if (lockedDescendants[node].empty()) return false;
// Check 3: all locked descendants must be same uid
for (int desc : lockedDescendants[node]) {
if (locked[desc] != uid) return false;
}
// Check 4: no ancestor locked
int cur = parent[node];
while (cur != -1) {
if (locked[cur] != -1) return false;
cur = parent[cur];
}
// All pass -- unlock all descendants, then lock this node
// IMPORTANT: copy the set first, because unlock modifies it
vector<int> toUnlock(lockedDescendants[node].begin(), lockedDescendants[node].end());
for (int desc : toUnlock) {
unlock(desc, uid);
}
lock(node, uid);
return true;
}Key point to mention: "Notice that I copy
lockedDescendants[node]into a vector before iterating, because eachunlock()call modifies the set (removes elements from ancestors' sets). If I iterate the set while modifying it, that's undefined behavior."
4. โก PHASE 3: The Thread-Safety Core Task
This is the main event of Part B. The interviewer will now ask:
Question 10: "Let's say you are running lock/unlock on a multi-core machine. Now you want to let multiple threads run lock(). Will doing lock on two nodes cause a race condition? If yes, how will you solve it?"
Additional Constraints (Mentioned Explicitly):
- Multiple threads running simultaneously should NOT affect correctness
- Try to make the critical sections more granular -- don't create big atomic/synchronized blocks that will kill parallelism
- Consider each individual memory operation (read or write of one variable) as atomic
First Thing You Should Say:
You: "Yes, there will absolutely be race conditions. Let me identify the exact cases where they happen.
Before I list them, let me clarify one thing -- when you say 'consider each operation is atomic', you mean each individual read or write to a single variable (like
locked[i]orparent[i]) is atomic, but a compound operation like 'read locked[i], check if it's -1, then write locked[i] = uid' is NOT atomic as a whole -- a context switch can happen between the read and the write. Is that correct?"
(This shows the interviewer you understand the subtlety. They will confirm yes.)
5. ๐ PHASE 4: The Complete 8 Race Conditions Catalogue
THIS IS THE MOST IMPORTANT SECTION. The #1 thing that separates selected candidates from rejected ones is being able to name the race conditions YOURSELF before the interviewer points them out.
Note on Race Condition 2 vs 4: These are the SAME fundamental problem (concurrent mutation of a shared ancestor's tracking data) but manifest differently depending on your data structure choice:
- If you use
unordered_set<int> lockedDescendantsโ you get Race Condition 2 (Set Corruption / Crash).- If you use
int lockedDescCountโ you get Race Condition 4 (Lost Update on counter).- You will only have ONE of these, not both โ whichever matches the data structure you chose.
๐ด Group 1: Lock vs Lock Races (The 3 Classic Ones)
Race Condition 1: Two threads locking THE SAME node simultaneously (TOCTOU)
The Setup:
Thread T1: lock(India, uid=5)
Thread T2: lock(India, uid=7)Expected Behavior:
One should return true, the other should return false. Order doesn't matter -- but exactly ONE must succeed.
What Goes Wrong (Trace Through YOUR Code):
YOUR lock() code:
if (locked[node] != -1) return false; // LINE A: check
... ancestor/descendant checks ...
locked[node] = uid; // LINE B: set
return true;Time | Thread T1 | Thread T2 | locked[India]
-----|--------------------------------------|--------------------------------------|-------------
T1 | LINE A: read locked[4] -> -1 | | -1
T2 | | LINE A: read locked[4] -> -1 | -1
T3 | (passes ancestor/descendant checks) | | -1
T4 | | (passes ancestor/descendant checks) | -1
T5 | LINE B: locked[4] = 5 | | 5
T6 | | LINE B: locked[4] = 7 | 7 <-- OVERWRITES!
T7 | return true | return true | 7Result: BOTH return true. India is "locked" by uid 7 but T1 also thinks it locked India. BUG!
Why It Happens:
"The check-then-set on
locked[node]is not atomic. Thread T1 readslocked[4] == -1, passes the check, but before it can writelocked[4] = 5, Thread T2 also readslocked[4] == -1and also passes. Both proceed to set the lock. This is a classic TOCTOU (Time-Of-Check to Time-Of-Use) bug."
Race Condition 2: Shared Ancestor Mutation โ Set Corruption (Sibling Collision)
(This race applies when using unordered_set<int> lockedDescendants. If using integer counter instead, see Race Condition 4.)
The Setup:
Tree: World (index 0)
/ \
Asia(1) Africa(2)
/ \
China(3) India(4)
Thread T1: lock(China, uid=1)
Thread T2: lock(India, uid=2)Expected Behavior:
Both should succeed -- China and India are siblings, neither is ancestor/descendant of the other.
What Goes Wrong (With the Optimized Version Using lockedDescendants):
When both threads try to propagateUp, they both need to update lockedDescendants[Asia] and lockedDescendants[World].
Time | Thread T1 | Thread T2
-----|----------------------------------------------|----------------------------------------------
T1 | propagateUp: lockedDescendants[1].insert(3) |
T2 | | propagateUp: lockedDescendants[1].insert(4) If the set operations are NOT thread-safe (and std::set / std::unordered_set are NOT thread-safe for concurrent modifications), the insert operations could corrupt the internal data structure -- memory corruption, segfaults, or silently lost data.
Why It Happens:
"Both threads are concurrently modifying the same shared data structure --
lockedDescendants[Asia]. The set container is not thread-safe for concurrent writes. Even if individual memory reads/writes are atomic, a set insertion involves multiple internal operations (rebalancing a tree, updating pointers). Two concurrent inserts can corrupt the set."
Race Condition 3: Ancestor-Descendant conflict -- One thread locks parent, another locks child
The Setup:
Thread T1: lock(World, uid=5)
Thread T2: lock(Asia, uid=7)Expected Behavior:
Exactly one should succeed. If World is locked, Asia (its descendant) cannot be locked. If Asia is locked, World (its ancestor) cannot be locked.
What Goes Wrong:
Time | Thread T1 | Thread T2 | State
-----|----------------------------------------|----------------------------------------|------
T1 | Check: locked[0]==-1 PASS | |
T2 | Check: lockedDescendants[0].empty()? YES| |
T3 | | Check: locked[1]==-1 PASS |
T4 | | Check ancestors: locked[0]==-1 PASS | T2 reads World as unlocked!
T5 | locked[0] = 5. return true. | | World NOW locked
T6 | | locked[1] = 7. return true. | Asia NOW lockedResult: BOTH succeed! World is locked AND its descendant Asia is locked. This violates the fundamental invariant.
Why It Happens:
"Thread T2 checks if its ancestor (World) is locked at time T4, sees it's unlocked, and proceeds. But between T4 and T6, Thread T1 locks World. T2 doesn't re-check -- it already passed the ancestor check and continues to lock Asia. The two checks (T2's ancestor check and T1's locking) are not coordinated."
Race Condition 4: Shared Ancestor Mutation โ Lost Update on Counter (Alternative to RC 2)
(This race applies when using int lockedDescCount instead of a set. If using set, see Race Condition 2 instead. You will have ONE of these, not both.)
If you use a lockedDescCount integer instead of a set:
Thread T1: lock(China, 1) -- needs to increment lockedDescCount[Asia] and lockedDescCount[World]
Thread T2: lock(India, 2) -- also needs to increment lockedDescCount[Asia] and lockedDescCount[World]Time | T1 | T2 | lockedDescCount[Asia]
-----|---------------------------------------------|---------------------------------------------|---------------------
T1 | read lockedDescCount[1] -> 0 | | 0
T2 | | read lockedDescCount[1] -> 0 | 0
T3 | write lockedDescCount[1] = 0 + 1 = 1 | | 1
T4 | | write lockedDescCount[1] = 0 + 1 = 1 | 1 <-- LOST UPDATE!Result: lockedDescCount[Asia] should be 2 (two locked children), but it's 1. Lost update bug.
Why It Happens:
"The increment
lockedDescCount[i]++is a read-modify-write: read current value, add 1, write back. If two threads do this concurrently, one update is lost. This is the classic lost update problem."
๐ก Group 2: Lock vs Unlock Races (Very Subtle!)
Race Condition 5: The "Ghost Lock" False Rejection (Lock during Unlock)
- Node
Indiawas locked. Thread 1 callsunlock(India). - Thread 1 sets
locked[India] = -1and begins walking up to remove India from ancestor sets. - Before Thread 1 updates
World, Thread 2 callslock(World). - Thread 2 checks
World.lockedDescendantsโ still sees India listed! - Result: Thread 2 is falsely rejected even though India is already completely unlocked.
Race Condition 6: Stale Ancestor Lock (Lock during Ancestor Unlock)
Worldwas locked. Thread 1 callsunlock(World).- Thread 2 calls
lock(India). - Thread 2 checks if ancestor
Worldis locked โ readslocked[World]before Thread 1's write to-1reaches main memory โ Thread 2 falsely fails.
๐ก Group 3: Unlock vs Unlock Race
Race Condition 7: Double Unlock on Same Node
- Thread 1 and Thread 2 both call
unlock(India, uid=5)at the exact same moment. - Both read
locked[India] == 5โ both pass the check! - Both walk up to decrement ancestor counters โ ancestor's
lockedDescCountbecomes negative (e.g.-1), corrupting futurelock()calls forever.
๐ด Group 4: Lock vs UpgradeLock Race (The "Interleaved Hijack")
Race Condition 8: Descendant Hijack During Upgrade Window
- Thread 1 calls
upgrade(Asia, uid=9). - Thread 1 verifies all descendants (
China,India) belong to9. โ - Thread 1 unlocks
ChinaandIndia(they become-1). - ๐จ Context switch!
- Thread 2 calls
lock(China, uid=2)โ China is now free, so Thread 2 successfully locks China for user 2! - Thread 1 resumes and locks
Asiafor user 9. - Result:
Asiais locked by user 9 ANDChinais locked by user 2! A complete violation of mutual exclusion โ the subtree containment invariant is broken.
๐ฃ Group 5: The "Rollback Collision" Race
Race Condition 9: Rollback Colliding with a Live Lock
- Thread 1 tries
lock(China)โ insertsChinaintoAsia.lockedDescendants. - Thread 1 reaches
Worldand seesWorldis locked โ Thread 1 fails and triggers a Rollback to eraseChinafromAsia. - At the exact same millisecond, Thread 2 calls
lock(India)and tries to insertIndiaintoAsia. - Result: Thread 1's
erase()and Thread 2'sinsert()collide onAsia's set โ memory corruption during error recovery!
Summary: All Race Conditions to Name (Say This to the Interviewer)
You: "I can identify race conditions across multiple groups:
Lock vs Lock (the 3 classic ones):
- Same node, two threads -- The check-then-set on
locked[node]is not atomic. Both threads can read 'unlocked', both proceed, both write 'locked'. Result: both return true.- Ancestor-descendant conflict -- One thread locks a parent while another thread is still checking if that parent is locked. The check and the lock happen in different threads without coordination.
- Shared ancestor data corruption -- When two threads lock sibling nodes, both try to update the shared ancestor's
lockedDescendantsset or counter concurrently. Sets are not thread-safe for concurrent writes, and counter increments are not atomic read-modify-write operations.Cross-operation races (if the interviewer asks 'any other?'): 4. Lock vs Unlock -- A thread can be falsely rejected if it reads an ancestor's descendant set before an unlock propagation finishes clearing it. 5. Lock vs Upgrade -- A third thread can hijack and lock a newly-freed descendant in the split-second window between when Upgrade unlocks descendants and when it locks the ancestor. 6. Rollback races -- An aborted lock's rollback erase can collide with a concurrent valid lock's insertion on shared ancestors."
6. ๐ก๏ธ PHASE 5: Solutions -- Coarse to Fine-Grained
The interviewer will now ask you to solve the race conditions. They expect you to start with the simplest solution and progressively refine when they add constraints.
Solution Level 1: Global Mutex (Will Be Rejected)
You: "The simplest solution is to wrap the entire lock() function in a global mutex. Only one thread can execute lock() at a time."
mutex globalMutex;
bool lock(int node, int uid) {
lock_guard<mutex> guard(globalMutex);
// ... original logic unchanged ...
}Interviewer will say: "You are not allowed to lock the complete tree. Multiple threads should be allowed to lock different nodes of the tree simultaneously. If you make the function synchronized, you're not allowing other threads to work on the tree."
Your response: "You're right. A global mutex serializes everything -- it kills all parallelism. Two threads trying to lock completely independent nodes (say, one in Asia's subtree and one in Africa's subtree) would unnecessarily block each other. We need a more granular approach."
Solution Level 2: Synchronized on lock() Method Only (Will Be Rejected)
You: "How about making just the lock() method synchronized?"
Interviewer: "Same problem -- synchronized on the method means only one thread can run it at a time. We need multiple threads to be able to work simultaneously on independent parts of the tree."
Solution Level 3: Per-Node Mutex (Partially Accepted, Then Constrained)
You: "We can give each node its own mutex. When locking node X:
- Acquire mutexes on node X and all its ancestors (in a consistent order -- always leaf-to-root to prevent deadlocks)
- Perform the checks
- If checks pass, mark node as locked and update ancestor data
- Release all mutexes
This way, two threads operating on independent subtrees can proceed in parallel -- their paths to the root don't overlap until a common ancestor, and by that point we coordinate properly."
vector<mutex> nodeMutex(N); // one mutex per node
bool lock(int node, int uid) {
// Acquire locks from node up to root
vector<unique_lock<mutex>> locks;
int cur = node;
while (cur != -1) {
locks.emplace_back(nodeMutex[cur]);
cur = parent[cur];
}
// Now we hold exclusive access to this entire path
// Perform all checks safely
if (locked[node] != -1) return false;
if (!lockedDescendants[node].empty()) return false;
// Ancestor check (we already hold locks, so state is stable)
cur = parent[node];
while (cur != -1) {
if (locked[cur] != -1) return false;
cur = parent[cur];
}
// Lock the node and propagate
locked[node] = uid;
cur = parent[node];
while (cur != -1) {
lockedDescendants[cur].insert(node);
cur = parent[cur];
}
return true;
// locks auto-release when vector goes out of scope
}Key Point About Deadlock Prevention (And the Upgrade Trap!):
You: "For
lock()operations, acquiring locks strictly from leaf to root (bottom-up) prevents deadlocks between concurrentlock()calls because the wait-for graph has no cycles.HOWEVER, there is a critical deadlock trap when
upgrade()is introduced:
lock(China)acquires locks leaf-to-root (China $\to$ Asia $\to$ World).upgrade(Asia)checks/locks descendants root-to-leaf (Asia $\to$ China).- If Thread 1 calls
upgrade(Asia)and acquires Asia's lock, while Thread 2 callslock(China)and acquires China's lock:
- Thread 1 holds
Asia, waiting forChina.- Thread 2 holds
China, waiting forAsia.- ๐ฅ CIRCULAR WAIT $\to$ DEADLOCK!
How to solve this completely:
- Global Node ID Ordering (Deterministic Order): Assign each node a unique integer ID (e.g., $0$ to $N-1$ in BFS level order). Whenever any operation (
lock,unlock, orupgrade) needs to acquire multiple locks, it MUST acquire them in strictly increasing order of Node ID. Since the ordering is globally monotonic, circular wait (Coffman Condition 4) is mathematically impossible.- Try-Lock with Backoff: In
upgrade(), acquire locks usingtry_lock(). If any descendant lock cannot be acquired immediately, release all currently held locks, wait a randomized backoff period, and retry."
Why the Interviewer Rejects std::mutex (The "OS Sleep / Futex Overhead"):
Interviewer will say: "Mutex will introduce lag in operations. We are not allowed to use mutex/semaphores. Why is mutex slow?"
Your response: "
std::mutexrelies on OS primitives like Linux futex (FUTEX_WAIT). Under lock contention, the OS transitions from User Mode (Ring 3) to Kernel Mode (Ring 0), deschedules the thread, enqueues it, and triggers a full context switch (saving CPU registers, flushing TLB, cold CPU cache penalty).
- A Tree of Space path check takes only ~20โ40 nanoseconds in L1 cache.
- An OS sleep + context switch + wake-up takes ~1,000โ5,000 nanoseconds (thousands of wasted CPU cycles). Putting a thread to sleep for 3,000 ns to wait for a 30 ns operation kills throughput. That's why low-latency systems demand user-space spinlocks or lock-free atomic operations."
Solution Level 4: Lock-Free with Atomic Variables (std::atomic / CAS)
You: "If atomic variables are permitted, we can eliminate locking entirely by using lock-free hardware atomic instructions (
std::atomic<int>in C++ orAtomicIntegerin Java)."
What is an "Atomic Variable"?
- Regular variable (
int x = 0;): Operations likex++or checkingif (x == -1) x = uid;are non-atomic (multiple CPU assembly steps:MOV,CMP,JNE,MOV). A context switch in between causes race conditions (TOCTOU / lost updates). - Atomic variable (
std::atomic<int> x{-1};): Operations likex.compare_exchange_strong(...)andx.fetch_add(1)are executed as a single, indivisible hardware instruction (LOCK CMPXCHG,LOCK XADD). The CPU hardware guarantees that no other core can read or writexmidway through the operation.
Tree of Space with std::atomic Implementation:
#include <atomic>
#include <vector>
// Lock-Free Tree of Space using Atomic Variables
vector<std::atomic<int>> locked(N); // -1 = unlocked, otherwise uid
vector<std::atomic<int>> lockedDescCount(N); // number of locked descendants
// Initialize:
// for (int i = 0; i < N; i++) { locked[i].store(-1); lockedDescCount[i].store(0); }
bool lockAtomic(int node, int uid) {
// 1. Fast Check: Any descendant locked? (O(1) atomic load)
if (lockedDescCount[node].load(std::memory_order_relaxed) > 0) return false;
// 2. Ancestor Check: Walk up to root
int cur = parent[node];
while (cur != -1) {
if (locked[cur].load(std::memory_order_relaxed) != -1) return false;
cur = parent[cur];
}
// 3. ATOMIC CLAIM: Compare-And-Swap (CAS) on target node
// Only succeeds if locked[node] is currently -1; atomically sets it to uid
int expected = -1;
if (!locked[node].compare_exchange_strong(expected, uid, std::memory_order_acq_rel)) {
return false; // Collision: another thread claimed the node first!
}
// 4. Double-Check Ancestors (Validate that no ancestor was locked concurrently while we claimed)
cur = parent[node];
while (cur != -1) {
if (locked[cur].load(std::memory_order_relaxed) != -1) {
// Rollback our lock and fail
locked[node].store(-1, std::memory_order_release);
return false;
}
cur = parent[cur];
}
// 5. ATOMIC PROPAGATION: Increment ancestor descendant counters
cur = parent[node];
while (cur != -1) {
lockedDescCount[cur].fetch_add(1, std::memory_order_relaxed); // Atomic hardware increment!
cur = parent[cur];
}
return true;
}
bool unlockAtomic(int node, int uid) {
// Atomic CAS to release only if currently locked by the same uid
int expected = uid;
if (!locked[node].compare_exchange_strong(expected, -1, std::memory_order_acq_rel)) {
return false; // Failed: node was not locked by this uid
}
// Atomically decrement ancestor counters
int cur = parent[node];
while (cur != -1) {
lockedDescCount[cur].fetch_sub(1, std::memory_order_relaxed); // Atomic hardware decrement!
cur = parent[cur];
}
return true;
}Why Interviewers Ban Atomic Variables:
Interviewer: "This is clean, but now: I am NOT allowing you to use any atomic variables (
std::atomic) or standard libraries. How will you solve it?"Your response: "Understood.
std::atomicis powerful because the hardware and standard library handle the synchronization under the hood. When atomic variables are banned, we must demonstrate how concurrency actually works from first principles:
- Phase 6: Building our own custom spinlock/mutex using compiler intrinsics (
__sync_*/ CAS).- Phase 7: Pure software-only mutual exclusion algorithms (Peterson's / Lamport's Bakery) using only raw memory reads and writes."
7. ๐จ PHASE 6: Building Your Own Mutex
When the interviewer says "we can't use mutex/semaphores", you respond:
You: "Okay, I can implement my own mutex using compiler-level atomic intrinsics -- specifically
__sync_lock_test_and_setand__sync_lock_release. This gives me a spin-lock."
What is Test-and-Set? (Explain This Clearly)
You: "
__sync_lock_test_and_set(&variable, new_value)is a hardware-level atomic instruction. It does THREE things as a single, indivisible, uninterruptible operation:
- Reads the current value of
variable- Writes
new_valueintovariable- Returns the OLD value that was there before the write
Because it's atomic, no other thread can read or write
variablebetween steps 1 and 2. This is guaranteed by the CPU hardware itself."
Implementation Level 1: Basic TAS Spinlock (No Ownership)
#include <unistd.h>
class SpinLock {
public:
SpinLock() {
isLocked = UNLOCKED_VALUE;
}
void lock() {
// Atomically set isLocked to 1 and get the old value
// If old value was 1 (already locked), someone else holds it -> spin
// If old value was 0 (unlocked), we just set it to 1 -> we got the lock!
while (__sync_lock_test_and_set(&isLocked, LOCKED_VALUE) == LOCKED_VALUE) {
// Spin-wait (busy loop) until the lock is released
}
}
void unlock() {
__sync_lock_release(&isLocked);
// This atomically sets isLocked back to 0
}
bool try_lock() {
// Non-blocking attempt: try to acquire, return immediately
return __sync_lock_test_and_set(&isLocked, LOCKED_VALUE) != LOCKED_VALUE;
}
private:
static const int LOCKED_VALUE = 1;
static const int UNLOCKED_VALUE = 0;
volatile int isLocked;
};โ ๏ธ THE OWNERSHIP FLAW IN BARE SPINLOCKS (Crucial Interview Point!): Notice that
SpinLockabove only toggles a binary flag (0vs1). It has no concept of thread ownership.
- If Thread A calls
lock(), Thread B can callunlock()and release the lock!- This means it technically behaves like a binary semaphore, not a true Mutex.
- In a true Mutex, ONLY the thread that locked the mutex is permitted to unlock it.
Implementation Level 2: The True Mutex (With Strict Thread Ownership)
You: "To make it a true
Mutexrather than a binary semaphore, we must record the Thread ID of the locking thread and verify ownership insideunlock(). We use Compare-And-Swap (CAS) to atomically claim ownership."
#include <thread>
#include <atomic>
#include <cassert>
#include <stdexcept>
class OwnershipMutex {
private:
// Tracks which thread owns the lock (default is empty/unowned)
std::atomic<std::thread::id> owner{std::thread::id()};
public:
OwnershipMutex() = default;
void lock() {
std::thread::id my_id = std::this_thread::get_id();
std::thread::id expected = std::thread::id(); // expected: unowned
// Atomically swap owner from unowned to my_id.
// memory_order_acquire ensures subsequent critical section operations
// cannot be reordered before this lock.
while (!owner.compare_exchange_weak(
expected,
my_id,
std::memory_order_acquire,
std::memory_order_relaxed))
{
expected = std::thread::id(); // reset expected on CAS failure
std::this_thread::yield(); // yield CPU or pause to reduce bus contention
}
}
void unlock() {
std::thread::id my_id = std::this_thread::get_id();
// ๐จ STRICT OWNERSHIP VERIFICATION:
if (owner.load(std::memory_order_relaxed) != my_id) {
throw std::runtime_error("IllegalMonitorState: Thread does NOT own this lock!");
}
// memory_order_release flushes all changes before clearing the lock
owner.store(std::thread::id(), std::memory_order_release);
}
bool try_lock() {
std::thread::id my_id = std::this_thread::get_id();
std::thread::id expected = std::thread::id();
return owner.compare_exchange_strong(
expected,
my_id,
std::memory_order_acquire,
std::memory_order_relaxed
);
}
};Why volatile vs Memory Barriers? (Crucial CS Fundamentals Distinction)
Interviewer: "Why did you use
volatile? Doesvolatilemake a variable thread-safe in C++?"Your response: "No! In C and C++,
volatileonly prevents the compiler from caching the variable in a CPU register or optimizing away reads/writes. It does NOT emit CPU memory fences, does not guarantee atomicity, and does not prevent out-of-order CPU memory reordering (unlike Java'svolatile).To ensure true thread safety across multi-core CPUs:
- We rely on hardware atomic intrinsics (
__sync_lock_test_and_set,__sync_val_compare_and_swap) or C++std::atomic.__sync_lock_test_and_setprovides an Acquire barrier (subsequent memory reads/writes cannot be moved before it).__sync_lock_releaseprovides a Release barrier (prior memory reads/writes must complete and flush before the lock is cleared)."
Why This Is a "Spin Lock":
You: "This is called a spin lock because the thread that fails to acquire the lock doesn't go to sleep or yield the CPU -- it literally spins in a
whileloop, repeatedly trying to grab the lock. This is fast for short critical sections (no context switch overhead) but wastes CPU cycles if the lock is held for a long time."
Why the Naive Approach Doesn't Work:
// THIS IS BROKEN:
// while (1) {
// if (isLocked == UNLOCKED_VALUE) { // READ
// isLocked = LOCKED_VALUE; // WRITE
// return;
// }
// }You: "This naive version has the same TOCTOU race condition. Between the
ifcheck (READ) and the assignment (WRITE), another thread can swoop in and also seeisLocked == 0. Both threads then set it to 1 and both think they acquired the lock. The__sync_lock_test_and_setversion fuses the read and write into a single atomic instruction, eliminating this gap."
Interviewer Follow-Up: "Now implement thread-safe lock() using your custom mutex."
You: "I can now use my custom Mutex class exactly like std::mutex. Each node gets its own Mutex, and I acquire/release along the ancestor path."
vector<Mutex> nodeLocks(N); // one custom spin-lock per node
bool lock(int node, int uid) {
// Acquire spin-locks from node up to root
vector<int> path;
int cur = node;
while (cur != -1) {
path.push_back(cur);
cur = parent[cur];
}
// Lock all nodes on the path (leaf to root order)
for (int p : path) {
nodeLocks[p].lock();
}
// === CRITICAL SECTION START ===
// Check 1: node already locked?
if (locked[node] != -1) {
for (int p : path) nodeLocks[p].unlock();
return false;
}
// Check 2: any descendant locked?
if (!lockedDescendants[node].empty()) {
for (int p : path) nodeLocks[p].unlock();
return false;
}
// Check 3: any ancestor locked?
for (int i = 1; i < (int)path.size(); i++) {
if (locked[path[i]] != -1) {
for (int p : path) nodeLocks[p].unlock();
return false;
}
}
// All checks pass -- lock the node
locked[node] = uid;
for (int i = 1; i < (int)path.size(); i++) {
lockedDescendants[path[i]].insert(node);
}
// === CRITICAL SECTION END ===
for (int p : path) nodeLocks[p].unlock();
return true;
}Why this preserves parallelism: "Two threads locking nodes in completely independent subtrees (e.g., one under Asia, one under Africa) acquire spin-locks on completely different nodes. They don't contend at all and proceed in parallel. Only when their paths share common ancestors do they need to coordinate -- and even then, the spin-lock hold times are very short."
8. ๐ง PHASE 7: The "No Primitives" Solution -- Software Algorithms & Set-and-Test
This is the hardest and most commonly failed part. The interviewer will now challenge:
Interviewer: "Now implement thread-safe locking WITHOUT using ANY libraries, atomic variables (
std::atomic), compiler atomic intrinsics (__sync_*), mutexes, semaphores, or thesynchronizedkeyword. You ONLY have standard, raw memory reads and writes."
โ ๏ธ The Trap Most Candidates Fall Into: The x++ Myth
Many candidates attempt to write:
lockedCount[node]++; // โ FATAL MISTAKE TO CLAIM THIS IS ATOMIC!CRUCIAL CS FUNDAMENTAL: In C/C++,
x++is NEVER an atomic write. It compiles into 3 separate assembly instructions:
MOV EAX, [lockedCount](Read from RAM to register)INC EAX(Modify register)MOV [lockedCount], EAX(Write back to RAM)If a context switch occurs between (1) and (3), another thread's update is permanently lost. If you claim
x++is atomic, the interviewer will reject you for failing basic computer architecture!
The True Answer: Pure Software Mutual Exclusion Algorithms
When hardware atomic instructions (LOCK CMPXCHG, XCHG) do not exist, computer science provides pure software mutual exclusion algorithms that rely strictly on atomic single-word memory reads and writes.
Solution A: Peterson's Algorithm (2 Threads per Resource)
If 2 threads compete for a node, Peterson's Algorithm guarantees Mutual Exclusion, Progress, and Bounded Waiting without any hardware primitives:
class PetersonLock {
private:
volatile bool flag[2] = {false, false}; // flag[i] = true means thread i wants lock
volatile int turn = 0; // tie-breaker
public:
void lock(int thread_id) { // thread_id is 0 or 1
int other = 1 - thread_id;
flag[thread_id] = true;
turn = other; // "Politeness": give priority to other thread first
// Wait while other thread wants to enter AND it is other thread's turn
while (flag[other] && turn == other) {
// spin-wait
}
}
void unlock(int thread_id) {
flag[thread_id] = false;
}
};Proof of Correctness: For both threads to be in the critical section,
flag[0] == trueandflag[1] == true. Butturncan only be0or1, not both. The last thread to writeturnwill be trapped in thewhileloop.
Solution B: Lamport's Bakery Algorithm ($N$ Threads)
For $N$ concurrent threads, we use Lamport's Bakery Algorithm (based on ticket numbers in a bakery).
- Each thread takes a ticket number
number[i] = 1 + max(all ticket numbers). - Threads are served in order of smallest ticket number:
(number[i], thread_id). - It relies only on reading and writing memory variables.
Solution C: The Juspay "Set-and-Test" Optimistic Pattern (With Caveats)
If the interviewer specifically asks: "Can we do an optimistic check without building full Peterson/Bakery queues?", then you present the Set-and-Test pattern:
You: "Instead of 'test-then-set' (checking if 0 then setting to 1, which has a huge TOCTOU window), we can use 'set-then-test' (claiming first, then verifying no collision).
However, I must emphasize that for
lockedCount++to be safe, the hardware must support single-instruction increment or we must accept that under raw memory, interleaved increments can still lose updates unless guarded by a software protocol."
// Optimistic Set-and-Test Pattern:
vector<int> lockedCount(N, 0); // 0 = free, >0 = claimed
vector<int> locked(N, -1); // stores uid
bool lock(int node, int uid) {
// EARLY CHECK: quick bail-out if obviously occupied
if (lockedCount[node] > 0 || !lockedDescendants[node].empty()) return false;
// 1. SET FIRST: optimistically claim the node
// (Note: Requires atomic increment or software lock to prevent lost updates)
lockedCount[node]++;
// 2. TEST AFTER: did another thread collide?
if (lockedCount[node] > 1) {
// Collision detected! Rollback and yield
lockedCount[node]--;
return false;
}
// 3. Walk up ancestors with rollback capability
int cur = parent[node];
while (cur != -1) {
// Check if ancestor is locked or descendant set modified
if (lockedCount[cur] > 0 || !lockedDescendants[node].empty()) {
// ROLLBACK: undo everything
lockedCount[node]--;
int rollback = parent[node];
while (rollback != cur) {
lockedDescendants[rollback].erase(node);
rollback = parent[rollback];
}
return false;
}
// Inform ancestor
lockedDescendants[cur].insert(node);
cur = parent[cur];
}
// All checks passed
locked[node] = uid;
return true;
}The Difference: "Test-and-Set" vs "Set-and-Test"
You: "The philosophical difference is:
- Test-and-Set (Naive): Read first (
if == 0), then write (val = 1). A context switch between read and write allows another thread to pass too (TOCTOU).- Set-and-Test (Optimistic): Write first (
val++), then read (if > 1). Both threads register their intent before checking, making collision detection possible."
9. ๐ PHASE 8: Finding Bugs in "Thread-Safe" Solutions
THE PSYCHOLOGICAL TRAP: The interviewer will present a solution (sometimes yours, sometimes one they suggest) and tell you it's thread-safe. The test is whether you push back and find the bug rather than accepting it.
How to Respond When Interviewer Says "This is thread-safe":
You: "Hmm, I think there might still be some issues. Can I take a minute to think through some specific thread interleavings?"
NEVER just agree. Always trace through specific scenarios.
Bug 1: Same Node -- Both Threads Get Rejected (No One Locks)
The Scenario:
Thread T1: lock(India, uid=5)
Thread T2: lock(India, uid=5)
Expected: (true, false) or (false, true) -- exactly one succeedsThe Buggy Trace:
Using the lockedCount approach:
bool lock(int node, int uid) {
if (lockedCount[node] > 0 || ...) return false; // condition1
// <- point1
lockedCount[node]++; // line2
// <- point2
if (lockedCount[node] > 1) { // condition3
// <- point3
lockedCount[node]--;
return false;
}
...
}Step | Thread T1 | Thread T2 | lockedCount[India]
-----|----------------------------------------------------|----------------------------------------------------|-------------------
1 | condition1: lockedCount == 0 -> false, continue | | 0
2 | [CONTEXT SWITCH at point1] | | 0
3 | | condition1: lockedCount == 0 -> false, continue | 0
4 | [CONTEXT SWITCH at point1] | | 0
| Both threads are now at point1 | |
5 | line2: lockedCount++ -> becomes 1 | | 1
6 | [CONTEXT SWITCH at point2] | | 1
7 | | line2: lockedCount++ -> becomes 2 | 2
8 | [CONTEXT SWITCH at point2] | | 2
| Both threads are now at point2 | |
9 | condition3: lockedCount > 1 -> 2 > 1 -> TRUE | | 2
10 | [CONTEXT SWITCH at point3] | | 2
11 | | condition3: lockedCount > 1 -> 2 > 1 -> TRUE | 2
12 | [CONTEXT SWITCH at point3] | | 2
| Both threads are now at point3 | |
13 | lockedCount-- -> becomes 1. return false. | | 1
14 | | lockedCount-- -> becomes 0. return false. | 0Result: Both return false. Nobody locked India. lockedCount is back to 0. BUG! Expected one of them to succeed.
Why It Happens:
"The early check (
condition1) before the increment creates a window. Both threads passcondition1seeinglockedCount == 0, then both increment, then both seelockedCount > 1and both roll back. The 'set-and-test' pattern can cause both threads to fail, violating the requirement that at least one should succeed."
Bug 2: Ancestor-Descendant -- Both Succeed (MAJOR BUG)
The Scenario:
Tree: World (0)
/
Asia (1)
Thread T1: lock(World, uid=5)
Thread T2: lock(Asia, uid=5)
Expected: (true, false) or (false, true) -- exactly one succeedsThe Buggy Trace:
bool lock(int node, int uid) {
if (lockedCount[node] > 0 || ...) return false; // condition1
// <- point1
lockedCount[node]++;
if (lockedCount[node] > 1) { lockedCount[node]--; return false; }
int cur = parent[node];
while (cur != -1) {
if (lockedCount[cur] > 0 || ...) { // condition2
// rollback and return false
}
// <- point2
lockedDescendants[cur].insert(node);
cur = parent[cur];
}
locked[node] = uid;
return true;
}Step | Thread T1 (locking World, idx=0) | Thread T2 (locking Asia, idx=1) | State
-----|-----------------------------------------------------|----------------------------------------------------|---------
1 | condition1: lockedCount[0]==0 PASS | |
| [CONTEXT SWITCH at point1 -- BEFORE increment!] | |
2 | | condition1: lockedCount[1]==0 PASS |
3 | | lockedCount[1]++ -> 1 | lc[1]=1
4 | | lockedCount[1] > 1? NO |
5 | | while(cur): cur = parent[1] = 0 |
6 | | condition2: lockedCount[0] > 0? -> 0 > 0? NO PASS |
7 | | lockedDescendants[0].insert(1) |
8 | | cur = parent[0] = -1 -> exit loop |
9 | | locked[1] = 5. return true. | Asia locked!
| [CONTEXT SWITCH back to T1] | |
10 | lockedCount[0]++ -> 1 | | lc[0]=1
11 | lockedCount[0] > 1? NO | |
12 | while(cur): parent[0] = -1 -> loop doesn't execute | |
13 | locked[0] = 5. return true. | | World locked!Result: BOTH return true! World is locked AND Asia (its descendant) is locked. VIOLATES THE INVARIANT.
Why It Happens:
"Thread T1 was about to increment
lockedCount[0]but got context-switched before doing so. Thread T2 proceeds with locking Asia, checks World'slockedCount[0]which is still 0, sees no conflict, and locks Asia successfully. Then T1 resumes, increments, and since World is the root (no parent to check), it also succeeds. Neither thread saw the other's lock because the increment happened too late."
The Key Insight to Communicate (The Interview Differentiator):
You: "This bug reveals the fundamental limit of naive Optimistic Concurrency Control (OCC) without coordination:
- Multi-Variable Invariant Problem: A single variable read or write being atomic is not enough when our business invariant spans multiple separate nodes (
locked[World]andlocked[Asia]). The state of the tree can change midway between checking node $A$ and modifying node $B$.- How Systems Actually Solve This:
- Path 1 (Hardware Atomics Permitted): In real production systems (and when compiler intrinsics are allowed), we use fine-grained per-node spinlocks or CAS state transitions (
std::atomic<int>or__sync_val_compare_and_swap), always acquiring locks in a Global Node ID order to eliminate deadlocks.- Path 2 (Strict Software-Only, No Hardware Atomics): When hardware atomic instructions are strictly prohibited, we protect the critical section using Lamport's Bakery Algorithm (or Peterson's Algorithm for 2 threads), which guarantees mutual exclusion using only ordinary atomic memory reads and writes.
The key is understanding why naive check-then-set and set-and-test break across multi-node trees, and knowing the exact theoretical and practical primitives that fix them."
10. ๐งต PHASE 9: Thread Interleaving Traces
The interviewer will ask you to trace specific scenarios on demand. Here are the ones to practice:
Trace Format (Use This Exact Format):
Time | Thread A | Thread B | Shared State
-----|------------------------------------|------------------------------------|------------------
T1 | [action] | | [state after]
T2 | | [action] | [state after]
T3 | [action] | | [state after]Scenario 1: Two threads lock same node (same uid)
T1 -> lock(India, 5)
T2 -> lock(India, 5)
Time | T1 | T2 | locked[India]
-----|-----------------------------------------|---------------------------------------|-------------
T1 | read locked[India] = -1 PASS | | -1
T2 | | read locked[India] = -1 PASS | -1
T3 | (ancestor check passes) | |
T4 | | (ancestor check passes) |
T5 | locked[India] = 5, return true | | 5
T6 | | locked[India] = 5, return true | 5 (overwrite)
RESULT: Both return true. BUG.Scenario 2: Two threads lock same node (different uid)
T1 -> lock(India, 5)
T2 -> lock(India, 7)
Time | T1 | T2 | locked[India]
-----|-----------------------------------------|---------------------------------------|-------------
T1 | read locked[India] = -1 PASS | | -1
T2 | | read locked[India] = -1 PASS | -1
T3 | locked[India] = 5 | | 5
T4 | | locked[India] = 7 | 7 <-- overwrite!
T5 | return true | return true |
RESULT: Both true. India "locked" by uid 7 but T1 thinks it locked India too. BUG.Scenario 3: Parent and child lock attempt
Tree: World
/
Asia
T1 -> lock(World, 1)
T2 -> lock(Asia, 2)
Correct: Exactly one should succeed.
Time | T1 | T2 | State
-----|-----------------------------------------|---------------------------------------|------------------
T1 | check locked[World]==-1 PASS | | all unlocked
T2 | check descendants(World): none locked | |
T3 | | check locked[Asia]==-1 PASS |
T4 | | check ancestors: locked[World]==-1 | T2 sees World unlocked
T5 | locked[World] = 1, return true | | World locked
T6 | | locked[Asia] = 2, return true | Asia locked too!
RESULT: Both true. World locked AND its child Asia locked. INVARIANT VIOLATED. BUG.Scenario 4: Siblings lock (should both succeed)
Tree: Asia
/ \
China India
T1 -> lock(China, 1)
T2 -> lock(India, 2)
With proper synchronization: Both should succeed.
Time | T1 | T2 | State
-----|-------------------------------------------|-----------------------------------------|-----
T1 | check locked[China]==-1 PASS | |
T2 | check descendants(China): empty PASS | |
T3 | check ancestors: locked[Asia]==-1 PASS | |
T4 | locked[China]=1, propagateUp(Asia) | | China locked
T5 | | check locked[India]==-1 PASS |
T6 | | check descendants(India): empty PASS |
T7 | | check ancestors: locked[Asia]==-1 PASS |
T8 | | locked[India]=2, propagateUp(Asia) | India locked
RESULT: Both true. CORRECT! (Siblings can both be locked.)But the race condition is in propagateUp -- both threads insert into lockedDescendants[Asia] concurrently. Need synchronized access to the set.
11. ๐ PHASE 10: OS / CN / Fundamentals Questions
These come toward the end of Part B. Prepare these specific answers.
Q: "What is a process vs a thread?"
You: "A process is an independent program in execution with its own memory space -- its own code segment, data segment, heap, and stack. Two processes don't share memory (unless explicitly using IPC like shared memory).
A thread is a lightweight unit of execution within a process. Multiple threads in the same process share the same code, data, and heap, but each thread has its own stack and program counter. This makes thread creation faster and inter-thread communication easier (they can directly access shared variables), but it also introduces synchronization challenges -- which is exactly what we've been discussing with the tree of space problem.
When to use processes: when you need isolation (one crash shouldn't affect another). When to use threads: when you need shared data and fast communication within the same task."
Q: "What happens when you play a song and write code on a single-core PC?"
You: "On a single-core CPU, true parallelism is impossible -- only one thread/process runs at a time. The OS creates the illusion of parallelism through context switching -- rapidly switching between the music player process and the code editor process.
When a context switch happens:
- The OS saves the current process's state (register values, program counter, stack pointer) into its PCB (Process Control Block)
- The OS loads the saved state of the next process from its PCB
- The CPU resumes executing the next process
This happens so fast (typically every 1-10 milliseconds) that it feels like both programs are running simultaneously. The scheduler (using algorithms like Round Robin) decides which process gets CPU time next.
The overhead: each context switch takes time to save/restore state, flush caches, etc. This is why on a single-core machine, running too many programs slows everything down."
Q: "What are the DNS record types?"
You:
- A record -- Maps a domain name to an IPv4 address (e.g., google.com -> 142.250.190.46)
- AAAA record -- Maps a domain name to an IPv6 address
- CNAME record -- Canonical Name -- creates an alias. Maps one domain name to another (e.g., www.google.com -> google.com). It's a redirect at the DNS level.
- MX record -- Mail Exchange -- specifies the mail server responsible for receiving emails for the domain
- NS record -- Name Server -- specifies which DNS servers are authoritative for the domain
- TXT record -- Text -- stores arbitrary text. Used for domain verification (SPF, DKIM for email authentication), and other metadata"
Q: "What is idempotency and why does it matter in payment systems?"
You: "Idempotency means that making the same request multiple times produces the same result as making it once. For example, if I send 'charge 500 to card ending 1234' twice due to a network retry, the customer should only be charged 500 once, not 1000.
Why it's critical in payments:
- Network failures cause automatic retries
- Users may double-click the 'Pay' button
- Mobile apps may resend requests on reconnection
How to implement: Each transaction gets a unique idempotency key (a UUID generated by the client). The server stores:
idempotency_key -> response. When a duplicate request arrives with the same key, the server returns the stored response without processing the transaction again.At Juspay specifically, this is critical because they process 300M+ transactions per day across multiple payment providers. Every retry must be idempotent."
Q: "How does SSL/TLS certificate validation work? How does your computer validate a CA?"
You: "When you visit https://google.com:
- Client Hello -- Your browser sends supported TLS versions and cipher suites
- Server Hello -- Server responds with its chosen cipher suite and its certificate (containing its public key, signed by a Certificate Authority)
- Certificate Validation -- Your browser validates the certificate:
- Checks the certificate isn't expired
- Checks the domain matches
- Traces the certificate chain up to a Root CA
- Your OS/browser ships with a pre-installed trust store of Root CA certificates (like DigiCert, Let's Encrypt). If the chain leads to one of these trusted roots, the certificate is valid.
- Key Exchange -- Client generates a pre-master secret, encrypts it with the server's public key (asymmetric encryption), sends it. Both sides derive the session key.
- Symmetric Encryption -- All further communication uses the shared session key (symmetric encryption like AES) -- it's faster than asymmetric."
Q: "How does HashMap work internally?"
You: "A HashMap stores key-value pairs using a hash-based approach:
Hash Function -- When you insert a key, the HashMap computes
hashCode(key)to get an integer, then useshash % bucketCountto determine which bucket the key goes into.Buckets -- Internally, it's an array of buckets. Each bucket holds entries that hash to the same index.
Collision Resolution -- When two keys hash to the same bucket:
- Before Java 8: Each bucket is a linked list. On collision, the new entry is appended. Lookup in a bucket is O(n) where n = chain length.
- After Java 8: When a bucket's chain exceeds 8 entries, it's converted from a linked list to a red-black tree. This improves worst-case lookup from O(n) to O(log n).
Load Factor & Rehashing -- When the number of entries exceeds
capacity x loadFactor(default 0.75), the HashMap doubles its bucket count and rehashes all entries.Average case: O(1) for get/put. Worst case: O(n) (all keys hash to same bucket, pre-Java 8) or O(log n) (Java 8+)."
Q: "What are the four conditions for deadlock?"
You: "A deadlock occurs when all four of these conditions hold simultaneously:
- Mutual Exclusion -- At least one resource is held in a non-shareable mode (only one thread can use it at a time)
- Hold and Wait -- A thread holds at least one resource and is waiting to acquire additional resources
- No Preemption -- Resources cannot be forcibly taken from a thread; they must be released voluntarily
- Circular Wait -- A circular chain of threads exists where each thread holds a resource the next thread wants
Prevention: Break any one condition.
- Break circular wait: impose an ordering on resource acquisition (e.g., always lock nodes from leaf to root)
- Break hold and wait: require threads to acquire all resources at once (or release all before requesting new ones)
- Break no preemption: allow the OS to forcibly reclaim resources
In our tree locking problem, we prevent deadlocks by always acquiring node locks in a consistent order (leaf-to-root), which breaks the circular wait condition."
Q: "What is cache coherence? What is the MESI protocol?"
You: "In a multicore processor, each core has its own cache (L1, L2). When multiple cores access the same memory location, their caches can hold different (stale) values. Cache coherence protocols ensure that all cores see a consistent view of memory.
MESI Protocol -- Each cache line is in one of four states:
- M (Modified) -- This core has modified the data. No other cache has a valid copy. Must be written back to main memory before anyone else reads it.
- E (Exclusive) -- This core has the data and no one else does. It's clean (matches main memory). Can be promoted to Modified without bus traffic.
- S (Shared) -- Multiple cores have this data. All copies match main memory. To modify, must invalidate other copies first.
- I (Invalid) -- This cache line is stale/empty. Must fetch from main memory or another cache.
This is directly relevant to our tree problem -- when one thread modifies
locked[node], the MESI protocol ensures other cores see the updated value (eventually). But 'eventually' is the problem -- without proper synchronization, a thread might read a stale cached value."
12. ๐ค PHASE 11: Resume & Project Deep-Dive
This typically comes toward the end. For EVERY project on your resume, prepare:
- Problem Statement -- What problem does it solve? (1 sentence)
- Architecture -- High-level components and how they interact
- Tech Stack -- Why you chose each technology
- Your Contribution -- What specifically YOU built
- Challenges -- Hardest technical challenge and how you solved it
- Scale -- How many users/requests/data?
- Trade-offs -- What design trade-offs and why?
- What Would You Change -- If you rebuilt it, what differently?
Cross-questioning patterns to expect:
- "Why did you use X instead of Y?"
- "What happens if this component fails?"
- "How would you scale this to 10x users?"
- "What's the time complexity of your critical path?"
- "How would you make this thread-safe?"
"Why Juspay?"
"Juspay is building the infrastructure layer for digital payments in India -- processing over 300 million transactions per day for companies like Amazon, Flipkart, and Google Pay. I'm excited about the engineering challenges at this scale: ensuring zero-downtime, idempotent transaction processing, low-latency routing across multiple payment gateways, and building systems that handle India's peak traffic volumes like during sale events. The concurrency and data structure challenges in the hackathon are a great reflection of the kind of engineering problems Juspay solves daily."
13. ๐ Every Possible Question -- With Full Answers
Here is a comprehensive list of every question that has been asked in Part B:
| # | Question | Where Answered |
|---|---|---|
| 1 | "Explain the logic you used to solve Part A and its time/space complexity" | Phase 1, Q1 |
| 2 | "Walk me through your Part A solution line by line" | Phase 1, Q1 |
| 3 | "Can you improve these complexities?" | Phase 2, Q6 |
| 4 | "What will be the final complexity after this change? Same or different?" | Phase 2, Q7 |
| 5 | "Why is Lock O(log_m N) and not O(N)?" | Phase 2, Q8 |
| 6 | "Make lock() thread-safe -- multiple threads running simultaneously" | Phase 3, Q10 |
| 7 | "In which cases there will be a race condition?" | Phase 4 -- All Race Conditions |
| 8 | "Any other case where there will be race condition?" | Phase 4 -- Race Conditions 2, 3, 4 |
| 9 | "How will you solve those race conditions?" | Phase 5 -- Solutions |
| 10 | "You can't use mutex/semaphores -- how?" | Phase 6 -- Build Your Own Mutex |
| 11 | "How will you implement your own mutex?" | Phase 6 -- Mutex Implementation |
| 12 | "Now take away atomics too -- implement without ANY atomic/mutex/semaphore/synchronized" | Phase 7 -- Set-and-Test |
| 13 | "Is this solution thread-safe?" (they present a buggy solution) | Phase 8 -- Finding Bugs |
| 14 | "Two threads lock same node -- what happens?" | Phase 9, Scenario 1-2 |
| 15 | "Two threads lock different nodes that share a common ancestor -- what happens?" | Phase 9, Scenario 3-4 |
| 16 | "Thread A does X, context switch, Thread B does Y -- what state is the tree in?" | Phase 9 -- All Traces |
| 17 | "Build your own mutex from compiler intrinsics" | Phase 6 |
| 18 | "What is process vs thread?" | Phase 10 |
| 19 | "What are DNS record types?" | Phase 10 |
| 20 | "What is idempotency? Why critical in payments?" | Phase 10 |
| 21 | "How does SSL/TLS certificate validation work?" | Phase 10 |
| 22 | "How does HashMap work internally?" | Phase 10 |
| 23 | "What are the four conditions for deadlock?" | Phase 10 |
| 24 | "What is cache coherence / MESI protocol?" | Phase 10 |
| 25 | "What happens when you play a song and code on single-core PC?" | Phase 10 |
| 26 | "Why Juspay?" | Phase 11 |
| 27 | "We will run lock method only or all three?" | Phase 1 -- Clarification |
| 28 | "What are the edge cases?" | Phase 1, Q4 |
| 29 | "What about fine-grained vs coarse-grained locking?" | Key Concepts below |
| 30 | "What about lock ordering to prevent deadlocks?" | Phase 10 -- Deadlocks |
| 31 | "What is the volatile keyword?" | Phase 6 -- Why volatile |
| 32 | "What is the ABA problem?" | Key Concepts below |
| 33 | "How would you handle unlock/upgrade for thread safety?" | Additional Topics below |
Additional: What About Unlock and Upgrade Thread Safety?
If the interviewer asks about making unlock() and upgrade() thread-safe too:
Unlock Thread Safety:
"Unlock has a simpler race condition -- two threads could try to unlock the same node. With the lockedCount approach, unlock decrements lockedCount. If two threads call unlock simultaneously on the same node, and lockedCount is 1, both read 1, both decrement to 0, and we get lockedCount = -1. The fix is the same -- use atomic decrement or test-and-set."
Upgrade Thread Safety:
"Upgrade is the most complex because it does three things: checks descendants, unlocks them all, then locks the node. The window between unlocking descendants and locking the node is dangerous -- another thread could lock one of those descendants in between. In a production system, you'd hold a lock on the entire path during the upgrade operation."
Key Concepts You Must Be Able to Explain
| Concept | What to Say |
|---|---|
| Contention | "When many threads try to lock nearby nodes simultaneously, they compete for the same ancestor spin-locks/counters. This slows everything down as threads spend time spinning instead of doing useful work." |
| Fine-grained vs Coarse-grained locking | "Coarse-grained: one big lock for the entire tree -- simple but no parallelism. Fine-grained: per-node locks -- complex but allows independent operations on different subtrees to proceed in parallel. Trade-off: more locks = more memory overhead and complexity, but better throughput under contention." |
| Lock-free programming | "Using CAS (Compare-And-Swap) operations instead of locks. CAS atomically checks if a variable has an expected value and sets it to a new value if it does. Multiple threads can attempt CAS simultaneously -- exactly one succeeds, others retry. No thread blocks, so no deadlock." |
| Why naive mutex kills concurrency | "If you wrap lock() in a global mutex, only one thread can execute it at a time. Two threads trying to lock completely independent nodes (one in Asia, one in Africa) would block each other for no reason." |
| Siblings should not block each other | "China and India are siblings under Asia. Locking China should NOT prevent another thread from locking India simultaneously. They share an ancestor but they don't conflict -- proper fine-grained locking allows this." |
| Lock ordering | "To prevent deadlocks, always acquire locks in a globally consistent order. While leaf-to-root works for concurrent lock() calls, upgrade() acquires locks downwards, which causes a circular wait deadlock with lock(). The true universal solution is acquiring locks strictly in increasing order of Global Node ID." |
| ABA problem | "Thread 1 reads value A, gets preempted. Thread 2 changes A to B, then back to A. Thread 1 resumes, sees A, thinks nothing changed. But the state DID change in between. This is a problem for CAS-based approaches. Solution: use a version counter alongside the value." |
| Memory ordering / Instruction reordering | "Compilers and CPUs may reorder instructions for optimization. In single-threaded code this is safe. In multi-threaded code, reordering can cause threads to see stale or incorrect values. Memory barriers/fences enforce ordering." |
| Peterson's Algorithm | "A software-only mutual exclusion algorithm for two threads. Each thread sets a flag and yields turn to the other. Works without hardware atomics but only for exactly 2 threads and requires volatile/memory barriers." |
| Lamport's Bakery Algorithm | "Extends Peterson's to N threads. Each thread takes a 'number' (like a bakery ticket). The thread with the smallest number goes first. Requires only read/write atomicity, no special hardware." |
14. ๐ฃ๏ธ Quick-Reference: Things to Say Out Loud
When Explaining Your Part A Solution:
"I used flat vectors with integer indices for O(1) direct array access. I have an unordered_map for name-to-index mapping, a parent vector for O(1) parent lookup, an adjacency list for children, and a locked vector storing uids. The ancestor check walks up using the parent array in O(log_m N). My original descendant check was O(N) via BFS, but I can optimize it to O(1) using a lockedDescendants set propagated up during lock/unlock."
When Naming Race Conditions:
"There are three key race conditions:
- Same node, two threads -- the check-then-set on the locked flag is not atomic. Both read 'unlocked', both set 'locked'.
- Ancestor-descendant conflict -- one thread checks the ancestor is unlocked, another thread locks it between the check and the actual lock.
- Shared data corruption -- when two threads lock sibling nodes, both try to update the shared ancestor's lockedDescendants set concurrently, corrupting the container."
When Asked "Is This Thread-Safe?":
"Let me trace through a specific interleaving to verify. Suppose Thread A reaches this line, then a context switch happens..."
(NEVER just say "yes looks good" -- always trace!)
When Asked About Approach Without Mutex:
"If hardware atomic primitives are strictly prohibited, the mathematically sound answer is a pure software mutual exclusion algorithm like Peterson's Algorithm (for 2 threads) or Lamport's Bakery Algorithm (for N threads), which requires only atomic single-word memory reads and writes. If we want an optimistic approach, we can discuss set-and-test with lockedCount, but note that
lockedCount++is a 3-step read-modify-write that still requires coordination across multi-node trees to avoid race windows."
When Finding Bugs in Presented Code:
"I think this may have an issue. Let me trace through a specific case: suppose T1 is trying to lock node X and T2 is trying to lock node Y, where X is an ancestor of Y. If T1 reaches [point] and gets context-switched..."
15. โ ๏ธ Common Mistakes to Avoid
| Mistake | What To Do Instead |
|---|---|
| Saying "yes, this is thread-safe" without checking | ALWAYS trace through at least 2 interleavings before agreeing |
| Immediately suggesting global mutex | Start with global mutex but immediately explain why it's bad, then propose finer solutions |
| Claiming a bare spinlock is a true mutex | Clarify: A bare spinlock has no ownership (any thread can unlock it). A true mutex enforces that only the thread that called lock() can call unlock(). |
| Claiming leaf-to-root prevents all deadlocks | Address the upgrade() trap: Upgrade acquires downwards, creating a circular wait with lock(). Fix with Global Node ID Ordering or try-lock with backoff. |
Saying x++ is an atomic write | Clarify: x++ is a 3-instruction read-modify-write (MOV, INC, MOV). Without LOCK INC or atomics, it suffers from lost updates. |
| Giving up when hardware atomics are banned | Present Peterson's Algorithm (2 threads) or Lamport's Bakery Algorithm (N threads) as the pure software solution. |
| Not knowing your own code's complexity | Practice deriving O(log_m N) verbally without looking at code |
| Saying "each operation is atomic" without clarifying | Clarify: each SINGLE READ/WRITE is atomic, but a sequence (check-then-set) is NOT |
| Forgetting to mention rollback in the lockedCount approach | Always describe the rollback path -- what happens when a check fails mid-way |
| Not copying the lockedDescendants set before iterating in upgrade | Mention: "I copy the set first because unlocking modifies it -- iterating while modifying is undefined behavior" |
| Panicking when the interviewer says "wrong" or "this has a bug" | Stay calm. Say "Let me trace through this more carefully" |
| Writing code without thinking first | Think out loud for 2-3 minutes before writing. The interviewer values your reasoning process |
| Not asking clarifying questions | Ask about constraints early: "Only lock()? Each read/write is atomic? Can we use any library?" |
16. ๐ช Mental Preparation & Endurance Tips
Before the Interview:
- Sleep well the night before -- some Part B rounds last 8-12 hours
- Eat a proper meal -- you might not get a lunch break
- Keep water and snacks nearby
- Have your code open and ready -- they'll ask you to screen-share your Part A solution
- Practice explaining race conditions OUT LOUD -- hearing yourself say it builds confidence
During the Interview:
- Think out loud -- let the interviewer see your reasoning. Silence feels like you're stuck.
- Start with brute force, then optimize -- show progression
- Don't be afraid to say "I need a minute to think" -- it's better than blurting a wrong answer
- Ask for hints if stuck -- say "Could you give me a direction to think in?" Interviewers want to help.
- If you disagree with the interviewer, say so respectfully -- "I think there might be a case where this breaks. Let me trace through it."
- Stay calm when things go sideways -- unexpected questions, bugs in your code, interviewer challenging you -- it's all part of the process
The Psychological Warfare:
- The interviewer will sometimes deliberately present wrong solutions as correct to test if you push back
- They will sometimes not respond after you point out bugs -- this doesn't mean you're wrong
- They may give you impossible constraints (no atomics, no mutex, no synchronized) -- the point is to see how you reason about the problem, not to reach a perfect solution
- Your goal is to demonstrate understanding, not to write production-ready lock-free code in an interview
After Each Phase:
If there's a break (e.g., "think about this and we'll meet after lunch"):
- Actually think about it -- write notes
- Prepare 2-3 specific interleavings to show when you reconvene
- Review the concepts you were shaky on
Appendix A: Your Part A Code -- Quick Reference for the Interview
When the interviewer opens your submission, this is the structure they'll see. Be ready to explain each part:
YOUR DATA STRUCTURES:
+-- unordered_map<string, int> nameToIndex // O(1) name -> index lookup
+-- vector<vector<int>> adj // adjacency list: adj[i] = children of node i
+-- vector<int> parent // parent[i] = parent of node i (-1 for root)
+-- vector<int> locked // locked[i] = uid or -1
YOUR FUNCTIONS:
+-- buildTree(names, M) // BFS level-order tree construction
+-- checkAncestorLocked(node) // Walk up parent[] checking locked[] != -1
+-- checkDescendantLocked(node) // BFS through adj[] checking locked[] != -1
+-- lock(name, uid)
| +-- lookup index
| +-- check self not locked
| +-- check no ancestor locked (O(log_m N))
| +-- check no descendant locked (O(N))
| +-- set locked[node] = uid
+-- unlock(name, uid)
| +-- check locked[node] == uid
| +-- set locked[node] = -1
+-- upgrade(name, uid)
+-- check self not locked
+-- check >=1 descendant locked
+-- check all locked descendants have same uid
+-- check no ancestor locked
+-- unlock all locked descendants
+-- lock selfAppendix B: The Optimized Version (What to Present When Asked to Improve)
OPTIMIZED DATA STRUCTURES:
+-- unordered_map<string, int> nameToIndex
+-- vector<vector<int>> adj
+-- vector<int> parent
+-- vector<int> locked
+-- vector<unordered_set<int>> lockedDescendants // NEW
|
+-- propagateUp(node, lockedNode, isLocking) // NEW: walk up ancestors, insert/erase
|
+-- lock(name, uid)
| +-- check locked[node] != -1
| +-- check lockedDescendants[node].empty() // O(1) instead of O(N) BFS!
| +-- check no ancestor locked (O(log_m N))
| +-- locked[node] = uid
| +-- propagateUp(parent[node], node, true) // O(log_m N)
|
+-- unlock(name, uid)
| +-- check locked[node] == uid
| +-- locked[node] = -1
| +-- propagateUp(parent[node], node, false) // O(log_m N)
|
+-- upgrade(name, uid)
+-- check self not locked
+-- check lockedDescendants[node] not empty
+-- iterate lockedDescendants[node], check all same uid
+-- check no ancestor locked
+-- copy set -> unlock each -> lock self
+-- Total: O(K * log_m N) where K = locked descendantsAppendix C: Complexity Summary Card
| Operation | Your Original | Optimized | Why |
|---|---|---|---|
| Lock | O(N) | O(log_m N) | Descendant check: O(N)->O(1); Ancestor walk: O(log_m N) |
| Unlock | O(1) | O(log_m N) | Now needs propagateUp to update ancestor sets |
| Upgrade | O(N) | O(K x log_m N) | K descendants, each unlock is O(log_m N) |
| Space | O(N) | O(N) | Additional sets are bounded by total nodes |
| Height of M-ary tree | -- | log_M(N) | Each level has Mx more nodes than previous |
Last updated: August 29, 2026 Tailored to YOUR vector/index-based adjacency list Part A solution. Sources: Real interview experiences from GeeksforGeeks, LeetCode Discuss, Glassdoor, Medium, Unstop -- 2022 through 2026 batches.