Facebook Pixel

1117. Building H2O

MediumConcurrency
Leetcode Link

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 threads
  • oxygen(): Called by oxygen threads

Key Requirements:

  1. Molecule Formation: Threads must group in sets of exactly 3 - two hydrogen threads and one oxygen thread - to form one water molecule.

  2. 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
  3. 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.

  4. 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 proceed
  • self.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.

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

Intuition

The key insight is recognizing that we need to 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:

  1. Start by allowing 2 H threads (initialize H semaphore with 2)
  2. Block all O threads initially (initialize O semaphore with 0)
  3. When both H slots are filled, we know we have our 2 hydrogens ready, so we can signal the oxygen to proceed
  4. 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:

  1. acquire() decrements the hydrogen semaphore. If the value was > 0, the thread proceeds; otherwise it blocks
  2. Once acquired, the hydrogen atom is "released" (outputs "H")
  3. The critical check: if self.h._value == 0, it means both hydrogen slots are filled (2 hydrogens are ready)
  4. 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:

  1. acquire() on oxygen semaphore - this thread blocks until the second hydrogen signals it
  2. Once unblocked, the oxygen atom is "released" (outputs "O")
  3. 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 Evaluator

Example 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 threads
  • self.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 == 0YES! 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 == 0YES! 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 release
  • oxygen: 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 and self.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
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

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

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

Load More