Two Bucket in C: Complete Solution & Deep Dive Guide

a close up of a computer screen with code on it

Mastering Algorithmic Thinking: The Definitive Guide to Solving the Two Bucket Problem in C

The Two Bucket Problem is a classic logic puzzle that challenges you to measure a specific amount of liquid using only two buckets of different, fixed capacities. This guide provides a complete C solution, breaking down the state management, simulation logic, and core programming concepts required to solve it efficiently.


Have you ever been captivated by a brain teaser that seems simple on the surface but hides a surprising depth of logic? The Two Bucket problem is exactly that—a puzzle so iconic it even made a memorable appearance in the movie Die Hard with a Vengeance. It's a challenge that forces you to think in discrete steps, managing a limited set of actions to reach a precise goal.

Many aspiring programmers get stuck not on the C syntax, but on how to translate the real-world actions of filling, emptying, and pouring into a reliable algorithm. You might find your code getting stuck in an infinite loop or failing to handle edge cases correctly. This guide promises to demystify the process, providing you with a robust C implementation and a deep understanding of the state-based logic that powers the solution. We will transform this daunting puzzle into a clear, solvable programming exercise.


What Exactly is the Two Bucket Problem?

The Two Bucket Problem is a well-known puzzle in mathematics and computer science. The premise is straightforward: you are given two buckets, each with a specific maximum capacity (e.g., a 3-liter bucket and a 5-liter bucket). Your objective is to measure out an exact target amount of liquid (e.g., 4 liters) that is not equal to the capacity of either bucket.

To achieve this, you can perform a limited set of actions:

  • Fill: Completely fill one of the buckets from an infinite source.
  • Empty: Completely empty the contents of one bucket.
  • Pour: Pour the contents of one bucket into the other until the source bucket is empty or the destination bucket is full, whichever comes first.

The challenge lies in finding the correct sequence of these actions to reach the goal state. The solution must also determine which bucket contains the target amount and the total number of actions (or moves) it took. This problem is an excellent exercise in state-space search, where each state is defined by the amount of liquid in each bucket, and each action represents a transition from one state to another.


Why This Puzzle is a Cornerstone of Algorithmic Learning

At its core, the Two Bucket problem isn't just about water; it's a perfect, self-contained model for teaching fundamental computer science concepts. It forces developers to move beyond simple, linear code and embrace the idea of state machines and simulations. Understanding how to model and solve this puzzle provides a strong foundation for tackling more complex problems in areas like game development, resource allocation, and AI pathfinding.

Solving it in a low-level language like C adds another layer of learning. You must manage memory and data structures manually, reinforcing your understanding of pointers, structs, and control flow. The logic required—tracking current levels, calculating pour amounts, and checking for goal conditions—is a microcosm of the kind of detailed, step-by-step processing that is central to systems programming. This specific challenge from the kodikra C learning path is designed to sharpen these exact skills.


How to Model and Solve the Problem in C

The key to solving this puzzle programmatically is to stop thinking about physical buckets and start thinking about data and states. A "state" is simply a snapshot of the system at any given moment. In our case, a state can be defined by the amount of water in bucket one and the amount of water in bucket two.

Our goal is to write a C program that simulates the actions, transitioning from an initial state (both buckets empty) to a goal state (one of the buckets containing the target amount). We'll use a simple loop that continues until the goal is met, incrementing a counter for each action performed.

Defining the State and Actions

First, we need a way to represent the buckets and their properties. A struct in C is perfect for this. We'll define a structure to hold all the necessary information for our simulation.


// Represents the configuration of the two buckets and the goal.
typedef struct {
    int capacity_1;      // Max capacity of bucket one
    int capacity_2;      // Max capacity of bucket two
    int goal;            // Target amount of liquid
    const char *start_bucket; // Which bucket to fill first ("one" or "two")
} bucket_puzzle_t;

// Represents the result of the simulation.
typedef struct {
    bool possible;       // Was the goal reachable?
    int move_count;      // Total moves taken
    const char *goal_bucket; // Which bucket holds the goal amount?
    int other_bucket_level; // Liquid level in the other bucket
} bucket_result_t;

The simulation will run in a loop, performing one action per iteration. The logic must decide which action to take based on the current state of the two buckets. For example, if the starting bucket is empty, the first action must be to fill it. If the other bucket is full, the next logical action might be to empty it to make space.

The Simulation Logic Flow

