Facebook Pixel

365. Water and Jug Problem

Problem Description

You have two water jugs with capacities of x liters and y liters, along with an unlimited water supply. Your goal is to determine if it's possible to have exactly target liters of water in total across both jugs.

You can perform these operations:

  • Fill either jug completely from the water supply
  • Empty either jug completely
  • Pour water from one jug to another until either the receiving jug is full or the source jug is empty

The problem asks you to return true if you can achieve exactly target liters in total (counting water in both jugs combined), and false otherwise.

For example, with a 3-liter jug and a 5-liter jug, you can reach a target of 4 liters through a series of fill, empty, and pour operations. The key insight is that through these operations, you can measure various amounts of water that might not be directly equal to either jug's capacity.

The challenge is to determine whether any sequence of these three operations can result in having exactly target liters of water across the two jugs. This is a classic water jug puzzle that tests your ability to explore different states and combinations of water levels in both jugs.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • Yes: This problem can be modeled as a graph where each state (i, j) represents the amount of water in jug1 and jug2 respectively. The edges represent the possible operations (fill, empty, pour) that transition from one state to another.

Is it a tree?

  • No: The state space is not a tree structure. States can be revisited through different sequences of operations, and there can be multiple paths to reach the same state.

Is the problem related to directed acyclic graphs?

  • No: The state graph can have cycles. For example, you can fill a jug, empty it, and return to the original state, creating a cycle.

Is the problem related to shortest paths?

  • No: We don't need to find the shortest sequence of operations. We only need to determine if the target is reachable at all.

Does the problem involve connectivity?

  • No: We're not checking if all states are connected or finding connected components.

Does the problem have small constraints?

  • Yes: The constraints are relatively small (1 ≀ x, y, target ≀ 10Β³), making the state space manageable for exploration.

DFS/backtracking?

  • Yes: With small constraints and the need to explore different states to find if the target is reachable, DFS with backtracking is suitable.

Conclusion: The flowchart suggests using DFS/backtracking to explore all possible states (i, j) representing water amounts in the two jugs, checking if we can reach a state where the total equals the target.

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

Intuition

The key insight is to think of this problem as exploring different states of water in the two jugs. Each state can be represented as (i, j) where i is the amount of water in the first jug and j is the amount in the second jug.

Starting from the initial state (0, 0) where both jugs are empty, we can perform various operations to transition to new states. Each operation creates a new state:

  • Filling jug1 takes us from (i, j) to (x, j)
  • Filling jug2 takes us from (i, j) to (i, y)
  • Emptying jug1 takes us from (i, j) to (0, j)
  • Emptying jug2 takes us from (i, j) to (i, 0)
  • Pouring from one jug to another creates more complex transitions

The question becomes: can we reach any state where i == target or j == target or i + j == target?

This is essentially a graph traversal problem where we need to explore all reachable states from (0, 0). DFS is perfect for this because:

  1. We need to explore all possible paths until we find the target
  2. We can use recursion to naturally handle the branching of different operations
  3. We need to track visited states to avoid infinite loops (since we can return to previous states)

The most intricate part is handling the pour operation. When pouring from jug1 to jug2, we can only pour until either jug1 is empty or jug2 is full. The amount transferred is min(i, y - j). Similarly, when pouring from jug2 to jug1, the amount is min(j, x - i).

By recursively exploring all possible operations from each state and marking visited states, we can determine if the target is achievable. If at any point we reach a state where the total water equals the target, we return true.

Solution Approach

The solution implements a DFS approach with memoization to explore all possible states of water in the two jugs.

Core Data Structure:

  • A set called vis to track visited states and prevent infinite loops
  • Each state is represented as a tuple (i, j) where i is water in jug1 and j is water in jug2

DFS Function Design:

The recursive function dfs(i, j) checks if we can reach the target from state (i, j):

  1. Base Cases:

    • If (i, j) has been visited, return False to avoid cycles
    • If i == z or j == z or i + j == z, we've found the target, return True
  2. Mark Current State: Add (i, j) to the visited set

  3. Explore All Operations:

    • Fill jug1: Call dfs(x, j) - fill jug1 to capacity x
    • Fill jug2: Call dfs(i, y) - fill jug2 to capacity y
    • Empty jug1: Call dfs(0, j) - empty jug1 completely
    • Empty jug2: Call dfs(i, 0) - empty jug2 completely
  4. Pour Operations:

    • Pour jug1 β†’ jug2: Calculate amount a = min(i, y - j)
      • This is the minimum of water in jug1 and remaining capacity in jug2
      • New state: (i - a, j + a)
    • Pour jug2 β†’ jug1: Calculate amount b = min(j, x - i)
      • This is the minimum of water in jug2 and remaining capacity in jug1
      • New state: (i + b, j - b)

