1117. Building H2O
Problem Description
This is a thread synchronization problem where you need to coordinate threads representing hydrogen (H) and oxygen (O) atoms to form water molecules (H₂O).
The problem requires implementing a class H2O
with two methods:
hydrogen()
: Called by hydrogen threadsoxygen()
: Called by oxygen threads
Key Requirements:
-
Molecule Formation: Threads must group in sets of exactly 3 - two hydrogen threads and one oxygen thread - to form one water molecule.
-
Barrier Synchronization: Threads must wait at a barrier until a complete molecule can be formed:
- If an oxygen thread arrives when there are fewer than 2 hydrogen threads waiting, it must wait
- If a hydrogen thread arrives when there's no oxygen thread or only one hydrogen thread, it must wait
-
Atomic Bonding: Once three threads (2H + 1O) are available, they must all "bond" together before any threads from the next molecule can proceed. This ensures molecules form completely without mixing threads from different molecules.
-
Output Order: While the exact pairing doesn't matter, the output must always show groups of three with the pattern of 2 H's and 1 O (in any order within each group).
Example Scenarios:
- If threads arrive in order H, H, O → they can immediately form H₂O
- If threads arrive in order O, H, H → the O waits, then all three proceed together
- If threads arrive in order H, O, H → the first H waits, then all three proceed together
The solution uses semaphores to control access:
self.h = Semaphore(2)
: Allows up to 2 hydrogen threads to proceedself.o = Semaphore(0)
: Initially blocks oxygen threads
The hydrogen method acquires one H permit, releases hydrogen, and when both H permits are taken (meaning 2 hydrogens are ready), it releases the oxygen semaphore. The oxygen method waits for permission, releases oxygen, then releases 2 hydrogen permits for the next molecule formation.
Intuition
The key insight is recognizing that we need to control the "capacity" of each type of thread that can proceed at any given time. Since a water molecule needs exactly 2 hydrogen atoms and 1 oxygen atom, we can think of this as having "slots" available for each type.
Initially, we should allow 2 hydrogen threads to enter because that's how many we need for one molecule. But we shouldn't let any oxygen thread proceed until we have those 2 hydrogens ready. This naturally leads us to think about semaphores - they're perfect for controlling how many threads can access a resource.
The clever part is understanding the cycle:
- Start by allowing 2 H threads (initialize H semaphore with 2)
- Block all O threads initially (initialize O semaphore with 0)
- When both H slots are filled, we know we have our 2 hydrogens ready, so we can signal the oxygen to proceed
- After the oxygen executes, it should "reset" the system for the next molecule by releasing 2 new H permits
The "handoff" mechanism is crucial: the second hydrogen thread (when self.h._value == 0
) acts as the trigger to release the oxygen. This ensures we always have exactly 2 H's before any O can proceed. Then the oxygen thread, after executing, becomes responsible for enabling the next pair of hydrogens.
This creates a repeating pattern: H, H, O → H, H, O → H, H, O...
The beauty of this approach is that it doesn't matter in what order the threads arrive. The semaphores naturally queue them up and release them in valid groups of three. If an oxygen arrives first, it waits. If only one hydrogen arrives, it takes one slot and waits for another hydrogen to fill the second slot before they collectively release the oxygen.
Solution Approach
The solution uses semaphores as the primary synchronization mechanism. Let's walk through the implementation step by step:
Data Structure Initialization
def __init__(self):
self.h = Semaphore(2) # Allow 2 hydrogen threads initially
self.o = Semaphore(0) # Block all oxygen threads initially
self.h
: Controls hydrogen thread access with initial value 2 (since we need 2 H atoms per molecule)self.o
: Controls oxygen thread access with initial value 0 (oxygen must wait for hydrogens first)
Hydrogen Thread Logic
def hydrogen(self, releaseHydrogen):
self.h.acquire() # Try to get one of the 2 hydrogen permits
releaseHydrogen() # Output "H"
if self.h._value == 0: # Check if both H permits are taken
self.o.release() # Signal oxygen thread to proceed
Step-by-step execution:
acquire()
decrements the hydrogen semaphore. If the value was > 0, the thread proceeds; otherwise it blocks- Once acquired, the hydrogen atom is "released" (outputs "H")
- The critical check: if
self.h._value == 0
, it means both hydrogen slots are filled (2 hydrogens are ready) - When both hydrogens are ready, release the oxygen semaphore to allow one oxygen thread to proceed
Oxygen Thread Logic
def oxygen(self, releaseOxygen):
self.o.acquire() # Wait for signal from hydrogen threads
releaseOxygen() # Output "O"
self.h.release(2) # Reset: allow 2 new hydrogen threads
Step-by-step execution:
acquire()
on oxygen semaphore - this thread blocks until the second hydrogen signals it- Once unblocked, the oxygen atom is "released" (outputs "O")
release(2)
on hydrogen semaphore adds 2 permits back, allowing the next pair of hydrogen threads to start forming the next molecule
The Synchronization Pattern
The implementation creates a cyclic barrier pattern:
- Phase 1: Two hydrogen threads enter (consuming both H permits)
- Phase 2: Second hydrogen signals the oxygen thread
- Phase 3: Oxygen thread executes and resets the system for the next molecule
This ensures that threads always group in sets of three (2H + 1O) before any thread from the next molecule can proceed. The semaphores naturally handle queueing - if extra threads arrive, they wait in the semaphore's internal queue until their turn comes in the next molecule formation cycle.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example where 6 threads arrive in the order: H₁, O₁, H₂, O₂, H₃, H₄
Initial State:
self.h = Semaphore(2)
- can accept 2 hydrogen threadsself.o = Semaphore(0)
- blocks all oxygen threads
Step 1: H₁ arrives
- H₁ calls
self.h.acquire()
→ succeeds (semaphore: 2→1) - H₁ outputs "H"
- H₁ checks
self.h._value == 1
(not 0) → doesn't release oxygen - H₁ completes
Step 2: O₁ arrives
- O₁ calls
self.o.acquire()
→ BLOCKS (semaphore value is 0) - O₁ waits in queue...
Step 3: H₂ arrives
- H₂ calls
self.h.acquire()
→ succeeds (semaphore: 1→0) - H₂ outputs "H"
- H₂ checks
self.h._value == 0
→ YES! Both H slots filled - H₂ calls
self.o.release()
→ unblocks O₁ - H₂ completes
Step 4: O₁ unblocks and executes
- O₁ resumes from
self.o.acquire()
- O₁ outputs "O"
- O₁ calls
self.h.release(2)
→ resets H semaphore (0→2) - O₁ completes
- First molecule complete: "HHO"
Step 5: O₂ arrives
- O₂ calls
self.o.acquire()
→ BLOCKS (semaphore back to 0) - O₂ waits in queue...
Step 6: H₃ arrives
- H₃ calls
self.h.acquire()
→ succeeds (semaphore: 2→1) - H₃ outputs "H"
- H₃ checks
self.h._value == 1
(not 0) → doesn't release oxygen - H₃ completes
Step 7: H₄ arrives
- H₄ calls
self.h.acquire()
→ succeeds (semaphore: 1→0) - H₄ outputs "H"
- H₄ checks
self.h._value == 0
→ YES! Both H slots filled - H₄ calls
self.o.release()
→ unblocks O₂ - H₄ completes
Step 8: O₂ unblocks and executes
- O₂ resumes from
self.o.acquire()
- O₂ outputs "O"
- O₂ calls
self.h.release(2)
→ resets for next molecule - O₂ completes
- Second molecule complete: "HHO"
Final Output: "HHOHHO" (two complete water molecules)
The key observation is how the semaphores enforce the grouping: even though O₁ arrived before H₂, it had to wait until two hydrogens were ready. Similarly, O₂ waited for H₃ and H₄ to both arrive before proceeding. The pattern ensures threads always group in valid sets of three.
Solution Implementation
1from threading import Semaphore
2from typing import Callable
3
4
5class H2O:
6 def __init__(self):
7 # Semaphore to control hydrogen threads (initially allows 2 hydrogens)
8 self.hydrogen_semaphore = Semaphore(2)
9 # Semaphore to control oxygen threads (initially blocks oxygen)
10 self.oxygen_semaphore = Semaphore(0)
11 # Counter to track how many hydrogen atoms have been released
12 self.hydrogen_count = 0
13
14 def hydrogen(self, releaseHydrogen: Callable[[], None]) -> None:
15 """
16 Method called by hydrogen threads.
17 Two hydrogen threads must combine with one oxygen thread to form H2O.
18 """
19 # Wait for permission to release a hydrogen atom
20 self.hydrogen_semaphore.acquire()
21
22 # releaseHydrogen() outputs "H". Do not change or remove this line.
23 releaseHydrogen()
24
25 # Increment the hydrogen counter
26 self.hydrogen_count += 1
27
28 # If two hydrogens have been released, signal oxygen to proceed
29 if self.hydrogen_count == 2:
30 self.hydrogen_count = 0 # Reset counter for next molecule
31 self.oxygen_semaphore.release()
32
33 def oxygen(self, releaseOxygen: Callable[[], None]) -> None:
34 """
35 Method called by oxygen threads.
36 One oxygen thread must wait for two hydrogen threads before forming H2O.
37 """
38 # Wait for two hydrogen atoms to be available
39 self.oxygen_semaphore.acquire()
40
41 # releaseOxygen() outputs "O". Do not change or remove this line.
42 releaseOxygen()
43
44 # Allow two more hydrogen atoms to be released for the next molecule
45 self.hydrogen_semaphore.release()
46 self.hydrogen_semaphore.release()
47
1class H2O {
2 // Semaphore to control hydrogen threads (initially allows 2 H atoms)
3 private Semaphore hydrogenSemaphore = new Semaphore(2);
4
5 // Semaphore to control oxygen threads (initially blocks, waiting for 2 H atoms)
6 private Semaphore oxygenSemaphore = new Semaphore(0);
7
8 /**
9 * Constructor for H2O synchronization class
10 */
11 public H2O() {
12 }
13
14 /**
15 * Method called by hydrogen threads to form water molecules
16 * Ensures 2 hydrogen atoms are paired with 1 oxygen atom
17 *
18 * @param releaseHydrogen Runnable that outputs "H"
19 * @throws InterruptedException if thread is interrupted while waiting
20 */
21 public void hydrogen(Runnable releaseHydrogen) throws InterruptedException {
22 // Acquire permit to release hydrogen (max 2 before oxygen is needed)
23 hydrogenSemaphore.acquire();
24
25 // Output "H" - represents releasing a hydrogen atom
26 releaseHydrogen.run();
27
28 // Signal that one hydrogen is ready (oxygen needs 2 signals to proceed)
29 oxygenSemaphore.release();
30 }
31
32 /**
33 * Method called by oxygen threads to form water molecules
34 * Waits for 2 hydrogen atoms before proceeding
35 *
36 * @param releaseOxygen Runnable that outputs "O"
37 * @throws InterruptedException if thread is interrupted while waiting
38 */
39 public void oxygen(Runnable releaseOxygen) throws InterruptedException {
40 // Wait for 2 hydrogen atoms to be available
41 oxygenSemaphore.acquire(2);
42
43 // Output "O" - represents releasing an oxygen atom
44 releaseOxygen.run();
45
46 // Allow 2 more hydrogen atoms to be processed for next water molecule
47 hydrogenSemaphore.release(2);
48 }
49}
50
1#include <semaphore.h>
2#include <functional>
3
4class H2O {
5private:
6 sem_t hydrogen_semaphore; // Controls how many hydrogen threads can proceed
7 sem_t oxygen_semaphore; // Controls when oxygen thread can proceed
8 int hydrogen_count; // Tracks number of hydrogen atoms ready
9
10public:
11 H2O() {
12 // Initialize semaphores
13 // Allow 2 hydrogen threads initially (need 2 H for each H2O molecule)
14 sem_init(&hydrogen_semaphore, 0, 2);
15
16 // Block oxygen thread initially (need 2 H before O can proceed)
17 sem_init(&oxygen_semaphore, 0, 0);
18
19 // Initialize hydrogen counter
20 hydrogen_count = 0;
21 }
22
23 void hydrogen(std::function<void()> releaseHydrogen) {
24 // Wait for permission to release hydrogen
25 // This blocks if 2 hydrogen threads are already waiting/processing
26 sem_wait(&hydrogen_semaphore);
27
28 // releaseHydrogen() outputs "H". Do not change or remove this line.
29 releaseHydrogen();
30
31 // Increment counter to track hydrogen atoms
32 ++hydrogen_count;
33
34 // Check if we have 2 hydrogen atoms ready
35 if (hydrogen_count == 2) {
36 // Signal oxygen thread that it can now proceed
37 // We have enough hydrogen atoms to form H2O
38 sem_post(&oxygen_semaphore);
39 }
40 }
41
42 void oxygen(std::function<void()> releaseOxygen) {
43 // Wait until 2 hydrogen atoms are ready
44 // This blocks until hydrogen_count reaches 2
45 sem_wait(&oxygen_semaphore);
46
47 // releaseOxygen() outputs "O". Do not change or remove this line.
48 releaseOxygen();
49
50 // Reset hydrogen counter for next molecule formation
51 hydrogen_count = 0;
52
53 // Allow 2 new hydrogen threads to proceed for next H2O molecule
54 sem_post(&hydrogen_semaphore);
55 sem_post(&hydrogen_semaphore);
56 }
57};
58
1// Semaphore-like counters for thread synchronization
2let hydrogenSemaphore: number = 2; // Controls how many hydrogen threads can proceed (initially 2)
3let oxygenSemaphore: number = 0; // Controls when oxygen thread can proceed (initially blocked)
4let hydrogenCount: number = 0; // Tracks number of hydrogen atoms ready for molecule formation
5
6// Mutex locks for thread-safe operations
7let hydrogenLock: boolean = false; // Lock for hydrogen thread critical section
8let oxygenLock: boolean = false; // Lock for oxygen thread critical section
9
10// Wait function to simulate semaphore wait operation
11async function wait(ms: number): Promise<void> {
12 return new Promise(resolve => setTimeout(resolve, ms));
13}
14
15// Hydrogen thread function - releases hydrogen atom for H2O formation
16async function hydrogen(releaseHydrogen: () => void): Promise<void> {
17 // Wait until hydrogen semaphore allows this thread to proceed
18 // This simulates sem_wait(&hydrogen_semaphore)
19 while (hydrogenSemaphore <= 0) {
20 await wait(1);
21 }
22
23 // Acquire lock for critical section
24 while (hydrogenLock) {
25 await wait(1);
26 }
27 hydrogenLock = true;
28
29 // Decrement semaphore counter (consuming one permit)
30 hydrogenSemaphore--;
31
32 // Release hydrogen atom - outputs "H"
33 releaseHydrogen();
34
35 // Increment counter to track hydrogen atoms ready
36 hydrogenCount++;
37
38 // Check if we have 2 hydrogen atoms ready for molecule formation
39 if (hydrogenCount === 2) {
40 // Signal oxygen thread that it can now proceed
41 // We have enough hydrogen atoms to form H2O
42 oxygenSemaphore++;
43 }
44
45 // Release lock
46 hydrogenLock = false;
47}
48
49// Oxygen thread function - releases oxygen atom for H2O formation
50async function oxygen(releaseOxygen: () => void): Promise<void> {
51 // Wait until 2 hydrogen atoms are ready
52 // This simulates sem_wait(&oxygen_semaphore)
53 while (oxygenSemaphore <= 0) {
54 await wait(1);
55 }
56
57 // Acquire lock for critical section
58 while (oxygenLock) {
59 await wait(1);
60 }
61 oxygenLock = true;
62
63 // Decrement semaphore counter (consuming one permit)
64 oxygenSemaphore--;
65
66 // Release oxygen atom - outputs "O"
67 releaseOxygen();
68
69 // Reset hydrogen counter for next molecule formation
70 hydrogenCount = 0;
71
72 // Allow 2 new hydrogen threads to proceed for next H2O molecule
73 // This simulates sem_post(&hydrogen_semaphore) twice
74 hydrogenSemaphore += 2;
75
76 // Release lock
77 oxygenLock = false;
78}
79
Time and Space Complexity
Time Complexity: O(1)
per operation
Each method (hydrogen
and oxygen
) performs a constant number of operations:
hydrogen
: One semaphore acquire, one function call, one conditional check, and potentially one semaphore releaseoxygen
: One semaphore acquire, one function call, and one semaphore release with count 2
All semaphore operations (acquire
and release
) are O(1)
in terms of the operation itself, though threads may block waiting for synchronization. The blocking time depends on thread scheduling and arrival order, not on input size.
Space Complexity: O(1)
The space usage is constant regardless of how many times the methods are called:
- Two semaphore objects (
self.h
andself.o
) - Each semaphore internally maintains a counter and a queue for waiting threads
- The queue size is bounded by the number of threads that can be blocked at once (at most 2 hydrogen threads waiting)
The space does not grow with the number of method invocations, making it constant space complexity.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Race Condition in Hydrogen Counter Check
The Pitfall:
The code shown uses a shared hydrogen_count
variable that's accessed and modified by multiple hydrogen threads without proper synchronization:
self.hydrogen_count += 1 if self.hydrogen_count == 2: self.hydrogen_count = 0 self.oxygen_semaphore.release()
This creates a race condition. Consider this scenario:
- Thread H1 increments counter (now 1)
- Thread H2 increments counter (now 2)
- Thread H2 checks if count == 2 (true)
- Thread H1 also checks if count == 2 (true)
- Both threads might release the oxygen semaphore, causing two oxygen threads to proceed instead of one!
The Solution: Use the semaphore's internal state directly without additional shared variables:
def hydrogen(self, releaseHydrogen):
self.hydrogen_semaphore.acquire()
releaseHydrogen()
# Check if this is the second hydrogen (both permits taken)
if self.hydrogen_semaphore._value == 0:
self.oxygen_semaphore.release()
2. Incorrect Semaphore Release Order
The Pitfall: Some implementations try to release permits before outputting the atom:
def oxygen(self, releaseOxygen):
self.oxygen_semaphore.acquire()
self.hydrogen_semaphore.release(2) # Wrong: releasing before output
releaseOxygen()
This breaks the barrier synchronization - new hydrogen threads could start processing before the current molecule completes formation.
The Solution: Always output the atom first, then release permits for the next molecule:
def oxygen(self, releaseOxygen):
self.oxygen_semaphore.acquire()
releaseOxygen() # Output first
self.hydrogen_semaphore.release(2) # Then allow next molecule
3. Using Mutex/Lock Instead of Semaphores
The Pitfall: Attempting to use a simple Lock or Mutex:
def __init__(self):
self.lock = Lock()
self.h_count = 0
def hydrogen(self, releaseHydrogen):
with self.lock:
while self.h_count >= 2:
# Deadlock! Can't wait while holding the lock
pass
releaseHydrogen()
self.h_count += 1
This leads to deadlock because threads can't wait for conditions while holding the lock.
The Solution: Use counting semaphores which naturally handle waiting and counting:
- Semaphores allow threads to block without holding exclusive access
- They maintain internal counters that handle the "2 hydrogens, 1 oxygen" requirement elegantly
4. Forgetting to Handle Thread Arrival Order
The Pitfall: Assuming threads will arrive in a convenient order (like H, H, O repeatedly):
def hydrogen(self, releaseHydrogen):
releaseHydrogen() # Just output immediately
# Assumes oxygen will coordinate - wrong!
The Solution: The semaphore pattern handles any arrival order:
- If O arrives first, it blocks on
oxygen_semaphore.acquire()
- If H arrives first, it proceeds but ensures O waits until 2 H's are ready
- The pattern self-organizes regardless of thread arrival timing
Problem: Given a list of tasks and a list of requirements, compute a sequence of tasks that can be performed, such that we complete every task once while satisfying all the requirements.
Which of the following method should we use to solve this problem?
Recommended Readings
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Runtime Overview When learning about algorithms and data structures you'll frequently encounter the term time complexity This concept is fundamental in computer science and offers insights into how long an algorithm takes to complete given a certain input size What is Time Complexity Time complexity represents the amount of time
Want a Structured Path to Master System Design Too? Don’t Miss This!