The algorithm iteratively applies the rules until a solution is found. It's a deterministic process, meaning that for a given starting condition, the sequence of moves is always the same.

    ● Start
    │
    ▼
  ┌───────────────────────┐
  │ Initialize Buckets    │
  │ (Both are 0)          │
  │ Initialize Move Count │
  └──────────┬────────────┘
             │
             ▼
  ┌───────────────────────┐
  │ Loop (while goal not met) │
  ├───────────────────────┤
  │                       │
  │   ◆ Is starting bucket empty?
  │  ╱         ╲
  │ Yes         No
  │  │           │
  │  ▼           ▼
  │ Fill it   ◆ Is goal in other bucket?
  │            ╱         ╲
  │           Yes         No
  │            │           │
  │            ▼           ▼
  │         Empty it   ◆ Is starting bucket full?
  │                     ╱         ╲
  │                    Yes         No
  │                     │           │
  │                     ▼           ▼
  │                   Pour into   Fill starting
  │                   other bucket  bucket
  │                       │
  │                       ▼
  │      Increment Move Count
  │                       │
  └──────────┬────────────┘
             │
             ▼
    ◆ Goal Met?
   ╱           ╲
 Yes             No (Impossible)
  │               │
  ▼               ▼
[Return Result]  [Return Impossible]
  │
  ▼
 ● End

This flow chart illustrates the core decision-making process within each step of our simulation. It's a chain of conditional logic that determines the next best action based on the current water levels.

The Complete C Solution

Here is the full implementation in C. The code is structured to be readable and follows the logic we've outlined. Pay close attention to the comments, as they explain the purpose of each block of code.


#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <math.h>

// Define a structure for the input parameters of the puzzle.
typedef struct {
    int capacity_1;
    int capacity_2;
    int goal;
    const char *start_bucket;
} bucket_puzzle_t;

// Define a structure for the output/result of the puzzle.
typedef struct {
    bool possible;
    int move_count;
    const char *goal_bucket;
    int other_bucket_level;
} bucket_result_t;

// Helper function to calculate the Greatest Common Divisor (GCD).
// A solution is only possible if the goal is a multiple of the GCD of the two bucket capacities.
int gcd(int a, int b) {
    while (b) {
        int temp = b;
        b = a % b;
        a = temp;
    }
    return a;
}

// The main function to solve the two-bucket puzzle.
bucket_result_t bucket_solve(const bucket_puzzle_t *puzzle) {
    // Pre-computation check: If the goal is not divisible by the GCD of capacities, it's impossible.
    if (puzzle->goal % gcd(puzzle->capacity_1, puzzle->capacity_2) != 0) {
        return (bucket_result_t){.possible = false};
    }
    // Pre-computation check: If goal is larger than both capacities, impossible unless goal equals a capacity.
    if (puzzle->goal > puzzle->capacity_1 && puzzle->goal > puzzle->capacity_2) {
         return (bucket_result_t){.possible = false};
    }

    int cap1 = puzzle->capacity_1;
    int cap2 = puzzle->capacity_2;
    const char* start_node = puzzle->start_bucket;

    int level1 = 0;
    int level2 = 0;
    int moves = 0;

    // Determine which bucket is the primary (starting) and secondary.
    int* p_primary_level = (strcmp(start_node, "one") == 0) ? &level1 : &level2;
    int primary_cap = (strcmp(start_node, "one") == 0) ? cap1 : cap2;
    
    int* p_secondary_level = (strcmp(start_node, "one") == 0) ? &level2 : &level1;
    int secondary_cap = (strcmp(start_node, "one") == 0) ? cap2 : cap1;

    // Main simulation loop
    while (level1 != puzzle->goal && level2 != puzzle->goal) {
        moves++;

        if (*p_primary_level == 0) {
            // Action: Fill the primary (starting) bucket.
            *p_primary_level = primary_cap;
        } else if (*p_secondary_level == puzzle->goal) {
            // Edge case: if the secondary bucket hits the goal, fill it to formalize the step.
            *p_secondary_level = secondary_cap;
        } else if (*p_secondary_level == secondary_cap) {
            // Action: Empty the secondary bucket if it's full.
            *p_secondary_level = 0;
        } else {
            // Action: Pour from primary to secondary.
            int available_space = secondary_cap - *p_secondary_level;
            int pour_amount = fmin(*p_primary_level, available_space);
            
            *p_primary_level -= pour_amount;
            *p_secondary_level += pour_amount;
        }
        
        // Safety break to prevent potential infinite loops in unsolvable (but mathematically possible) cases.
        if (moves > 200) { // A reasonable upper limit
            return (bucket_result_t){.possible = false};
        }
    }

    // Prepare the result structure
    bucket_result_t result = {.possible = true, .move_count = moves};
    if (level1 == puzzle->goal) {
        result.goal_bucket = "one";
        result.other_bucket_level = level2;
    } else {
        result.goal_bucket = "two";
        result.other_bucket_level = level1;
    }

    return result;
}