Algorithm Flow:

  1. Initialize empty visited set
  2. Start DFS from state (0, 0) (both jugs empty)
  3. At each state, check if target is reached
  4. If not, try all six possible operations
  5. Return True if any path leads to the target

The algorithm efficiently explores the state space using DFS, pruning paths that lead to already-visited states. The time complexity depends on the number of unique states, which is bounded by O(x * y), and each state is visited at most once.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with x = 3 (jug1 capacity), y = 5 (jug2 capacity), and target = 4.

Initial State: (0, 0) - both jugs are empty

Step 1: From (0, 0), we explore all possible operations:

  • Fill jug1: go to (3, 0)
  • Fill jug2: go to (0, 5)
  • Empty operations have no effect since jugs are already empty
  • Pour operations have no effect since there's no water to pour

Let's follow the path through filling jug2 first.

Step 2: State (0, 5) - jug2 is full

  • Check: Is 0 == 4? No. Is 5 == 4? No. Is 0 + 5 == 4? No.
  • Mark (0, 5) as visited
  • Explore next operations:
    • Pour jug2 β†’ jug1: amount = min(5, 3-0) = 3
    • New state: (0+3, 5-3) = (3, 2)

Step 3: State (3, 2) - jug1 has 3L, jug2 has 2L

  • Check: Is 3 == 4? No. Is 2 == 4? No. Is 3 + 2 == 4? No.
  • Mark (3, 2) as visited
  • Explore operations:
    • Empty jug1: go to (0, 2)

Step 4: State (0, 2) - jug1 is empty, jug2 has 2L

  • Check: Is 0 == 4? No. Is 2 == 4? No. Is 0 + 2 == 4? No.
  • Mark (0, 2) as visited
  • Explore operations:
    • Pour jug2 β†’ jug1: amount = min(2, 3-0) = 2
    • New state: (0+2, 2-2) = (2, 0)

Step 5: State (2, 0) - jug1 has 2L, jug2 is empty

  • Check: Is 2 == 4? No. Is 0 == 4? No. Is 2 + 0 == 4? No.
  • Mark (2, 0) as visited
  • Explore operations:
    • Fill jug2: go to (2, 5)

Step 6: State (2, 5) - jug1 has 2L, jug2 is full

  • Check: Is 2 == 4? No. Is 5 == 4? No. Is 2 + 5 == 4? No.
  • Mark (2, 5) as visited
  • Explore operations:
    • Pour jug2 β†’ jug1: amount = min(5, 3-2) = 1
    • New state: (2+1, 5-1) = (3, 4)

Step 7: State (3, 4) - jug1 has 3L, jug2 has 4L

  • Check: Is 3 == 4? No. Is 4 == 4? Yes!
  • Target found! Return True

The sequence of operations that achieved the target:

  1. Fill jug2 (5L)
  2. Pour jug2 into jug1 (3L to jug1, 2L remains in jug2)
  3. Empty jug1
  4. Pour jug2 into jug1 (2L to jug1, jug2 empty)
  5. Fill jug2 (5L)
  6. Pour jug2 into jug1 (1L to jug1, 4L remains in jug2)
  7. Result: jug2 contains exactly 4L

Solution Implementation

