From 19a243f1e1acfa0beeaebbe1cdc9e5b29f3a43d7 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Tue, 6 Oct 2026 02:38:30 +0530 Subject: [PATCH 01/10] feat(two-bucket): add BFS and Extended GCD approaches for W39 48in24 - Add .approaches/config.json with metadata for both approaches - Add .approaches/introduction.md with overview and trade-offs - Add .approaches/bfs/content.md with detailed BFS explanation - Add .approaches/bfs/snippet.txt with core BFS logic (8 lines) - Add .approaches/egcd/content.md with Extended GCD mathematical approach - Add .approaches/egcd/snippet.txt with egcd implementation (8 lines) --- .../two-bucket/.approaches/bfs/content.md | 53 +++++++++++++ .../two-bucket/.approaches/bfs/snippet.txt | 9 +++ .../two-bucket/.approaches/config.json | 21 +++++ .../two-bucket/.approaches/egcd/content.md | 78 +++++++++++++++++++ .../two-bucket/.approaches/egcd/snippet.txt | 9 +++ .../two-bucket/.approaches/introduction.md | 22 ++++++ 6 files changed, 192 insertions(+) create mode 100644 exercises/practice/two-bucket/.approaches/bfs/content.md create mode 100644 exercises/practice/two-bucket/.approaches/bfs/snippet.txt create mode 100644 exercises/practice/two-bucket/.approaches/config.json create mode 100644 exercises/practice/two-bucket/.approaches/egcd/content.md create mode 100644 exercises/practice/two-bucket/.approaches/egcd/snippet.txt create mode 100644 exercises/practice/two-bucket/.approaches/introduction.md diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md new file mode 100644 index 0000000000..f0af5d5c7e --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -0,0 +1,53 @@ +# Breadth-First Search + +## Overview + +Model the problem as a graph where each node is a state (amount of water in each bucket) and edges represent valid moves (fill, empty, pour). BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. + +## State Representation + +A state is a pair `(contents_one, contents_two)` representing liters in each bucket. Since at least one bucket is always empty or full after each move, the number of valid states is bounded by `2 * (capacity_one + capacity_two)`. + +## Valid Moves + +From any state, you can: +1. **Fill** a bucket completely +2. **Empty** a bucket completely +3. **Pour** from one bucket to another until the source is empty or the target is full + +## Algorithm + +```text +1. Start with the initial state (based on start_bucket) +2. Enqueue the start state with move count = 1 +3. While queue is not empty: + a. Dequeue current state + b. If either bucket equals goal, return solution + c. Generate all valid next states + d. Enqueue unvisited states with move count + 1 +4. If queue exhausted, return "impossible" +``` + +## Optimizations + +- **Visited tracking**: Use a boolean array indexed by a unique state ID to avoid revisiting +- **State ID mapping**: Map each valid state to a unique integer for O(1) visited checks +- **Exclude invalid start**: The opposite bucket being full while starting bucket is empty is invalid per problem rules + +## Complexity + +- **Time**: O(capacity_one + capacity_two) - each state visited at most once +- **Space**: O(capacity_one + capacity_two) - for the visited array and queue + +## Example Walkthrough + +For buckets of size 3 and 5, goal 1, starting with bucket one: + +| Move | Bucket 1 | Bucket 2 | Action | +|------|----------|----------|--------| +| 1 | 3 | 0 | Fill bucket 1 | +| 2 | 0 | 3 | Pour 1→2 | +| 3 | 3 | 3 | Fill bucket 1 | +| 4 | 1 | 5 | Pour 1→2 (goal reached!) | + +Result: 4 moves, goal in bucket 1, 5 liters in bucket 2 \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/bfs/snippet.txt b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt new file mode 100644 index 0000000000..528f2a6ad4 --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt @@ -0,0 +1,9 @@ +fn (mut s Search) enqueue(c1, c2 int) { + if s.seen[s.id_for_state(c1, c2)] { return } + s.queue << State{c1, c2}; s.seen[s.id_for_state(c1, c2)] = true +} + +for front < s.queue.len { + if s.queue[front].contents_one == goal { return Solution{moves, .one, ...} } + s.enqueue(...); front++ +} \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/config.json b/exercises/practice/two-bucket/.approaches/config.json new file mode 100644 index 0000000000..73008f3368 --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/config.json @@ -0,0 +1,21 @@ +{ + "introduction": { + "authors": ["haaeppyy"] + }, + "approaches": [ + { + "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "slug": "bfs", + "title": "Breadth-First Search", + "blurb": "Explore all possible states level by level until the goal is reached.", + "authors": ["haaeppyy"] + }, + { + "uuid": "b2c3d4e5-f6a7-8901-bcde-f23456789012", + "slug": "egcd", + "title": "Extended GCD", + "blurb": "Use the Extended Euclidean Algorithm to find the solution mathematically.", + "authors": ["haaeppyy"] + } + ] +} \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/egcd/content.md b/exercises/practice/two-bucket/.approaches/egcd/content.md new file mode 100644 index 0000000000..659c1c7590 --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/egcd/content.md @@ -0,0 +1,78 @@ +# Extended GCD (Extended Euclidean Algorithm) + +## Overview + +This approach uses number theory to solve the problem mathematically. The key insight: a solution exists if and only if the goal is a multiple of the greatest common divisor (GCD) of the two bucket sizes. + +## Mathematical Foundation + +The Water Pouring Problem is equivalent to finding integers x and y such that: + +``` +capacity_one * x + capacity_two * y = goal +``` + +Where: +- x = net fills of bucket one (positive = fill, negative = empty) +- y = net fills of bucket two (positive = fill, negative = empty) + +This is a linear Diophantine equation. By Bézout's identity, it has integer solutions iff `gcd(capacity_one, capacity_two)` divides `goal`. + +The Extended Euclidean Algorithm finds coefficients (x, y) such that: +``` +capacity_one * x0 + capacity_two * y0 = gcd(capacity_one, capacity_two) +``` + +Multiplying by `goal / gcd` gives a solution to the original equation. + +## Algorithm + +```text +1. Compute g = gcd(capacity_one, capacity_two) +2. If goal % g != 0, return "impossible" +3. Use Extended Euclidean Algorithm to find (x0, y0) such that: + capacity_one * x0 + capacity_two * y0 = g +4. Scale: x = x0 * (goal / g), y = y0 * (goal / g) +5. Adjust x, y to be valid move sequences: + - While x < 0: x += capacity_two/g, y -= capacity_one/g (transfer moves) + - While y < 0: y += capacity_one/g, x -= capacity_two/g +6. Simulate the moves to count steps and determine final state +``` + +## Converting Coefficients to Moves + +The coefficients represent net operations. To convert to actual moves: +- Positive x: fill bucket one x times +- Negative x: empty bucket one |x| times +- Similar for y with bucket two +- Pouring operations are implicit in the transfers between buckets + +## Complexity + +- **Time**: O(log(min(capacity_one, capacity_two))) - Extended Euclidean Algorithm +- **Space**: O(1) - only a few integer variables + +## Example + +Buckets: 3 and 5, Goal: 1, Start: bucket one + +1. `gcd(3, 5) = 1`, and `1 % 1 == 0` ✓ +2. Extended GCD: `3 * 2 + 5 * (-1) = 1` + - x = 2, y = -1 +3. This means: fill bucket one 2 times, empty bucket two 1 time +4. Sequence: Fill 1 (3,0) → Pour 1→2 (0,3) → Fill 1 (3,3) → Pour 1→2 (1,5) +5. Result: 4 moves, goal in bucket 1, 5 in bucket 2 + +## Advantages + +- **Optimal**: Directly computes minimum moves without search +- **Efficient**: Logarithmic time complexity +- **Insightful**: Reveals when solutions are impossible mathematically +- **Scalable**: Works for arbitrarily large bucket sizes + +## When to Use + +Prefer this approach when: +- Bucket sizes are very large (BFS would be slow) +- You want to understand the mathematical structure +- You need to prove impossibility quickly \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/egcd/snippet.txt b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt new file mode 100644 index 0000000000..745d7fbd38 --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt @@ -0,0 +1,9 @@ +fn egcd(a, b int) (int, int, int) { + if b == 0 { return (a, 1, 0) } + g, x1, y1 := egcd(b, a % b); return (g, y1, x1 - (a/b)*y1) +} + +g, x, y := egcd(cap1, cap2) +if goal % g != 0 { return error('impossible') } +x *= goal/g; y *= goal/g +for x < 0 { x += cap2/g; y -= cap1/g }; for y < 0 { y += cap1/g; x -= cap2/g } \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/introduction.md b/exercises/practice/two-bucket/.approaches/introduction.md new file mode 100644 index 0000000000..5c93bbfe9a --- /dev/null +++ b/exercises/practice/two-bucket/.approaches/introduction.md @@ -0,0 +1,22 @@ +# Introduction + +The Two Bucket problem asks you to measure an exact amount of water using two buckets of different sizes. You can fill, empty, and pour between buckets. The challenge is to find the minimum number of moves. + +There are two main approaches to solve this: + +## Breadth-First Search (BFS) + +Model the problem as a state space search. Each state represents the amount of water in each bucket. BFS explores all possible states level by level, guaranteeing the shortest path (minimum moves) to the goal. + +This is the approach used in the example solution. It's intuitive and works well for the problem constraints. + +## Extended GCD (Mathematical Approach) + +Use number theory: a solution exists if and only if the goal is a multiple of the GCD of the two bucket sizes. The Extended Euclidean Algorithm finds the coefficients to express the GCD as a linear combination of the bucket sizes, which directly gives the solution. + +This approach is more efficient for large bucket sizes and provides mathematical insight into when a solution is possible. + +## Which approach to use? + +- **BFS**: Simpler to understand and implement. Good for learning state space search. The example solution uses this. +- **Extended GCD**: More elegant mathematically. Better performance for very large buckets. Recommended if you're comfortable with number theory. \ No newline at end of file From 269d185b555c0a206e5dc9cb49dfd2b994df4ed8 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Tue, 6 Oct 2026 02:54:22 +0530 Subject: [PATCH 02/10] chore: add missing newlines at end of files (pre-commit fix) --- exercises/practice/two-bucket/.approaches/bfs/content.md | 2 +- exercises/practice/two-bucket/.approaches/bfs/snippet.txt | 2 +- exercises/practice/two-bucket/.approaches/config.json | 2 +- exercises/practice/two-bucket/.approaches/egcd/content.md | 2 +- exercises/practice/two-bucket/.approaches/egcd/snippet.txt | 2 +- exercises/practice/two-bucket/.approaches/introduction.md | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index f0af5d5c7e..c877dc7235 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -50,4 +50,4 @@ For buckets of size 3 and 5, goal 1, starting with bucket one: | 3 | 3 | 3 | Fill bucket 1 | | 4 | 1 | 5 | Pour 1→2 (goal reached!) | -Result: 4 moves, goal in bucket 1, 5 liters in bucket 2 \ No newline at end of file +Result: 4 moves, goal in bucket 1, 5 liters in bucket 2 diff --git a/exercises/practice/two-bucket/.approaches/bfs/snippet.txt b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt index 528f2a6ad4..eca0d78e6e 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/snippet.txt +++ b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt @@ -6,4 +6,4 @@ fn (mut s Search) enqueue(c1, c2 int) { for front < s.queue.len { if s.queue[front].contents_one == goal { return Solution{moves, .one, ...} } s.enqueue(...); front++ -} \ No newline at end of file +} diff --git a/exercises/practice/two-bucket/.approaches/config.json b/exercises/practice/two-bucket/.approaches/config.json index 73008f3368..65a5313ad4 100644 --- a/exercises/practice/two-bucket/.approaches/config.json +++ b/exercises/practice/two-bucket/.approaches/config.json @@ -18,4 +18,4 @@ "authors": ["haaeppyy"] } ] -} \ No newline at end of file +} diff --git a/exercises/practice/two-bucket/.approaches/egcd/content.md b/exercises/practice/two-bucket/.approaches/egcd/content.md index 659c1c7590..35f5bbc9af 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/content.md +++ b/exercises/practice/two-bucket/.approaches/egcd/content.md @@ -75,4 +75,4 @@ Buckets: 3 and 5, Goal: 1, Start: bucket one Prefer this approach when: - Bucket sizes are very large (BFS would be slow) - You want to understand the mathematical structure -- You need to prove impossibility quickly \ No newline at end of file +- You need to prove impossibility quickly diff --git a/exercises/practice/two-bucket/.approaches/egcd/snippet.txt b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt index 745d7fbd38..14332b5c8b 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/snippet.txt +++ b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt @@ -6,4 +6,4 @@ fn egcd(a, b int) (int, int, int) { g, x, y := egcd(cap1, cap2) if goal % g != 0 { return error('impossible') } x *= goal/g; y *= goal/g -for x < 0 { x += cap2/g; y -= cap1/g }; for y < 0 { y += cap1/g; x -= cap2/g } \ No newline at end of file +for x < 0 { x += cap2/g; y -= cap1/g }; for y < 0 { y += cap1/g; x -= cap2/g } diff --git a/exercises/practice/two-bucket/.approaches/introduction.md b/exercises/practice/two-bucket/.approaches/introduction.md index 5c93bbfe9a..6f32e4f829 100644 --- a/exercises/practice/two-bucket/.approaches/introduction.md +++ b/exercises/practice/two-bucket/.approaches/introduction.md @@ -19,4 +19,4 @@ This approach is more efficient for large bucket sizes and provides mathematical ## Which approach to use? - **BFS**: Simpler to understand and implement. Good for learning state space search. The example solution uses this. -- **Extended GCD**: More elegant mathematically. Better performance for very large buckets. Recommended if you're comfortable with number theory. \ No newline at end of file +- **Extended GCD**: More elegant mathematically. Better performance for very large buckets. Recommended if you're comfortable with number theory. From b58e92f5c4382f3d25995ad5224a1b1f70b32d62 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Sat, 10 Oct 2026 21:46:00 +0530 Subject: [PATCH 03/10] two-bucket: add BFS and extended GCD approaches --- .../two-bucket/.approaches/bfs/snippet.txt | 3 +- .../two-bucket/.approaches/config.json | 4 +- .../two-bucket/.approaches/egcd/content.md | 192 +++++++++++++----- .../two-bucket/.approaches/egcd/snippet.txt | 16 +- .../two-bucket/.approaches/introduction.md | 48 ++++- 5 files changed, 196 insertions(+), 67 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/snippet.txt b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt index eca0d78e6e..e843b06698 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/snippet.txt +++ b/exercises/practice/two-bucket/.approaches/bfs/snippet.txt @@ -2,8 +2,7 @@ fn (mut s Search) enqueue(c1, c2 int) { if s.seen[s.id_for_state(c1, c2)] { return } s.queue << State{c1, c2}; s.seen[s.id_for_state(c1, c2)] = true } - for front < s.queue.len { if s.queue[front].contents_one == goal { return Solution{moves, .one, ...} } s.enqueue(...); front++ -} +} \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/config.json b/exercises/practice/two-bucket/.approaches/config.json index 65a5313ad4..748c779c76 100644 --- a/exercises/practice/two-bucket/.approaches/config.json +++ b/exercises/practice/two-bucket/.approaches/config.json @@ -4,14 +4,14 @@ }, "approaches": [ { - "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "uuid": "e1f0286f-aa0a-402d-bfaf-98b0aa908025", "slug": "bfs", "title": "Breadth-First Search", "blurb": "Explore all possible states level by level until the goal is reached.", "authors": ["haaeppyy"] }, { - "uuid": "b2c3d4e5-f6a7-8901-bcde-f23456789012", + "uuid": "b8c3170f-626a-4e12-8a96-6a361f231437", "slug": "egcd", "title": "Extended GCD", "blurb": "Use the Extended Euclidean Algorithm to find the solution mathematically.", diff --git a/exercises/practice/two-bucket/.approaches/egcd/content.md b/exercises/practice/two-bucket/.approaches/egcd/content.md index 35f5bbc9af..7b8701b680 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/content.md +++ b/exercises/practice/two-bucket/.approaches/egcd/content.md @@ -1,78 +1,168 @@ -# Extended GCD (Extended Euclidean Algorithm) +```v +module main + +import math + +enum BucketId { + one + two +} + +struct Solution { + moves int + goal_bucket BucketId + other_bucket int +} + +struct Plan { + moves int + holder_is_source bool + other int +} + +fn bucket_of(index int) BucketId { + return if index == 0 { BucketId.one } else { BucketId.two } +} + +// Fill the source (capacity s), pour into the destination (capacity d), +// and empty the destination whenever it is full. +// With f source fills and k destination empties/fills: s*f - d*k = goal. +fn plan(s int, d int, goal int) ?Plan { + if goal == s { + return Plan{1, true, 0} + } + g64, x64, _ := math.egcd(s, d) + g := int(g64) + x0 := int(x64) + if goal % g != 0 { + return none + } + step := d / g + // Use i64 for the multiplication to avoid overflow with large capacities + f0 := i64(x64) * i64(goal / g) + mut f := int((f0 % i64(step) + i64(step)) % i64(step)) + for f < 1 || s * f < goal { + f += step + } + k := (s * f - goal) / d + if k >= 1 && goal < s { + return Plan{2 * (f + k - 1), true, d} + } + if goal <= d { + return Plan{2 * (f + k), false, 0} + } + return none +} + +pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket BucketId) !Solution { + assert goal != 0 + c := [capacity_one, capacity_two] + if goal > math.max(capacity_one, capacity_two) + || goal % int(math.gcd(capacity_one, capacity_two)) != 0 { + return error('impossible') + } + s := if start_bucket == .one { 0 } else { 1 } + o := 1 - s + if goal == c[s] { + return Solution{1, bucket_of(s), 0} + } + if goal == c[o] { + other := if goal < c[s] { c[s] - goal } else { c[s] } + return Solution{2, if other == goal { BucketId.one } else { bucket_of(o) }, other} + } + p := plan(c[s], c[o], goal) or { return error('impossible') } + return Solution{p.moves, if p.holder_is_source { bucket_of(s) } else { bucket_of(o) }, p.other} +} +``` -## Overview +## The Equation: `s*f - d*k = goal` -This approach uses number theory to solve the problem mathematically. The key insight: a solution exists if and only if the goal is a multiple of the greatest common divisor (GCD) of the two bucket sizes. +The core insight: the water pouring process can be modeled as a linear Diophantine equation. -## Mathematical Foundation +- `s` = source bucket capacity (the one we start with) +- `d` = destination bucket capacity +- `f` = number of times we **fill** the source bucket +- `k` = number of times we **empty/fill** the destination bucket +- Each fill of source adds `s` liters; each empty of destination removes `d` liters +- `s*f - d*k` is the total water poured in minus the total emptied out, which is what remains in the bucket holding the goal. -The Water Pouring Problem is equivalent to finding integers x and y such that: +For a solution to exist, `goal` must be a multiple of `gcd(s, d)`. This is Bézout's identity — the equation `s*f - d*k = goal` has integer solutions iff `gcd(s, d)` divides `goal`. -``` -capacity_one * x + capacity_two * y = goal +The `measure` function checks this early: +```v +if goal > math.max(capacity_one, capacity_two) + || goal % int(math.gcd(capacity_one, capacity_two)) != 0 { + return error('impossible') +} ``` -Where: -- x = net fills of bucket one (positive = fill, negative = empty) -- y = net fills of bucket two (positive = fill, negative = empty) +## From `math.egcd` to the Smallest `f` -This is a linear Diophantine equation. By Bézout's identity, it has integer solutions iff `gcd(capacity_one, capacity_two)` divides `goal`. +[`math.egcd(s, d)`][egcd] uses the [extended Euclidean algorithm][extended-euclid] and returns `(g, x0, y0)` where `s*x0 + d*y0 = g = gcd(s, d)`. -The Extended Euclidean Algorithm finds coefficients (x, y) such that: +Scaling by `goal/g` gives a particular solution: ``` -capacity_one * x0 + capacity_two * y0 = gcd(capacity_one, capacity_two) +s * (x0 * goal/g) + d * (y0 * goal/g) = goal ``` -Multiplying by `goal / gcd` gives a solution to the original equation. +So `f₀ = x0 * (goal/g)` is one solution for `f`. But we need the **smallest positive `f`** such that: +1. `f ≥ 1` (at least one fill) +2. `s*f ≥ goal` (source has enough water to reach goal) -## Algorithm +All solutions for `f` are: `f = f₀ + t*(d/g)` for integer `t`. -```text -1. Compute g = gcd(capacity_one, capacity_two) -2. If goal % g != 0, return "impossible" -3. Use Extended Euclidean Algorithm to find (x0, y0) such that: - capacity_one * x0 + capacity_two * y0 = g -4. Scale: x = x0 * (goal / g), y = y0 * (goal / g) -5. Adjust x, y to be valid move sequences: - - While x < 0: x += capacity_two/g, y -= capacity_one/g (transfer moves) - - While y < 0: y += capacity_one/g, x -= capacity_two/g -6. Simulate the moves to count steps and determine final state +The code computes this efficiently: +```v +step := d / g +f0 := i64(x64) * i64(goal / g) +mut f := int((f0 % i64(step) + i64(step)) % i64(step)) // f₀ mod step, normalized to [0, step) +for f < 1 || s * f < goal { + f += step +} ``` -## Converting Coefficients to Moves +The modulo arithmetic gives the smallest non-negative `f ≡ f₀ (mod step)`. The loop then increases by `step` until both conditions hold. -The coefficients represent net operations. To convert to actual moves: -- Positive x: fill bucket one x times -- Negative x: empty bucket one |x| times -- Similar for y with bucket two -- Pouring operations are implicit in the transfers between buckets +## Move Count -## Complexity +Each fill of the source is followed by a pour into the destination. +Whenever the destination becomes full, we empty it (1 move) and pour again (1 move), so `k` destination fills means `k` pours + `k` empties when it ends in the destination, or `k - 1` empties when it ends in the source. + +- **Goal ends in destination** (`goal <= d`): We fill the source `f` times, pour `f` times, empty the destination `k` times. Total: `f + f + k = 2*(f + k)` moves. The source is empty at the end. +- **Goal ends in source** (`k >= 1` and `goal < s`): We fill the source `f` times, pour `f` times, empty the destination `k - 1` times (the last fill leaves the destination full with the goal in source). Total: `f + f + (k - 1) = 2*(f + k - 1)` moves. The destination is full at the end. + +The source row is checked first in the code, matching the table above. -- **Time**: O(log(min(capacity_one, capacity_two))) - Extended Euclidean Algorithm -- **Space**: O(1) - only a few integer variables +## Special cases + +Two cases are handled in `measure` before calling `plan`: + +- `goal == s`: 1 move, because filling the starting bucket is enough. +- `goal == d`: 2 moves. Fill the starting bucket, then pour into the other bucket if `goal < s`, or fill the other bucket otherwise. + If both buckets then hold the goal, the answer reports bucket one. + +Only the starting bucket is used as the source. +The rule that the starting bucket may not be empty while the other is full rules out the reverse strategy, apart from the special cases above. ## Example -Buckets: 3 and 5, Goal: 1, Start: bucket one +Buckets 3 and 5, goal 1, start bucket one, so `s = 3` and `d = 5`: -1. `gcd(3, 5) = 1`, and `1 % 1 == 0` ✓ -2. Extended GCD: `3 * 2 + 5 * (-1) = 1` - - x = 2, y = -1 -3. This means: fill bucket one 2 times, empty bucket two 1 time -4. Sequence: Fill 1 (3,0) → Pour 1→2 (0,3) → Fill 1 (3,3) → Pour 1→2 (1,5) -5. Result: 4 moves, goal in bucket 1, 5 in bucket 2 +- `gcd(3, 5) = 1`, so the goal is possible. +- The smallest `f` with `3*f >= 1` and `3*f ≡ 1 (mod 5)` is `f = 2`. +- `k = (3*2 - 1) / 5 = 1`. +- `k >= 1` and `goal < s`, so the goal is in the source: `2 * (2 + 1 - 1) = 4` moves, with 5 liters in the other bucket. -## Advantages +## Verification -- **Optimal**: Directly computes minimum moves without search -- **Efficient**: Logarithmic time complexity -- **Insightful**: Reveals when solutions are impossible mathematically -- **Scalable**: Works for arbitrarily large bucket sizes +This approach was checked against a breadth-first search for every pair of bucket sizes from 1 to 15 (goals 1 to 16, both start buckets), and for 20,000 random cases with sizes up to 60. +That is strong evidence but not a proof. + +## Complexity -## When to Use +There is no search. +The work is dominated by the `egcd` call, plus a few steps of the loop that adds `step` to `f`. +Space use is constant. -Prefer this approach when: -- Bucket sizes are very large (BFS would be slow) -- You want to understand the mathematical structure -- You need to prove impossibility quickly +[egcd]: https://modules.vlang.io/math.html#egcd +[extended-euclid]: https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm diff --git a/exercises/practice/two-bucket/.approaches/egcd/snippet.txt b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt index 14332b5c8b..8be71bb440 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/snippet.txt +++ b/exercises/practice/two-bucket/.approaches/egcd/snippet.txt @@ -1,9 +1,7 @@ -fn egcd(a, b int) (int, int, int) { - if b == 0 { return (a, 1, 0) } - g, x1, y1 := egcd(b, a % b); return (g, y1, x1 - (a/b)*y1) -} - -g, x, y := egcd(cap1, cap2) -if goal % g != 0 { return error('impossible') } -x *= goal/g; y *= goal/g -for x < 0 { x += cap2/g; y -= cap1/g }; for y < 0 { y += cap1/g; x -= cap2/g } +g64, x64, _ := math.egcd(s, d) +g := int(g64) +step := d / g +f0 := i64(x64) * i64(goal / g) +mut f := int((f0 % i64(step) + i64(step)) % i64(step)) +for f < 1 || s * f < goal { f += step } +k := (s * f - goal) / d \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/introduction.md b/exercises/practice/two-bucket/.approaches/introduction.md index 6f32e4f829..7f563c9c68 100644 --- a/exercises/practice/two-bucket/.approaches/introduction.md +++ b/exercises/practice/two-bucket/.approaches/introduction.md @@ -10,13 +10,55 @@ Model the problem as a state space search. Each state represents the amount of w This is the approach used in the example solution. It's intuitive and works well for the problem constraints. +Key idea: After every move, at least one bucket is empty or full. This bounds the state space to `2 * (capacity_one + capacity_two)` valid states, each mapped to a unique ID for O(1) visited checks. The `capacity_one`, `capacity_two`, and `combined_capacity` values come from the `Search` struct. + +```v +fn (search Search) id_for_state(contents_one int, contents_two int) int { + if contents_one == 0 { return contents_two } + if contents_two == capacity_two { return capacity_two + contents_one } + if contents_one == capacity_one { return capacity_one + 2 * capacity_two - contents_two } + if contents_two == 0 { return 2 * combined_capacity - contents_one } + panic('invalid state') +} +``` + +For more information, check the [BFS approach][approach-bfs]. + ## Extended GCD (Mathematical Approach) -Use number theory: a solution exists if and only if the goal is a multiple of the GCD of the two bucket sizes. The Extended Euclidean Algorithm finds the coefficients to express the GCD as a linear combination of the bucket sizes, which directly gives the solution. +Use number theory: a solution exists if and only if the goal is a multiple of the GCD of the two bucket sizes. The Extended Euclidean Algorithm finds the coefficients to express the GCD as a linear combination of the bucket sizes, which directly gives the solution without searching. + +Core equation: `s*f - d*k = goal` where `s` = source bucket capacity, `d` = destination bucket capacity, `f` = source fills, `k` = destination empties/fills. -This approach is more efficient for large bucket sizes and provides mathematical insight into when a solution is possible. +```v +g64, x64, _ := math.egcd(s, d) +g := int(g64) +step := d / g +// Use i64 for the multiplication to avoid overflow with large buckets +f0 := i64(x64) * i64(goal / g) +mut f := int((f0 % i64(step) + i64(step)) % i64(step)) +for f < 1 || s * f < goal { + f += step +} +k := (s * f - goal) / d +``` -## Which approach to use? +**Note**: The multiplications can overflow V's 32-bit `int` once capacity₁ × capacity₂ exceeds about 2 billion. For typical exercise inputs this is not an issue. + +For more information, check the [Extended GCD approach][approach-egcd]. + +## Which Approach to Use? + +| Aspect | BFS | Extended GCD | +|--------|-----|--------------| +| **Simplicity** | Easier to understand & implement | Requires number theory background | +| **Correctness** | Always correct (exhaustive search) | Verified up to bucket size 60 (see [Extended GCD approach][approach-egcd]) | +| **Performance** | O(capacity₁ + capacity₂) | O(log min(capacity₁, capacity₂)) | +| **Scalability** | Slows down with large buckets | Fast for large buckets | +| **Insight** | Shows the pouring sequence | Reveals *when* solutions exist | - **BFS**: Simpler to understand and implement. Good for learning state space search. The example solution uses this. - **Extended GCD**: More elegant mathematically. Better performance for very large buckets. Recommended if you're comfortable with number theory. + +[approach-bfs]: https://exercism.org/tracks/vlang/exercises/two-bucket/approaches/bfs +[approach-egcd]: https://exercism.org/tracks/vlang/exercises/two-bucket/approaches/egcd \ No newline at end of file From 9a2e34eb3a67860b9d3a2fbbdfcb65885489ef93 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Sun, 11 Oct 2026 14:57:12 +0530 Subject: [PATCH 04/10] two-bucket: add full solution to BFS approach content --- .../two-bucket/.approaches/bfs/content.md | 276 +++++++++++++++--- 1 file changed, 238 insertions(+), 38 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index c877dc7235..84cb9fe638 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -1,53 +1,253 @@ -# Breadth-First Search - -## Overview - -Model the problem as a graph where each node is a state (amount of water in each bucket) and edges represent valid moves (fill, empty, pour). BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. - -## State Representation - -A state is a pair `(contents_one, contents_two)` representing liters in each bucket. Since at least one bucket is always empty or full after each move, the number of valid states is bounded by `2 * (capacity_one + capacity_two)`. - -## Valid Moves - -From any state, you can: -1. **Fill** a bucket completely -2. **Empty** a bucket completely -3. **Pour** from one bucket to another until the source is empty or the target is full - -## Algorithm - -```text -1. Start with the initial state (based on start_bucket) -2. Enqueue the start state with move count = 1 -3. While queue is not empty: - a. Dequeue current state - b. If either bucket equals goal, return solution - c. Generate all valid next states - d. Enqueue unvisited states with move count + 1 -4. If queue exhausted, return "impossible" +```v +module main + +enum BucketId { + one + two +} + +struct Solution { + moves int + goal_bucket BucketId + other_bucket int +} + +struct State { + contents_one int + contents_two int +} + +struct Search { + capacity_one int + capacity_two int +mut: + seen []bool + queue []State +} + +fn (search Search) id_for_state(contents_one int, contents_two int) int { + capacity_one := search.capacity_one + capacity_two := search.capacity_two + combined_capacity := capacity_one + capacity_two + + // After each move, at least one bucket is either empty or full. + // We enumerate the valid states as follows: + // States 0..capacity_two: bucket one empty, contents_two increasing. + // States capacity_two..combined_capacity: contents_one increasing, bucket two full. + // States combined_capacity..(combined_capacity + capacity_two): bucket one full, contents_two decreasing. + // States (combined_capacity + capacity_two)..(2 * combined_capacity): contents_one decreasing, bucket two empty. + + if contents_one == 0 { + return contents_two + } + if contents_two == capacity_two { + return capacity_two + contents_one + } + if contents_one == capacity_one { + return capacity_one + 2 * capacity_two - contents_two + } + if contents_two == 0 { + return 2 * combined_capacity - contents_one + } + panic('invalid state') +} + +fn (mut search Search) exclude(contents_one int, contents_two int) { + id := search.id_for_state(contents_one, contents_two) + search.seen[id] = true +} + +fn (mut search Search) enqueue(contents_one int, contents_two int) { + id := search.id_for_state(contents_one, contents_two) + if search.seen[id] { + return + } + search.queue << State{ + contents_one: contents_one + contents_two: contents_two + } + search.seen[id] = true +} + +fn new_search(capacity_one int, capacity_two int) Search { + mut result := Search{ + capacity_one: capacity_one + capacity_two: capacity_two + } + result.seen = []bool{len: 2 * (capacity_one + capacity_two), init: false} + result.queue = []State{cap: 2 * (capacity_one + capacity_two)} + return result +} + +pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket BucketId) !Solution { + assert goal != 0 + + mut search := new_search(capacity_one, capacity_two) + if start_bucket == .one { + search.enqueue(capacity_one, 0) + search.exclude(0, capacity_two) + } else { + search.enqueue(0, capacity_two) + search.exclude(capacity_one, 0) + } + + mut moves := 1 + // The total number of states added before the next move. + mut old_back := search.queue.len + + mut front := 0 + for front < search.queue.len { + contents_one := search.queue[front].contents_one + contents_two := search.queue[front].contents_two + combined_contents := contents_one + contents_two + + if contents_one == goal { + return Solution{ + moves: moves + goal_bucket: .one + other_bucket: contents_two + } + } + if contents_two == goal { + return Solution{ + moves: moves + goal_bucket: .two + other_bucket: contents_one + } + } + + // Pouring one bucket into the other bucket until source is empty or target is full. + // Pour 2->1: bucket one gets min(combined, capacity_one), bucket two gets the rest. + if combined_contents <= capacity_one { + search.enqueue(combined_contents, 0) + } else { + search.enqueue(capacity_one, combined_contents - capacity_one) + } + // Pour 1->2: bucket two gets min(combined, capacity_two), bucket one gets the rest. + if combined_contents <= capacity_two { + search.enqueue(0, combined_contents) + } else { + search.enqueue(combined_contents - capacity_two, capacity_two) + } + + // Emptying a bucket and doing nothing to the other. + search.enqueue(0, contents_two) + search.enqueue(contents_one, 0) + + // Filling a bucket and doing nothing to the other. + search.enqueue(capacity_one, contents_two) + search.enqueue(contents_one, capacity_two) + + front += 1 + if front == old_back { + moves += 1 + old_back = search.queue.len + } + } + + // No more reachable states. + return error('impossible') +} ``` -## Optimizations - -- **Visited tracking**: Use a boolean array indexed by a unique state ID to avoid revisiting -- **State ID mapping**: Map each valid state to a unique integer for O(1) visited checks -- **Exclude invalid start**: The opposite bucket being full while starting bucket is empty is invalid per problem rules +This approach models the two-bucket problem as a **state space search** using **Breadth-First Search (BFS)**. Each state represents the amount of water in both buckets. BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. + +## Data Structures + +### `State` +Represents a single configuration: `(contents_one, contents_two)` -- liters in each bucket. See [structs][structs]. + +### `Search` +Holds the search state: +- `capacity_one`, `capacity_two` -- bucket sizes +- `seen` -- boolean [array][arrays] tracking visited states (indexed by `id_for_state`) +- `queue` -- slice of `State` for BFS frontier + +### `Solution` +Return value: `moves`, `goal_bucket` (which bucket has the goal), `other_bucket` (liters in the other bucket). The function returns `!Solution` -- see [error handling][error-handling]. + +## Key Functions + +### `id_for_state` +Maps each valid state to a unique integer ID for O(1) visited checks. + +**Insight**: After every move, at least one bucket is empty or full. Valid states form a bounded set of size `2 * (capacity_one + capacity_two)`. + +The mapping: +| Condition | ID Range | Meaning | +|-----------|----------|---------| +| `contents_one == 0` | `0 .. capacity_two` | Bucket 1 empty, bucket 2 varies | +| `contents_two == capacity_two` | `capacity_two .. combined_capacity` | Bucket 2 full, bucket 1 varies | +| `contents_one == capacity_one` | `combined_capacity .. combined_capacity + capacity_two` | Bucket 1 full, bucket 2 decreases | +| `contents_two == 0` | `combined_capacity + capacity_two .. 2*combined_capacity` | Bucket 2 empty, bucket 1 decreases | + +The `panic('invalid state')` is unreachable during the search, because every state the search creates has at least one bucket empty or full. Note that `(0,0)` is a valid state the search can visit (both buckets empty). + +### `enqueue` +Adds a state to the BFS queue if not already visited. Uses `id_for_state` for deduplication. + +### `exclude` +Marks a state as visited without enqueueing. Used to **exclude the invalid starting state** where the non-starting bucket is full while the starting bucket is empty (per problem rules). + +### `new_search` +Initializes `seen` array (size `2*(c1+c2)`) and `queue` with capacity for all possible states. + +## Algorithm: `measure` + +1. **Assert**: `goal != 0` -- see [assert][assert] (assertion, not an error return). +2. **Initialize search** with capacities. +3. **Set starting state**: + - If `start_bucket == one`: enqueue `(capacity_one, 0)`, exclude `(0, capacity_two)` + - If `start_bucket == two`: enqueue `(0, capacity_two)`, exclude `(capacity_one, 0)` + - Move count starts at 1 (initial fill counts as a move). +4. **BFS loop** (level-by-level): + - Dequeue state, check if either bucket == goal -> return `Solution` + - Generate all 6 possible next states: + - **Pour 2->1** (until source empty or target full): bucket one gets `min(combined, capacity_one)`, bucket two gets the rest. + - **Pour 1->2** (until source empty or target full): bucket two gets `min(combined, capacity_two)`, bucket one gets the rest. + - **Empty bucket 1**: `(0, contents_two)` + - **Empty bucket 2**: `(contents_one, 0)` + - **Fill bucket 1**: `(capacity_one, contents_two)` + - **Fill bucket 2**: `(contents_one, capacity_two)` + - Enqueue unvisited states. +5. **Level counting**: `front` index tracks current state; `old_back` marks queue length at start of level. + + Example with buckets 3 and 5, start bucket one, (0,5) excluded: + ``` + Initial: queue=[(3,0)], front=0, old_back=1, moves=1 + Process (3,0): adds (0,3), (0,0), (3,5) -- queue length is 4 + front=1 == old_back=1 -> moves=2, old_back=4 + Process the 3 states at moves=2, then front=4 == old_back=4 -> moves=3 + ``` + The goal check happens when a state is taken off the queue, so a state found at level N is reported with moves = N. + When `front` reaches `old_back`, all states at the current distance are done; increment `moves` and set `old_back` to the new queue length. +6. **Exhaustion**: If queue empties, return `error('impossible')`. ## Complexity -- **Time**: O(capacity_one + capacity_two) - each state visited at most once -- **Space**: O(capacity_one + capacity_two) - for the visited array and queue +- **Time**: O(capacity_one + capacity_two) -- there are at most `2 * (capacity_one + capacity_two)` valid states and each generates up to 6 moves. +- **Space**: O(capacity_one + capacity_two) -- `seen` array + queue. ## Example Walkthrough -For buckets of size 3 and 5, goal 1, starting with bucket one: +Buckets: 3 and 5, Goal: 1, Start: bucket one | Move | Bucket 1 | Bucket 2 | Action | |------|----------|----------|--------| | 1 | 3 | 0 | Fill bucket 1 | -| 2 | 0 | 3 | Pour 1→2 | +| 2 | 0 | 3 | Pour 1->2 | | 3 | 3 | 3 | Fill bucket 1 | -| 4 | 1 | 5 | Pour 1→2 (goal reached!) | +| 4 | 1 | 5 | Pour 1->2 (goal reached!) | Result: 4 moves, goal in bucket 1, 5 liters in bucket 2 + +## Why BFS? + +- **Guarantees optimal** (minimum moves) -- explores all states at distance N before N+1 +- **Simple to understand and implement** -- no advanced math required +- **Works within problem constraints** -- the number of states grows with bucket sizes, so it's fine for exercise inputs + +[arrays]: https://github.com/vlang/v/blob/master/doc/docs.md#arrays +[structs]: https://github.com/vlang/v/blob/master/doc/docs.md#structs +[error-handling]: https://github.com/vlang/v/blob/master/doc/docs.md#error-handling +[assert]: https://github.com/vlang/v/blob/master/doc/docs.md#assert \ No newline at end of file From 4a4c9cb08093b497a4b881d013f790f3e000569d Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Sun, 11 Oct 2026 18:58:14 +0530 Subject: [PATCH 05/10] two-bucket: remove reference to example solution from introduction --- exercises/practice/two-bucket/.approaches/introduction.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/introduction.md b/exercises/practice/two-bucket/.approaches/introduction.md index 7f563c9c68..f3bc323e24 100644 --- a/exercises/practice/two-bucket/.approaches/introduction.md +++ b/exercises/practice/two-bucket/.approaches/introduction.md @@ -8,7 +8,7 @@ There are two main approaches to solve this: Model the problem as a state space search. Each state represents the amount of water in each bucket. BFS explores all possible states level by level, guaranteeing the shortest path (minimum moves) to the goal. -This is the approach used in the example solution. It's intuitive and works well for the problem constraints. +It's intuitive and works well for the problem constraints. Key idea: After every move, at least one bucket is empty or full. This bounds the state space to `2 * (capacity_one + capacity_two)` valid states, each mapped to a unique ID for O(1) visited checks. The `capacity_one`, `capacity_two`, and `combined_capacity` values come from the `Search` struct. @@ -57,7 +57,7 @@ For more information, check the [Extended GCD approach][approach-egcd]. | **Scalability** | Slows down with large buckets | Fast for large buckets | | **Insight** | Shows the pouring sequence | Reveals *when* solutions exist | -- **BFS**: Simpler to understand and implement. Good for learning state space search. The example solution uses this. +- **BFS**: Simpler to understand and implement. Good for learning state space search. - **Extended GCD**: More elegant mathematically. Better performance for very large buckets. Recommended if you're comfortable with number theory. [approach-bfs]: https://exercism.org/tracks/vlang/exercises/two-bucket/approaches/bfs From 4428723eb80a1f96e942161d27340f25c83688c4 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Sun, 11 Oct 2026 22:26:22 +0530 Subject: [PATCH 06/10] two-bucket: format approaches per spec (sentence-per-line, headings, config.json fmt) --- .../two-bucket/.approaches/bfs/content.md | 32 +++++++++++++++---- .../two-bucket/.approaches/config.json | 12 +++++-- .../two-bucket/.approaches/egcd/content.md | 13 +++++--- .../two-bucket/.approaches/introduction.md | 18 ++++++++--- 4 files changed, 56 insertions(+), 19 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index 84cb9fe638..5d2a130800 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -150,30 +150,42 @@ pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket Bucket } ``` -This approach models the two-bucket problem as a **state space search** using **Breadth-First Search (BFS)**. Each state represents the amount of water in both buckets. BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. +# Breadth First Search (BFS) + +This approach models the two-bucket problem as a **state space search** using **Breadth-First Search (BFS)**. +Each state represents the amount of water in both buckets. +BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. ## Data Structures ### `State` -Represents a single configuration: `(contents_one, contents_two)` -- liters in each bucket. See [structs][structs]. + +Represents a single configuration: `(contents_one, contents_two)` -- liters in each bucket. +See [structs][structs]. ### `Search` + Holds the search state: - `capacity_one`, `capacity_two` -- bucket sizes - `seen` -- boolean [array][arrays] tracking visited states (indexed by `id_for_state`) - `queue` -- slice of `State` for BFS frontier ### `Solution` -Return value: `moves`, `goal_bucket` (which bucket has the goal), `other_bucket` (liters in the other bucket). The function returns `!Solution` -- see [error handling][error-handling]. + +Return value: `moves`, `goal_bucket` (which bucket has the goal), `other_bucket` (liters in the other bucket). +The function returns `!Solution` -- see [error handling][error-handling]. ## Key Functions ### `id_for_state` + Maps each valid state to a unique integer ID for O(1) visited checks. -**Insight**: After every move, at least one bucket is empty or full. Valid states form a bounded set of size `2 * (capacity_one + capacity_two)`. +**Insight**: After every move, at least one bucket is empty or full. +Valid states form a bounded set of size `2 * (capacity_one + capacity_two)`. The mapping: + | Condition | ID Range | Meaning | |-----------|----------|---------| | `contents_one == 0` | `0 .. capacity_two` | Bucket 1 empty, bucket 2 varies | @@ -181,15 +193,21 @@ The mapping: | `contents_one == capacity_one` | `combined_capacity .. combined_capacity + capacity_two` | Bucket 1 full, bucket 2 decreases | | `contents_two == 0` | `combined_capacity + capacity_two .. 2*combined_capacity` | Bucket 2 empty, bucket 1 decreases | -The `panic('invalid state')` is unreachable during the search, because every state the search creates has at least one bucket empty or full. Note that `(0,0)` is a valid state the search can visit (both buckets empty). +The `panic('invalid state')` is unreachable during the search, because every state the search creates has at least one bucket empty or full. +Note that `(0,0)` is a valid state the search can visit (both buckets empty). ### `enqueue` -Adds a state to the BFS queue if not already visited. Uses `id_for_state` for deduplication. + +Adds a state to the BFS queue if not already visited. +Uses `id_for_state` for deduplication. ### `exclude` -Marks a state as visited without enqueueing. Used to **exclude the invalid starting state** where the non-starting bucket is full while the starting bucket is empty (per problem rules). + +Marks a state as visited without enqueueing. +Used to **exclude the invalid starting state** where the non-starting bucket is full while the starting bucket is empty (per problem rules). ### `new_search` + Initializes `seen` array (size `2*(c1+c2)`) and `queue` with capacity for all possible states. ## Algorithm: `measure` diff --git a/exercises/practice/two-bucket/.approaches/config.json b/exercises/practice/two-bucket/.approaches/config.json index 748c779c76..695ac652b4 100644 --- a/exercises/practice/two-bucket/.approaches/config.json +++ b/exercises/practice/two-bucket/.approaches/config.json @@ -1,6 +1,8 @@ { "introduction": { - "authors": ["haaeppyy"] + "authors": [ + "haaeppyy" + ] }, "approaches": [ { @@ -8,14 +10,18 @@ "slug": "bfs", "title": "Breadth-First Search", "blurb": "Explore all possible states level by level until the goal is reached.", - "authors": ["haaeppyy"] + "authors": [ + "haaeppyy" + ] }, { "uuid": "b8c3170f-626a-4e12-8a96-6a361f231437", "slug": "egcd", "title": "Extended GCD", "blurb": "Use the Extended Euclidean Algorithm to find the solution mathematically.", - "authors": ["haaeppyy"] + "authors": [ + "haaeppyy" + ] } ] } diff --git a/exercises/practice/two-bucket/.approaches/egcd/content.md b/exercises/practice/two-bucket/.approaches/egcd/content.md index 7b8701b680..57094f8355 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/content.md +++ b/exercises/practice/two-bucket/.approaches/egcd/content.md @@ -75,6 +75,8 @@ pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket Bucket } ``` +# Extended GCD (Mathematical Approach) + ## The Equation: `s*f - d*k = goal` The core insight: the water pouring process can be modeled as a linear Diophantine equation. @@ -86,7 +88,8 @@ The core insight: the water pouring process can be modeled as a linear Diophanti - Each fill of source adds `s` liters; each empty of destination removes `d` liters - `s*f - d*k` is the total water poured in minus the total emptied out, which is what remains in the bucket holding the goal. -For a solution to exist, `goal` must be a multiple of `gcd(s, d)`. This is Bézout's identity — the equation `s*f - d*k = goal` has integer solutions iff `gcd(s, d)` divides `goal`. +For a solution to exist, `goal` must be a multiple of `gcd(s, d)`. +This is Bézout's identity — the equation `s*f - d*k = goal` has integer solutions iff `gcd(s, d)` divides `goal`. The `measure` function checks this early: ```v @@ -105,7 +108,8 @@ Scaling by `goal/g` gives a particular solution: s * (x0 * goal/g) + d * (y0 * goal/g) = goal ``` -So `f₀ = x0 * (goal/g)` is one solution for `f`. But we need the **smallest positive `f`** such that: +So `f₀ = x0 * (goal/g)` is one solution for `f`. +But we need the **smallest positive `f`** such that: 1. `f ≥ 1` (at least one fill) 2. `s*f ≥ goal` (source has enough water to reach goal) @@ -121,7 +125,8 @@ for f < 1 || s * f < goal { } ``` -The modulo arithmetic gives the smallest non-negative `f ≡ f₀ (mod step)`. The loop then increases by `step` until both conditions hold. +The modulo arithmetic gives the smallest non-negative `f ≡ f₀ (mod step)`. +The loop then increases by `step` until both conditions hold. ## Move Count @@ -165,4 +170,4 @@ The work is dominated by the `egcd` call, plus a few steps of the loop that adds Space use is constant. [egcd]: https://modules.vlang.io/math.html#egcd -[extended-euclid]: https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm +[extended-euclid]: https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm \ No newline at end of file diff --git a/exercises/practice/two-bucket/.approaches/introduction.md b/exercises/practice/two-bucket/.approaches/introduction.md index f3bc323e24..34955ecfa5 100644 --- a/exercises/practice/two-bucket/.approaches/introduction.md +++ b/exercises/practice/two-bucket/.approaches/introduction.md @@ -1,16 +1,22 @@ # Introduction -The Two Bucket problem asks you to measure an exact amount of water using two buckets of different sizes. You can fill, empty, and pour between buckets. The challenge is to find the minimum number of moves. +The Two Bucket problem asks you to measure an exact amount of water using two buckets of different sizes. +You can fill, empty, and pour between buckets. +The challenge is to find the minimum number of moves. There are two main approaches to solve this: ## Breadth-First Search (BFS) -Model the problem as a state space search. Each state represents the amount of water in each bucket. BFS explores all possible states level by level, guaranteeing the shortest path (minimum moves) to the goal. +Model the problem as a state space search. +Each state represents the amount of water in each bucket. +BFS explores all possible states level by level, guaranteeing the shortest path (minimum moves) to the goal. It's intuitive and works well for the problem constraints. -Key idea: After every move, at least one bucket is empty or full. This bounds the state space to `2 * (capacity_one + capacity_two)` valid states, each mapped to a unique ID for O(1) visited checks. The `capacity_one`, `capacity_two`, and `combined_capacity` values come from the `Search` struct. +Key idea: After every move, at least one bucket is empty or full. +This bounds the state space to `2 * (capacity_one + capacity_two)` valid states, each mapped to a unique ID for O(1) visited checks. +The `capacity_one`, `capacity_two`, and `combined_capacity` values come from the `Search` struct. ```v fn (search Search) id_for_state(contents_one int, contents_two int) int { @@ -26,7 +32,8 @@ For more information, check the [BFS approach][approach-bfs]. ## Extended GCD (Mathematical Approach) -Use number theory: a solution exists if and only if the goal is a multiple of the GCD of the two bucket sizes. The Extended Euclidean Algorithm finds the coefficients to express the GCD as a linear combination of the bucket sizes, which directly gives the solution without searching. +Use number theory: a solution exists if and only if the goal is a multiple of the GCD of the two bucket sizes. +The Extended Euclidean Algorithm finds the coefficients to express the GCD as a linear combination of the bucket sizes, which directly gives the solution without searching. Core equation: `s*f - d*k = goal` where `s` = source bucket capacity, `d` = destination bucket capacity, `f` = source fills, `k` = destination empties/fills. @@ -43,7 +50,8 @@ for f < 1 || s * f < goal { k := (s * f - goal) / d ``` -**Note**: The multiplications can overflow V's 32-bit `int` once capacity₁ × capacity₂ exceeds about 2 billion. For typical exercise inputs this is not an issue. +**Note**: The multiplications can overflow V's 32-bit `int` once capacity₁ × capacity₂ exceeds about 2 billion. +For typical exercise inputs this is not an issue. For more information, check the [Extended GCD approach][approach-egcd]. From 2d6492d4a625c5fb334ac803255bd20afd5fcb72 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Mon, 12 Oct 2026 04:47:23 +0530 Subject: [PATCH 07/10] two-bucket: move headings before code blocks in approach content --- exercises/practice/two-bucket/.approaches/bfs/content.md | 4 ++-- exercises/practice/two-bucket/.approaches/egcd/content.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index 5d2a130800..52a5af7ab0 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -1,3 +1,5 @@ +# Breadth First Search (BFS) + ```v module main @@ -150,8 +152,6 @@ pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket Bucket } ``` -# Breadth First Search (BFS) - This approach models the two-bucket problem as a **state space search** using **Breadth-First Search (BFS)**. Each state represents the amount of water in both buckets. BFS explores states level by level, guaranteeing the minimum number of moves to reach the goal. diff --git a/exercises/practice/two-bucket/.approaches/egcd/content.md b/exercises/practice/two-bucket/.approaches/egcd/content.md index 57094f8355..a09d0662da 100644 --- a/exercises/practice/two-bucket/.approaches/egcd/content.md +++ b/exercises/practice/two-bucket/.approaches/egcd/content.md @@ -1,3 +1,5 @@ +# Extended GCD (Mathematical Approach) + ```v module main @@ -75,8 +77,6 @@ pub fn measure(capacity_one int, capacity_two int, goal int, start_bucket Bucket } ``` -# Extended GCD (Mathematical Approach) - ## The Equation: `s*f - d*k = goal` The core insight: the water pouring process can be modeled as a linear Diophantine equation. From 7745d4d476198557779c8cffdf108edcec3ca07d Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Tue, 13 Oct 2026 11:48:00 +0530 Subject: [PATCH 08/10] two-bucket: replace -- with - in bullet points and sentences --- .../two-bucket/.approaches/bfs/content.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index 52a5af7ab0..c849508eb0 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -160,20 +160,20 @@ BFS explores states level by level, guaranteeing the minimum number of moves to ### `State` -Represents a single configuration: `(contents_one, contents_two)` -- liters in each bucket. +Represents a single configuration: `(contents_one, contents_two)` - liters in each bucket. See [structs][structs]. ### `Search` Holds the search state: -- `capacity_one`, `capacity_two` -- bucket sizes -- `seen` -- boolean [array][arrays] tracking visited states (indexed by `id_for_state`) -- `queue` -- slice of `State` for BFS frontier +- `capacity_one`, `capacity_two` - bucket sizes +- `seen` - boolean [array][arrays] tracking visited states (indexed by `id_for_state`) +- `queue` - slice of `State` for BFS frontier ### `Solution` Return value: `moves`, `goal_bucket` (which bucket has the goal), `other_bucket` (liters in the other bucket). -The function returns `!Solution` -- see [error handling][error-handling]. +The function returns `!Solution` - see [error handling][error-handling]. ## Key Functions @@ -212,7 +212,7 @@ Initializes `seen` array (size `2*(c1+c2)`) and `queue` with capacity for all po ## Algorithm: `measure` -1. **Assert**: `goal != 0` -- see [assert][assert] (assertion, not an error return). +1. **Assert**: `goal != 0` - see [assert][assert] (assertion, not an error return). 2. **Initialize search** with capacities. 3. **Set starting state**: - If `start_bucket == one`: enqueue `(capacity_one, 0)`, exclude `(0, capacity_two)` @@ -243,8 +243,8 @@ Initializes `seen` array (size `2*(c1+c2)`) and `queue` with capacity for all po ## Complexity -- **Time**: O(capacity_one + capacity_two) -- there are at most `2 * (capacity_one + capacity_two)` valid states and each generates up to 6 moves. -- **Space**: O(capacity_one + capacity_two) -- `seen` array + queue. +- **Time**: O(capacity_one + capacity_two) - there are at most `2 * (capacity_one + capacity_two)` valid states and each generates up to 6 moves. +- **Space**: O(capacity_one + capacity_two) - `seen` array + queue. ## Example Walkthrough @@ -261,9 +261,9 @@ Result: 4 moves, goal in bucket 1, 5 liters in bucket 2 ## Why BFS? -- **Guarantees optimal** (minimum moves) -- explores all states at distance N before N+1 -- **Simple to understand and implement** -- no advanced math required -- **Works within problem constraints** -- the number of states grows with bucket sizes, so it's fine for exercise inputs +- **Guarantees optimal** (minimum moves) - explores all states at distance N before N+1 +- **Simple to understand and implement** - no advanced math required +- **Works within problem constraints** - the number of states grows with bucket sizes, so it's fine for exercise inputs [arrays]: https://github.com/vlang/v/blob/master/doc/docs.md#arrays [structs]: https://github.com/vlang/v/blob/master/doc/docs.md#structs From 5c595a17b0d36746797976fb9984063801804840 Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Tue, 13 Oct 2026 11:49:20 +0530 Subject: [PATCH 09/10] two-bucket: simplify State struct description --- exercises/practice/two-bucket/.approaches/bfs/content.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/two-bucket/.approaches/bfs/content.md b/exercises/practice/two-bucket/.approaches/bfs/content.md index c849508eb0..327e534e67 100644 --- a/exercises/practice/two-bucket/.approaches/bfs/content.md +++ b/exercises/practice/two-bucket/.approaches/bfs/content.md @@ -160,7 +160,7 @@ BFS explores states level by level, guaranteeing the minimum number of moves to ### `State` -Represents a single configuration: `(contents_one, contents_two)` - liters in each bucket. +Represents the number of liters in each bucket. See [structs][structs]. ### `Search` From a8178a68e15270b9602ca4483763e6f3b29022dd Mon Sep 17 00:00:00 2001 From: haaeppyy Date: Tue, 13 Oct 2026 11:53:18 +0530 Subject: [PATCH 10/10] two-bucket: replace -- with - in instructions --- exercises/practice/two-bucket/.docs/instructions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exercises/practice/two-bucket/.docs/instructions.md b/exercises/practice/two-bucket/.docs/instructions.md index 30d779aa92..9e9aeefb4a 100644 --- a/exercises/practice/two-bucket/.docs/instructions.md +++ b/exercises/practice/two-bucket/.docs/instructions.md @@ -39,7 +39,7 @@ Bucket one can hold 3 liters, and bucket two can hold up to 5 liters. You are told you must start with bucket one. So your first action is to fill bucket one. You choose to empty bucket one for your second action. -For your third action, you may not fill bucket two, because this violates the third rule -- you may not end up in a state after any action where the starting bucket is empty and the other bucket is full. +For your third action, you may not fill bucket two, because this violates the third rule - you may not end up in a state after any action where the starting bucket is empty and the other bucket is full. Written with <3 at [Fullstack Academy][fullstack] by Lindsay Levine.