Compiling and Running the Code

To test this solution, you would typically write a main function or a test suite that calls bucket_solve with different puzzle configurations. Save the code as two_bucket.c and compile it using a standard C compiler like GCC.


gcc -std=c11 -o two_bucket_solver two_bucket.c -lm
./two_bucket_solver

The -std=c11 flag ensures compatibility with a modern C standard, and -lm is necessary to link the math library for the fmin function. For a deeper dive into C compilation and project setup, explore our comprehensive C programming guide.


Detailed Code Walkthrough

Let's dissect the C code to understand how each part contributes to the final solution. The logic is sequential and relies on careful state management within the main loop.

1. Data Structures: bucket_puzzle_t and bucket_result_t

We use two struct types to organize our data. bucket_puzzle_t neatly packages all the inputs: the capacities of both buckets, the target goal, and which bucket to fill first. bucket_result_t holds all the outputs, making the function's return value clean and easy to understand. This is far better than passing multiple pointers to be modified.

2. The GCD Pre-Check

The function gcd(int a, int b) calculates the Greatest Common Divisor of the two bucket capacities. According to the properties of Diophantine equations (of which this problem is a type), a solution is only possible if the target goal is a multiple of the GCD of the two capacities. For example, with a 6-liter and a 9-liter bucket (GCD=3), you can only measure multiples of 3. Trying to measure 4 liters would be impossible.

This check, if (puzzle->goal % gcd(...) != 0), allows us to fail fast and avoid an infinite loop for impossible scenarios. This is a crucial optimization.

3. Pointer-Based Role Assignment

Instead of writing duplicate logic for when "one" or "two" is the starting bucket, we use pointers to abstract the roles:


int* p_primary_level = (strcmp(start_node, "one") == 0) ? &level1 : &level2;
int* p_secondary_level = (strcmp(start_node, "one") == 0) ? &level2 : &level1;

p_primary_level always points to the level of the bucket we are actively manipulating (the "start_bucket"), and p_secondary_level points to the other. This simplifies the main loop immensely, as we can write the logic in terms of "primary" and "secondary" without caring which is bucket 1 or 2.

4. The Main Simulation Loop: while (...)

The loop continues as long as neither bucket contains the goal amount. Inside the loop, a series of if-else if statements determines the single action to perform for that move.

  • if (*p_primary_level == 0): If the primary bucket is empty, the only logical first step is to fill it.
  • else if (*p_secondary_level == secondary_cap): If the secondary bucket is full, we must empty it to make space for future pours.
  • else: This is the pour action, the most complex step.

5. The Pouring Logic Explained

The pour is the heart of the simulation. We need to calculate exactly how much water to transfer.

  ● Start Pour Action
  │
  ├─ Have: primary_level, secondary_level
  ├─ Have: secondary_cap
  │
  ▼
┌───────────────────────────┐
│ Calculate available_space │
│ in secondary bucket       │
│ (secondary_cap - secondary_level) │
└────────────┬──────────────┘
             │
             ▼
┌───────────────────────────┐
│ Determine pour_amount     │
│ min(primary_level, available_space) │
└────────────┬──────────────┘
             │
             ▼
    ◆ Was primary_level <= available_space?
   ╱           ╲
 Yes (Primary empties) No (Secondary fills)
  │               │
  ▼               ▼
┌────────────────┐ ┌────────────────┐
│ primary_level=0│ │ primary_level -= │
│ secondary_level│ │ pour_amount    │
│ += pour_amount │ │ secondary_level│
│                │ │ = secondary_cap│
└────────────────┘ └────────────────┘
  │               │
  └──────┬────────┘
         │
         ▼
● End Pour Action (Levels Updated)

The line int pour_amount = fmin(*p_primary_level, available_space); elegantly handles both outcomes of a pour. The amount transferred is the smaller of what's in the primary bucket and the available space in the secondary bucket. This single line prevents us from overfilling the destination or pouring more than we have.