1class Solution:
2    def canMeasureWater(self, x: int, y: int, z: int) -> bool:
3        """
4        Determine if it's possible to measure exactly z liters using two jugs of capacity x and y.
5      
6        Args:
7            x: Capacity of jug 1
8            y: Capacity of jug 2
9            z: Target amount to measure
10          
11        Returns:
12            True if z liters can be measured, False otherwise
13        """
14      
15        def dfs(jug1_amount: int, jug2_amount: int) -> bool:
16            """
17            Depth-first search to explore all possible states of water in both jugs.
18          
19            Args:
20                jug1_amount: Current amount of water in jug 1
21                jug2_amount: Current amount of water in jug 2
22              
23            Returns:
24                True if target can be reached from current state, False otherwise
25            """
26            # Check if this state has been visited before
27            if (jug1_amount, jug2_amount) in visited_states:
28                return False
29          
30            # Mark current state as visited
31            visited_states.add((jug1_amount, jug2_amount))
32          
33            # Check if we've reached the target amount in either jug or combined
34            if jug1_amount == z or jug2_amount == z or jug1_amount + jug2_amount == z:
35                return True
36          
37            # Try all possible operations:
38            # 1. Fill jug 1 completely
39            # 2. Fill jug 2 completely  
40            # 3. Empty jug 1
41            # 4. Empty jug 2
42            if (dfs(x, jug2_amount) or 
43                dfs(jug1_amount, y) or 
44                dfs(0, jug2_amount) or 
45                dfs(jug1_amount, 0)):
46                return True
47          
48            # Pour from jug 1 to jug 2 (until jug 2 is full or jug 1 is empty)
49            pour_amount_1_to_2 = min(jug1_amount, y - jug2_amount)
50          
51            # Pour from jug 2 to jug 1 (until jug 1 is full or jug 2 is empty)
52            pour_amount_2_to_1 = min(jug2_amount, x - jug1_amount)
53          
54            # Try both pouring operations
55            return (dfs(jug1_amount - pour_amount_1_to_2, jug2_amount + pour_amount_1_to_2) or 
56                    dfs(jug1_amount + pour_amount_2_to_1, jug2_amount - pour_amount_2_to_1))
57      
58        # Set to track visited states to avoid cycles
59        visited_states = set()
60      
61        # Start with both jugs empty
62        return dfs(0, 0)
63
1class Solution {
2    // Set to track visited states to avoid infinite loops
3    private Set<Long> visitedStates = new HashSet<>();
4    // Store jug capacities and target capacity as instance variables
5    private int jug1Capacity, jug2Capacity, targetCapacity;
6
7    /**
8     * Determines if it's possible to measure exactly targetCapacity liters using two jugs
9     * @param jug1Capacity capacity of the first jug
10     * @param jug2Capacity capacity of the second jug
11     * @param targetCapacity the target amount of water to measure
12     * @return true if target capacity can be measured, false otherwise
13     */
14    public boolean canMeasureWater(int jug1Capacity, int jug2Capacity, int targetCapacity) {
15        this.jug1Capacity = jug1Capacity;
16        this.jug2Capacity = jug2Capacity;
17        this.targetCapacity = targetCapacity;
18      
19        // Start DFS with both jugs empty
20        return dfs(0, 0);
21    }
22
23    /**
24     * Depth-first search to explore all possible states
25     * @param currentJug1 current amount of water in jug1
26     * @param currentJug2 current amount of water in jug2
27     * @return true if target can be reached from current state
28     */
29    private boolean dfs(int currentJug1, int currentJug2) {
30        // Create unique state identifier for current water levels
31        long stateKey = encodeState(currentJug1, currentJug2);
32      
33        // Check if this state has been visited before to avoid cycles
34        if (!visitedStates.add(stateKey)) {
35            return false;
36        }
37      
38        // Check if we've reached the target capacity
39        if (currentJug1 == targetCapacity || 
40            currentJug2 == targetCapacity || 
41            currentJug1 + currentJug2 == targetCapacity) {
42            return true;
43        }
44      
45        // Try all possible operations:
46        // 1. Fill jug1 completely
47        if (dfs(jug1Capacity, currentJug2)) {
48            return true;
49        }
50      
51        // 2. Fill jug2 completely
52        if (dfs(currentJug1, jug2Capacity)) {
53            return true;
54        }
55      
56        // 3. Empty jug1 completely
57        if (dfs(0, currentJug2)) {
58            return true;
59        }
60      
61        // 4. Empty jug2 completely
62        if (dfs(currentJug1, 0)) {
63            return true;
64        }
65      
66        // 5. Pour from jug1 to jug2 (as much as possible)
67        int pourAmount1To2 = Math.min(currentJug1, jug2Capacity - currentJug2);
68        if (dfs(currentJug1 - pourAmount1To2, currentJug2 + pourAmount1To2)) {
69            return true;
70        }
71      
72        // 6. Pour from jug2 to jug1 (as much as possible)
73        int pourAmount2To1 = Math.min(currentJug2, jug1Capacity - currentJug1);
74        return dfs(currentJug1 + pourAmount2To1, currentJug2 - pourAmount2To1);
75    }
76
77    /**
78     * Encodes two jug states into a single long value for hashing
79     * @param jug1Amount amount of water in jug1
80     * @param jug2Amount amount of water in jug2
81     * @return encoded state as a long value
82     */
83    private long encodeState(int jug1Amount, int jug2Amount) {
84        return jug1Amount * 1000000L + jug2Amount;
85    }
86}
87
1class Solution {
2public:
3    bool canMeasureWater(int x, int y, int z) {
4        // Define pair type for representing state (water in jug1, water in jug2)
5        using State = pair<int, int>;
6      
7        // Stack for DFS traversal of possible states
8        stack<State> stateStack;
9        stateStack.emplace(0, 0);  // Start with both jugs empty
10      
11        // Custom hash function for pair<int, int> to use in unordered_set
12        auto hashFunction = [](const State& state) { 
13            return hash<int>()(state.first) ^ hash<int>()(state.second); 
14        };
15      
16        // Set to track visited states to avoid infinite loops
17        unordered_set<State, decltype(hashFunction)> visitedStates(0, hashFunction);
18      
19        // DFS to explore all possible states
20        while (!stateStack.empty()) {
21            State currentState = stateStack.top();
22            stateStack.pop();
23          
24            // Skip if this state has already been visited
25            if (visitedStates.count(currentState)) {
26                continue;
27            }
28          
29            // Mark current state as visited
30            visitedStates.emplace(currentState);
31          
32            // Extract water amounts in both jugs
33            auto [waterInJug1, waterInJug2] = currentState;
34          
35            // Check if we've reached the target amount
36            if (waterInJug1 == z || waterInJug2 == z || waterInJug1 + waterInJug2 == z) {
37                return true;
38            }
39          
40            // Generate all possible next states from current state
41          
42            // Operation 1: Fill jug1 completely
43            stateStack.emplace(x, waterInJug2);
44          
45            // Operation 2: Fill jug2 completely
46            stateStack.emplace(waterInJug1, y);
47          
48            // Operation 3: Empty jug1
49            stateStack.emplace(0, waterInJug2);
50          
51            // Operation 4: Empty jug2
52            stateStack.emplace(waterInJug1, 0);
53          
54            // Operation 5: Pour from jug1 to jug2
55            // Amount to pour = min(water in jug1, space left in jug2)
56            int pourAmount1To2 = min(waterInJug1, y - waterInJug2);
57            stateStack.emplace(waterInJug1 - pourAmount1To2, waterInJug2 + pourAmount1To2);
58          
59            // Operation 6: Pour from jug2 to jug1
60            // Amount to pour = min(water in jug2, space left in jug1)
61            int pourAmount2To1 = min(waterInJug2, x - waterInJug1);
62            stateStack.emplace(waterInJug1 + pourAmount2To1, waterInJug2 - pourAmount2To1);
63        }
64      
65        // No valid state found that gives us exactly z liters
66        return false;
67    }
68};
69
1function canMeasureWater(x: number, y: number, z: number): boolean {
2    // Define type for representing state (water in jug1, water in jug2)
3    type State = [number, number];
4  
5    // Stack for DFS traversal of possible states
6    const stateStack: State[] = [];
7    stateStack.push([0, 0]); // Start with both jugs empty
8  
9    // Set to track visited states to avoid infinite loops
10    // Using string representation as key since TypeScript doesn't have built-in pair hashing
11    const visitedStates = new Set<string>();
12  
13    // Helper function to create unique string key for state
14    const getStateKey = (state: State): string => {
15        return `${state[0]},${state[1]}`;
16    };
17  
18    // DFS to explore all possible states
19    while (stateStack.length > 0) {
20        const currentState = stateStack.pop()!;
21        const stateKey = getStateKey(currentState);
22      
23        // Skip if this state has already been visited
24        if (visitedStates.has(stateKey)) {
25            continue;
26        }
27      
28        // Mark current state as visited
29        visitedStates.add(stateKey);
30      
31        // Extract water amounts in both jugs
32        const [waterInJug1, waterInJug2] = currentState;
33      
34        // Check if we've reached the target amount
35        if (waterInJug1 === z || waterInJug2 === z || waterInJug1 + waterInJug2 === z) {
36            return true;
37        }
38      
39        // Generate all possible next states from current state
40      
41        // Operation 1: Fill jug1 completely
42        stateStack.push([x, waterInJug2]);
43      
44        // Operation 2: Fill jug2 completely
45        stateStack.push([waterInJug1, y]);
46      
47        // Operation 3: Empty jug1
48        stateStack.push([0, waterInJug2]);
49      
50        // Operation 4: Empty jug2
51        stateStack.push([waterInJug1, 0]);
52      
53        // Operation 5: Pour from jug1 to jug2
54        // Amount to pour = min(water in jug1, space left in jug2)
55        const pourAmount1To2 = Math.min(waterInJug1, y - waterInJug2);
56        stateStack.push([waterInJug1 - pourAmount1To2, waterInJug2 + pourAmount1To2]);
57      
58        // Operation 6: Pour from jug2 to jug1
59        // Amount to pour = min(water in jug2, space left in jug1)
60        const pourAmount2To1 = Math.min(waterInJug2, x - waterInJug1);
61        stateStack.push([waterInJug1 + pourAmount2To1, waterInJug2 - pourAmount2To1]);
62    }
63  
64    // No valid state found that gives us exactly z liters
65    return false;
66}
67

