262 lines
13 KiB
YAML
262 lines
13 KiB
YAML
title: Building H2O
|
|
slug: building-h2o
|
|
difficulty: medium
|
|
leetcode_id: 1117
|
|
leetcode_url: https://leetcode.com/problems/building-h2o/
|
|
categories:
|
|
- concurrency
|
|
patterns:
|
|
- synchronization
|
|
|
|
function_signature: "class H2O"
|
|
|
|
test_cases:
|
|
visible:
|
|
- input: { water: "HOH" }
|
|
expected: "HHO"
|
|
- input: { water: "OOHHHH" }
|
|
expected: "HHOHHO"
|
|
hidden:
|
|
- input: { water: "HHOOHH" }
|
|
expected: "HHOOHH"
|
|
- input: { water: "HOHOHOHOHOHO" }
|
|
expected: "HHOHHOHHOHHO"
|
|
- input: { water: "HHO" }
|
|
expected: "HHO"
|
|
- input: { water: "OHH" }
|
|
expected: "HHO"
|
|
- input: { water: "OOOHHHHHH" }
|
|
expected: "HHOHHOHHO"
|
|
- input: { water: "HHHHHHOOO" }
|
|
expected: "HHOHHOHHO"
|
|
|
|
description: |
|
|
There are two kinds of threads: `oxygen` and `hydrogen`. Your goal is to group these threads to form water molecules.
|
|
|
|
There is a barrier where each thread has to wait until a complete molecule can be formed. Hydrogen and oxygen threads will be given `releaseHydrogen` and `releaseOxygen` methods respectively, which will allow them to pass the barrier. These threads should pass the barrier in groups of three, and they must immediately bond with each other to form a water molecule.
|
|
|
|
You must guarantee that all the threads from one molecule bond **before** any other threads from the next molecule do.
|
|
|
|
In other words:
|
|
|
|
- If an oxygen thread arrives at the barrier when no hydrogen threads are present, it must wait for two hydrogen threads.
|
|
- If a hydrogen thread arrives at the barrier when no other threads are present, it must wait for an oxygen thread and another hydrogen thread.
|
|
|
|
We do not have to worry about matching the threads up explicitly; the threads do not necessarily know which other threads they are paired up with. The key is that threads pass the barriers in complete sets; thus, if we examine the sequence of threads that bond and divide them into groups of three, each group should contain one oxygen and two hydrogen threads.
|
|
|
|
Write synchronization code for oxygen and hydrogen molecules that enforces these constraints.
|
|
|
|
constraints: |
|
|
- `3 * n == water.length`
|
|
- `1 <= n <= 20`
|
|
- `water[i]` is either `'H'` or `'O'`
|
|
- There will be exactly `2 * n` `'H'` in `water`
|
|
- There will be exactly `n` `'O'` in `water`
|
|
|
|
examples:
|
|
- input: 'water = "HOH"'
|
|
output: '"HHO"'
|
|
explanation: '"HOH" and "OHH" are also valid answers.'
|
|
- input: 'water = "OOHHHH"'
|
|
output: '"HHOHHO"'
|
|
explanation: '"HOHHHO", "OHHHHO", "HHOHOH", "HOHHOH", "OHHHOH", "HHOOHH", "HOHOHH" and "OHHOHH" are also valid answers.'
|
|
|
|
explanation:
|
|
intuition: |
|
|
Think of this problem like a chemistry experiment where you need to assemble water molecules (H<sub>2</sub>O) on an assembly line.
|
|
|
|
Each water molecule requires exactly **two hydrogen atoms and one oxygen atom**. Imagine atoms (threads) arriving at a bonding station one by one in random order. The challenge is to hold each atom at the gate until exactly the right combination arrives, then release them together as a complete molecule.
|
|
|
|
The key insight is that we need a **coordination mechanism** that:
|
|
1. Counts how many hydrogen and oxygen atoms are waiting
|
|
2. Only allows atoms to proceed when a complete molecule (2H + 1O) can be formed
|
|
3. Ensures atoms from one molecule all bond before the next molecule starts forming
|
|
|
|
This is a classic **barrier synchronization** problem. We use semaphores (or similar synchronization primitives) as "tokens" — hydrogen threads need to acquire hydrogen tokens, oxygen threads need oxygen tokens, and we carefully control when these tokens become available.
|
|
|
|
approach: |
|
|
We solve this using **Semaphores with Barrier Synchronization**:
|
|
|
|
**Step 1: Set up synchronization primitives**
|
|
|
|
- `hydrogen_semaphore`: Controls how many hydrogen threads can proceed. Initially `2` (a molecule needs 2 hydrogens)
|
|
- `oxygen_semaphore`: Controls how many oxygen threads can proceed. Initially `1` (a molecule needs 1 oxygen)
|
|
- `barrier`: A barrier that waits for exactly 3 threads before releasing them all together
|
|
|
|
|
|
|
|
**Step 2: Implement the hydrogen method**
|
|
|
|
- Acquire a permit from `hydrogen_semaphore` (blocks if no permits available)
|
|
- Call `releaseHydrogen()` to output 'H'
|
|
- Wait at the barrier for the other 2 threads in this molecule
|
|
- After the barrier releases, if this is the last thread (determined by barrier return value), replenish the semaphores for the next molecule
|
|
|
|
|
|
|
|
**Step 3: Implement the oxygen method**
|
|
|
|
- Acquire a permit from `oxygen_semaphore` (blocks if no permits available)
|
|
- Call `releaseOxygen()` to output 'O'
|
|
- Wait at the barrier for the other 2 threads in this molecule
|
|
- After the barrier releases, if this is the last thread, replenish the semaphores for the next molecule
|
|
|
|
|
|
|
|
**Step 4: Reset for next molecule**
|
|
|
|
- The barrier has a reset mechanism — when all 3 threads pass through, one of them (the "winner") is responsible for releasing 2 hydrogen permits and 1 oxygen permit for the next molecule
|
|
|
|
|
|
|
|
The semaphores ensure the correct ratio (2H:1O), and the barrier ensures all three atoms of a molecule bond together before the next molecule can start forming.
|
|
|
|
common_pitfalls:
|
|
- title: Race Conditions Without Proper Synchronization
|
|
description: |
|
|
Without proper synchronization, threads can interleave incorrectly. For example, you might get output like "HHHHO" where 3 hydrogens bond before any oxygen.
|
|
|
|
Using simple counters with locks is error-prone because you need to handle the "wait until condition" pattern. Semaphores and barriers are purpose-built for this.
|
|
wrong_approach: "Using plain counters and busy-waiting"
|
|
correct_approach: "Use semaphores to control thread admission and barriers to synchronize release"
|
|
|
|
- title: Forgetting to Reset for Next Molecule
|
|
description: |
|
|
After a molecule is formed, the semaphores are exhausted (0 permits remaining). If you forget to replenish them, all subsequent threads will block forever.
|
|
|
|
The key is to detect when a complete molecule has passed the barrier, then add back 2 hydrogen permits and 1 oxygen permit.
|
|
wrong_approach: "Not replenishing semaphore permits after each molecule"
|
|
correct_approach: "Use barrier's return value to identify one thread responsible for resetting permits"
|
|
|
|
- title: Deadlock from Incorrect Semaphore Order
|
|
description: |
|
|
If you try to acquire multiple semaphores in different orders across threads, you risk deadlock. In this problem, each thread type only needs one semaphore, so this is less of a concern, but the pattern is important.
|
|
|
|
The barrier also helps prevent deadlock by ensuring all threads proceed together.
|
|
|
|
- title: Not Understanding Barrier Semantics
|
|
description: |
|
|
A barrier blocks threads until a specified number (3 in this case) have arrived. Then it releases all of them simultaneously. This is perfect for our "bond together" requirement.
|
|
|
|
In Python's `threading.Barrier`, the `wait()` method returns an integer from `0` to `parties-1`, with exactly one thread receiving `0`. This can be used to designate one thread to perform cleanup/reset actions.
|
|
|
|
key_takeaways:
|
|
- "**Semaphores for counting resources**: Use semaphores when you need to limit how many threads can access a resource (here, 2 hydrogen slots and 1 oxygen slot per molecule)"
|
|
- "**Barriers for synchronization points**: When multiple threads must reach a point before any can continue, barriers are the right tool"
|
|
- "**Producer-consumer with quotas**: This problem is a variant where the 'quota' is the molecular formula H<sub>2</sub>O"
|
|
- "**Thread coordination patterns**: This classic problem teaches synchronization primitives that apply to many real-world scenarios like connection pooling, batch processing, and resource allocation"
|
|
|
|
time_complexity: "O(n). Each of the `3n` threads performs constant-time operations (semaphore acquire/release, barrier wait)."
|
|
space_complexity: "O(1). We use a fixed number of synchronization primitives regardless of input size."
|
|
|
|
solutions:
|
|
- approach_name: Semaphores with Barrier
|
|
is_optimal: true
|
|
code: |
|
|
from threading import Semaphore, Barrier
|
|
|
|
class H2O:
|
|
def __init__(self):
|
|
# Allow 2 hydrogen threads to proceed per molecule
|
|
self.hydrogen_sem = Semaphore(2)
|
|
# Allow 1 oxygen thread to proceed per molecule
|
|
self.oxygen_sem = Semaphore(1)
|
|
# Wait for 3 threads (2H + 1O) before releasing
|
|
self.barrier = Barrier(3)
|
|
|
|
def hydrogen(self, releaseHydrogen: 'Callable[[], None]') -> None:
|
|
# Wait for a hydrogen slot to be available
|
|
self.hydrogen_sem.acquire()
|
|
|
|
# releaseHydrogen() outputs "H". Do not change or remove this line.
|
|
releaseHydrogen()
|
|
|
|
# Wait at barrier for the other atoms in this molecule
|
|
# Returns 0 for exactly one thread (the "winner")
|
|
winner = self.barrier.wait()
|
|
|
|
# One thread resets permits for the next molecule
|
|
if winner == 0:
|
|
self.hydrogen_sem.release()
|
|
self.hydrogen_sem.release()
|
|
self.oxygen_sem.release()
|
|
|
|
def oxygen(self, releaseOxygen: 'Callable[[], None]') -> None:
|
|
# Wait for an oxygen slot to be available
|
|
self.oxygen_sem.acquire()
|
|
|
|
# releaseOxygen() outputs "O". Do not change or remove this line.
|
|
releaseOxygen()
|
|
|
|
# Wait at barrier for the other atoms in this molecule
|
|
winner = self.barrier.wait()
|
|
|
|
# One thread resets permits for the next molecule
|
|
if winner == 0:
|
|
self.hydrogen_sem.release()
|
|
self.hydrogen_sem.release()
|
|
self.oxygen_sem.release()
|
|
explanation: |
|
|
**Time Complexity:** O(1) per thread — Each thread does constant work (acquire, release, barrier wait).
|
|
|
|
**Space Complexity:** O(1) — Fixed number of synchronization primitives.
|
|
|
|
The semaphores enforce the 2:1 ratio of hydrogen to oxygen, while the barrier ensures all three atoms of a molecule bond together. After each molecule forms, one thread replenishes the semaphores for the next molecule.
|
|
|
|
- approach_name: Condition Variables
|
|
is_optimal: false
|
|
code: |
|
|
from threading import Lock, Condition
|
|
|
|
class H2O:
|
|
def __init__(self):
|
|
self.lock = Lock()
|
|
self.condition = Condition(self.lock)
|
|
# Track how many of each type are waiting
|
|
self.hydrogen_count = 0
|
|
self.oxygen_count = 0
|
|
|
|
def hydrogen(self, releaseHydrogen: 'Callable[[], None]') -> None:
|
|
with self.condition:
|
|
# Wait until we can add a hydrogen to a molecule
|
|
# Need: hydrogen_count < 2 (room for more H)
|
|
while self.hydrogen_count >= 2:
|
|
self.condition.wait()
|
|
|
|
self.hydrogen_count += 1
|
|
|
|
# releaseHydrogen() outputs "H"
|
|
releaseHydrogen()
|
|
|
|
# Check if molecule is complete (2H + 1O)
|
|
if self.hydrogen_count == 2 and self.oxygen_count == 1:
|
|
# Reset for next molecule
|
|
self.hydrogen_count = 0
|
|
self.oxygen_count = 0
|
|
# Wake up waiting threads for next molecule
|
|
self.condition.notify_all()
|
|
|
|
def oxygen(self, releaseOxygen: 'Callable[[], None]') -> None:
|
|
with self.condition:
|
|
# Wait until we can add an oxygen to a molecule
|
|
while self.oxygen_count >= 1:
|
|
self.condition.wait()
|
|
|
|
self.oxygen_count += 1
|
|
|
|
# releaseOxygen() outputs "O"
|
|
releaseOxygen()
|
|
|
|
# Check if molecule is complete
|
|
if self.hydrogen_count == 2 and self.oxygen_count == 1:
|
|
# Reset for next molecule
|
|
self.hydrogen_count = 0
|
|
self.oxygen_count = 0
|
|
self.condition.notify_all()
|
|
explanation: |
|
|
**Time Complexity:** O(1) per thread — Each thread does constant work.
|
|
|
|
**Space Complexity:** O(1) — Fixed counters and synchronization objects.
|
|
|
|
This approach uses condition variables to coordinate threads. Each thread waits until there's room for its type in the current molecule, then checks if the molecule is complete. While this works, it's more verbose and easier to get wrong than the semaphore approach. The semaphore solution more directly expresses the "quota" concept.
|