6. Finalizing the Result

Once the loop terminates, we know one of the buckets has reached the goal. The final block of code checks which bucket holds the goal amount, populates the bucket_result_t struct accordingly, and returns it.


Alternative Approaches and Considerations

The solution provided is a direct simulation. It's intuitive and follows a single path to the solution. However, it's not the only way to solve this problem, and for more complex state-space searches, other algorithms are more powerful.

Breadth-First Search (BFS)

A more robust method is to treat the problem as a graph traversal. Each state (level1, level2) is a node in the graph, and each action (fill, empty, pour) is an edge to a new node. A Breadth-First Search (BFS) algorithm could explore this graph level by level.

The main advantage of BFS is that it is guaranteed to find the solution with the fewest possible moves. Our direct simulation finds *a* solution, but it might not be the most optimal one depending on the starting bucket and capacities.

Pros and Cons of Different Approaches

Approach Pros Cons
Direct Simulation (Our Solution)
  • Simple to understand and implement.
  • Low memory overhead; only tracks the current state.
  • Very fast for solvable paths.
  • May not find the most optimal (shortest) solution.
  • The logic can become complex with more rules.
  • Can get stuck in loops if not carefully designed.
Breadth-First Search (BFS)
  • Guaranteed to find the shortest path (optimal solution).
  • More generic and adaptable to other state-space problems.
  • Systematically explores all possibilities.
  • More complex to implement (requires a queue and a way to track visited states).
  • Higher memory usage to store the queue and visited set.
  • Can be slower if the state space is very large.

For the constraints of this particular kodikra.com module, the direct simulation is perfectly sufficient and provides an excellent learning experience in C programming logic.


Frequently Asked Questions (FAQ)

What happens if the goal is impossible to reach?
Our solution handles this in two ways. First, the GCD check at the beginning catches mathematically impossible scenarios immediately. Second, a safety break (if (moves > 200)) prevents infinite loops for other edge cases, returning an impossible result.
Why is the Greatest Common Divisor (GCD) so important?
The process of pouring water between two buckets of capacity C1 and C2 can only produce volumes that are linear combinations of C1 and C2. Bézout's identity states that the smallest positive integer that can be expressed this way is the GCD of C1 and C2. Therefore, any reachable volume must be a multiple of their GCD.
Can this problem be solved recursively?
Yes, it can. A recursive function could explore the state space, with each call representing one move. However, you would need to pass the current state (levels and move count) in each call and carefully manage a "visited" set to avoid infinite recursion, which essentially re-implements a Depth-First Search (DFS).
Does the starting bucket choice affect the outcome?
It can affect the number of moves and the final state of the non-goal bucket. Depending on the capacities and goal, starting with one bucket might lead to a solution faster than starting with the other. A BFS approach would inherently find the best starting move.
Why is C a good language for this kind of logic puzzle?
C's performance and low-level control make it ideal for simulations where efficiency matters. Using pointers, as we did to abstract the primary/secondary buckets, is a powerful C idiom that leads to cleaner, more efficient code by avoiding data copying and redundant logic.
What if the goal is equal to one of the bucket's capacities?
Our code handles this correctly. The first move would be to fill the corresponding bucket, and the loop condition while (level1 != puzzle->goal && level2 != puzzle->goal) would immediately terminate. The result would be 1 move.
How could this logic be extended to three or more buckets?
Extending to more buckets significantly increases the complexity. The number of possible actions (e.g., pour from A to B, A to C, B to A, etc.) grows rapidly. A direct simulation with hardcoded logic becomes impractical. This is where a more generic graph traversal algorithm like BFS becomes almost essential.

Conclusion: From Puzzle to Practical Skill

The Two Bucket Problem is a fantastic journey into the world of algorithmic thinking. By translating a physical puzzle into a C program, you've practiced state management, logical branching, and the importance of handling edge cases. The solution demonstrates how powerful C can be for creating efficient simulations, using features like structs and pointers to write clean and reusable logic.

You have not only solved a classic brain teaser but also built a mental model for tackling any problem that involves a series of states and actions. This skill is directly applicable to countless real-world programming challenges. As you continue on your kodikra learning journey, you will see these patterns of state-based logic appear again and again.

Technology Disclaimer: The C code in this article is written using the C11 standard and compiled with GCC. The concepts are fundamental and should be compatible with any modern C compiler (like Clang) with minimal to no modification.


Published by Kodikra — Your trusted C learning resource.