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.
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:
- We need to explore all possible paths until we find the target
- We can use recursion to naturally handle the branching of different operations
- 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
calledvis
to track visited states and prevent infinite loops - Each state is represented as a tuple
(i, j)
wherei
is water in jug1 andj
is water in jug2
DFS Function Design:
The recursive function dfs(i, j)
checks if we can reach the target from state (i, j)
:
-
Base Cases:
- If
(i, j)
has been visited, returnFalse
to avoid cycles - If
i == z
orj == z
ori + j == z
, we've found the target, returnTrue
- If
-
Mark Current State: Add
(i, j)
to the visited set -
Explore All Operations:
- Fill jug1: Call
dfs(x, j)
- fill jug1 to capacityx
- Fill jug2: Call
dfs(i, y)
- fill jug2 to capacityy
- Empty jug1: Call
dfs(0, j)
- empty jug1 completely - Empty jug2: Call
dfs(i, 0)
- empty jug2 completely
- Fill jug1: Call
-
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)
- Pour jug1 β jug2: Calculate amount
Algorithm Flow:
- Initialize empty visited set
- Start DFS from state
(0, 0)
(both jugs empty) - At each state, check if target is reached
- If not, try all six possible operations
- 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 EvaluatorExample 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. Is5 == 4
? No. Is0 + 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)
- Pour jug2 β jug1: amount =
Step 3: State (3, 2)
- jug1 has 3L, jug2 has 2L
- Check: Is
3 == 4
? No. Is2 == 4
? No. Is3 + 2 == 4
? No. - Mark
(3, 2)
as visited - Explore operations:
- Empty jug1: go to
(0, 2)
- Empty jug1: go to
Step 4: State (0, 2)
- jug1 is empty, jug2 has 2L
- Check: Is
0 == 4
? No. Is2 == 4
? No. Is0 + 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)
- Pour jug2 β jug1: amount =
Step 5: State (2, 0)
- jug1 has 2L, jug2 is empty
- Check: Is
2 == 4
? No. Is0 == 4
? No. Is2 + 0 == 4
? No. - Mark
(2, 0)
as visited - Explore operations:
- Fill jug2: go to
(2, 5)
- Fill jug2: go to
Step 6: State (2, 5)
- jug1 has 2L, jug2 is full
- Check: Is
2 == 4
? No. Is5 == 4
? No. Is2 + 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)
- Pour jug2 β jug1: amount =
Step 7: State (3, 4)
- jug1 has 3L, jug2 has 4L
- Check: Is
3 == 4
? No. Is4 == 4
? Yes! - Target found! Return
True
The sequence of operations that achieved the target:
- Fill jug2 (5L)
- Pour jug2 into jug1 (3L to jug1, 2L remains in jug2)
- Empty jug1
- Pour jug2 into jug1 (2L to jug1, jug2 empty)
- Fill jug2 (5L)
- Pour jug2 into jug1 (1L to jug1, 4L remains in jug2)
- 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.
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
https assets algo monster cover_photos dfs svg Depth First Search Prereqs Recursion Review problems recursion_intro Trees problems tree_intro With a solid understanding of recursion under our belts we are now ready to tackle one of the most useful techniques in coding interviews Depth First Search DFS As the name suggests
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Want a Structured Path to Master System Design Too? Donβt Miss This!