Time and Space Complexity

The time complexity is O(x * y) and the space complexity is O(x * y).

The algorithm performs a depth-first search (DFS) on the state space where each state is represented by a tuple (i, j), indicating the current water amounts in the two jugs.

For time complexity: In the worst case, the algorithm explores all possible states. Since i can range from 0 to x and j can range from 0 to y, there are at most (x + 1) * (y + 1) unique states. Each state is visited at most once due to the visited set vis, resulting in O(x * y) time complexity.

For space complexity: The visited set vis stores all explored states to avoid revisiting them. In the worst case, it contains all possible (i, j) pairs, which is O(x * y). Additionally, the recursion stack depth can be at most O(x * y) in the worst case when exploring all states before finding a solution or exhausting all possibilities. Therefore, the total space complexity is O(x * y).

Note: The reference answer suggests O(x + y) complexity, which would apply to an optimized solution using the mathematical property that the problem is solvable if and only if z is divisible by gcd(x, y) and z ≀ x + y. However, the provided DFS solution explores the state space more exhaustively, leading to the higher O(x * y) complexity.

Common Pitfalls

1. Stack Overflow Due to Deep Recursion

The DFS approach can hit Python's recursion limit (typically ~1000) when dealing with large jug capacities. For example, with jugs of capacity 10,000 and 10,001, the recursive calls can exceed the stack limit before finding a solution.

Solution: Convert the recursive DFS to an iterative approach using a stack or queue:

def canMeasureWater(self, x: int, y: int, z: int) -> bool:
    if z > x + y:
        return False
    if z == 0:
        return True
  
    visited = set()
    stack = [(0, 0)]
  
    while stack:
        jug1, jug2 = stack.pop()
      
        if (jug1, jug2) in visited:
            continue
          
        visited.add((jug1, jug2))
      
        if jug1 == z or jug2 == z or jug1 + jug2 == z:
            return True
      
        # All possible next states
        states = [
            (x, jug2),  # Fill jug1
            (jug1, y),  # Fill jug2
            (0, jug2),  # Empty jug1
            (jug1, 0),  # Empty jug2
            (jug1 - min(jug1, y - jug2), jug2 + min(jug1, y - jug2)),  # Pour 1->2
            (jug1 + min(jug2, x - jug1), jug2 - min(jug2, x - jug1))   # Pour 2->1
        ]
      
        for state in states:
            if state not in visited:
                stack.append(state)
  
    return False

2. Missing Edge Cases

The current solution doesn't handle some edge cases efficiently:

  • When z > x + y, it's impossible to measure z liters (you can't have more water than both jugs combined)
  • When z == 0, the answer is always True (empty jugs)

Solution: Add early termination checks:

def canMeasureWater(self, x: int, y: int, z: int) -> bool:
    # Early termination for impossible cases
    if z > x + y:
        return False
    if z == 0:
        return True
    if x == 0 or y == 0:
        return z == x or z == y

3. Inefficient for Large Inputs

The DFS/BFS approach explores O(x * y) states, which becomes inefficient for large values. There's actually a mathematical solution using the greatest common divisor (GCD).

Mathematical Solution: The target z is achievable if and only if z is a multiple of GCD(x, y) and z ≀ x + y.

import math

def canMeasureWater(self, x: int, y: int, z: int) -> bool:
    if z > x + y:
        return False
    if z == 0:
        return True
    if x == 0 or y == 0:
        return z == x or z == y
  
    return z % math.gcd(x, y) == 0

This reduces the time complexity from O(x * y) to O(log(min(x, y))) for the GCD calculation.

4. Memory Issues with Large State Space

The visited set can consume significant memory when x and y are large, potentially causing memory errors.

Solution: Use the mathematical approach above, or implement a more memory-efficient state representation if the search approach is required.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which two pointer techniques do you use to check if a string is a palindrome?


Recommended Readings

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

Load More