2137. Pour Water Between Buckets to Make Water Levels Equal
Problem Description
You have n buckets, each containing some amount of water. The water amounts are given in an array buckets, where buckets[i] represents the gallons of water in the i-th bucket.
Your goal is to make all buckets contain the same amount of water by pouring water from one bucket to another. However, there's a catch: whenever you pour k gallons of water from one bucket to another, you spill loss percent of that water. For example, if loss = 20 and you pour 10 gallons, only 8 gallons will reach the destination bucket (you lose 20% of 10 = 2 gallons).
You need to find the maximum possible amount of water that can be in each bucket after equalizing them all.
Key Points:
- You can pour any amount of water between buckets (not necessarily integers)
- Water is lost during transfer based on the
losspercentage - The goal is to maximize the final equal water level across all buckets
- The answer should be accurate within
10^-5of the actual value
Example Understanding:
If you have buckets with [10, 5, 15] gallons and loss = 10%:
- You could transfer water from the bucket with 15 gallons to the one with 5 gallons
- Due to the 10% loss during transfer, not all excess water can be redistributed perfectly
- The algorithm needs to find the optimal final water level that all buckets can achieve
The solution uses binary search to find this maximum achievable water level, checking at each potential level whether it's possible to redistribute the water (accounting for losses) to reach that level in all buckets.
Intuition
The key insight is recognizing that this problem has a monotonic property: if we can achieve a certain water level v in all buckets, then we can definitely achieve any water level less than v. Why? Because if we can reach level v, we can simply pour out some extra water to reach any lower level.
This monotonic property immediately suggests binary search as a solution approach. We're looking for the maximum achievable water level - the highest point where it's still possible to equalize all buckets.
Think about what happens when we try to achieve a target water level v:
- Buckets with water above
vare donors - they have excess water to give - Buckets with water below
vare receivers - they need water added
The critical observation is about the loss during transfer. If a bucket needs x gallons to reach level v, we actually need to pour x * 100 / (100 - loss) gallons from the donor buckets because some water spills during transfer. For example, if loss = 20% and we need 8 gallons to arrive at the destination, we must pour 8 * 100 / 80 = 10 gallons (losing 2 gallons in transit).
For a water level v to be achievable:
- Total available excess water from donors ≥ Total water needed to be poured (including spillage)
- In other words:
sum(water above v)≥sum(water needed below v) * 100 / (100 - loss)
The maximum achievable level lies somewhere between 0 and max(buckets) (we can't create water from nothing). By using binary search in this range and checking the feasibility condition at each midpoint, we can efficiently find the maximum water level where the available excess water is just enough to fill all deficient buckets despite the losses.
Learn more about Binary Search patterns.
Solution Implementation
1class Solution:
2 def equalizeWater(self, buckets: List[int], loss: int) -> float:
3 """
4 Find the maximum water level that can be achieved after equalizing water in buckets.
5
6 Uses binary search to find the boundary between feasible and infeasible water levels.
7
8 Args:
9 buckets: List of initial water amounts in each bucket
10 loss: Percentage of water lost during transfer (0 to 100)
11
12 Returns:
13 Maximum achievable water level after equalization
14 """
15
16 def feasible(target_level: float) -> bool:
17 """
18 Check if a target water level can be achieved across all buckets.
19
20 Water can be poured from buckets above the target level to those below.
21 During transfer, (loss)% of water is lost.
22
23 Args:
24 target_level: The water level we want to achieve
25
26 Returns:
27 True if the target level is achievable, False otherwise
28 """
29 water_available = 0.0
30 water_needed = 0.0
31
32 for current_water in buckets:
33 if current_water >= target_level:
34 water_available += current_water - target_level
35 else:
36 water_needed += (target_level - current_water) * 100 / (100 - loss)
37
38 return water_available >= water_needed
39
40 # Binary search for the maximum achievable water level
41 left = 0.0
42 right = float(max(buckets))
43 first_true_level = 0.0 # Track the last feasible level found
44 epsilon = 1e-5
45
46 # Binary search until we reach desired precision
47 while right - left > epsilon:
48 mid = (left + right) / 2
49
50 if feasible(mid):
51 first_true_level = mid
52 left = mid # Try for higher level
53 else:
54 right = mid # Try for lower level
55
56 return first_true_level
571class Solution {
2 private int[] buckets;
3 private int loss;
4
5 /**
6 * Equalizes water across buckets with a given loss percentage during transfer.
7 * Uses binary search to find the maximum achievable water level.
8 *
9 * @param buckets Array containing initial water amounts in each bucket
10 * @param loss Percentage of water lost during transfer (0-100)
11 * @return Maximum water level achievable after redistribution
12 */
13 public double equalizeWater(int[] buckets, int loss) {
14 this.buckets = buckets;
15 this.loss = loss;
16
17 // Binary search boundaries
18 double left = 0;
19 double right = 0;
20 for (int bucket : buckets) {
21 right = Math.max(right, bucket);
22 }
23
24 double firstTrueLevel = 0.0;
25 double epsilon = 1e-5;
26
27 // Binary search with precision threshold
28 while (right - left > epsilon) {
29 double mid = (left + right) / 2;
30
31 if (feasible(mid)) {
32 firstTrueLevel = mid;
33 left = mid; // Try for higher level
34 } else {
35 right = mid; // Try for lower level
36 }
37 }
38
39 return firstTrueLevel;
40 }
41
42 /**
43 * Checks if a target water level is achievable given the loss percentage.
44 *
45 * @param targetLevel Target water level to check
46 * @return true if the target level is achievable, false otherwise
47 */
48 private boolean feasible(double targetLevel) {
49 double excessWater = 0;
50 double requiredWater = 0;
51
52 for (int currentAmount : buckets) {
53 if (currentAmount >= targetLevel) {
54 excessWater += currentAmount - targetLevel;
55 } else {
56 requiredWater += (targetLevel - currentAmount) * 100 / (100 - loss);
57 }
58 }
59
60 return excessWater >= requiredWater;
61 }
62}
631class Solution {
2public:
3 double equalizeWater(vector<int>& buckets, int loss) {
4 // Lambda function to check if target water level is achievable
5 auto feasible = [&](double targetLevel) {
6 double excessWater = 0.0;
7 double deficitWater = 0.0;
8
9 for (int bucketAmount : buckets) {
10 if (bucketAmount >= targetLevel) {
11 excessWater += bucketAmount - targetLevel;
12 } else {
13 deficitWater += (targetLevel - bucketAmount) * 100.0 / (100.0 - loss);
14 }
15 }
16
17 return excessWater >= deficitWater;
18 };
19
20 // Binary search bounds
21 double left = 0.0;
22 double right = *max_element(buckets.begin(), buckets.end());
23 double firstTrueLevel = 0.0;
24 const double EPSILON = 1e-5;
25
26 // Binary search for the maximum achievable water level
27 while (right - left > EPSILON) {
28 double mid = (left + right) / 2.0;
29
30 if (feasible(mid)) {
31 firstTrueLevel = mid;
32 left = mid; // Try for a higher level
33 } else {
34 right = mid; // Lower the target
35 }
36 }
37
38 return firstTrueLevel;
39 }
40};
411/**
2 * Finds the maximum water level that can be achieved after equalizing water between buckets
3 * @param buckets - Array containing the initial water amount in each bucket
4 * @param loss - Percentage of water lost during transfer (0-100)
5 * @returns The maximum achievable uniform water level across all buckets
6 */
7function equalizeWater(buckets: number[], loss: number): number {
8 /**
9 * Checks if a target water level is achievable
10 * @param targetLevel - The water level to check
11 * @returns true if the target level can be achieved, false otherwise
12 */
13 const feasible = (targetLevel: number): boolean => {
14 let excessWater = 0;
15 let requiredWater = 0;
16
17 for (const currentAmount of buckets) {
18 if (currentAmount >= targetLevel) {
19 excessWater += currentAmount - targetLevel;
20 } else {
21 requiredWater += ((targetLevel - currentAmount) * 100) / (100 - loss);
22 }
23 }
24
25 return excessWater >= requiredWater;
26 };
27
28 // Binary search bounds
29 let left = 0;
30 let right = Math.max(...buckets);
31 let firstTrueLevel = 0;
32 const PRECISION = 1e-5;
33
34 // Binary search for the maximum achievable water level
35 while (right - left > PRECISION) {
36 const mid = (left + right) / 2;
37
38 if (feasible(mid)) {
39 firstTrueLevel = mid;
40 left = mid; // Try a higher level
41 } else {
42 right = mid; // Try a lower level
43 }
44 }
45
46 return firstTrueLevel;
47}
48Solution Approach
The solution implements binary search on floating-point numbers to find the maximum achievable water level using the boundary-finding template.
The Feasible Function:
For each candidate water level v, we check if it's achievable by comparing excess water to required water:
def feasible(target_level):
water_available = 0.0
water_needed = 0.0
for current_water in buckets:
if current_water >= target_level:
water_available += current_water - target_level
else:
water_needed += (target_level - current_water) * 100 / (100 - loss)
return water_available >= water_needed
- Variable
water_availableaccumulates the total excess water from buckets above levelv - Variable
water_neededaccumulates the total water that needs to be poured (accounting for loss) - For buckets below level
v, we calculate the actual amount to pour using the formula:(v - x) * 100 / (100 - loss)
Binary Search with Template Pattern:
Since we're searching on floating-point values, we use a precision-based loop instead of integer boundaries. The search creates a pattern where:
- Low values are feasible (we have enough excess water)
- High values are not feasible (not enough water after accounting for loss)
We search for the boundary - the maximum level that's still feasible.
left, right = 0.0, max(buckets)
epsilon = 1e-5
while right - left > epsilon:
mid = (left + right) / 2
if feasible(mid):
left = mid # mid is achievable, try higher
else:
right = mid # mid is not achievable, try lower
When the loop terminates, left contains the maximum achievable water level (the last feasible value found).
Time Complexity: O(n × log(M / ε)) where n is the number of buckets, M is the maximum bucket value, and ε = 1e-5 is the precision
Space Complexity: O(1) as we only use a constant amount of extra space
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with buckets = [10, 5, 15] and loss = 20%.
Initial Setup:
- We have 3 buckets with 10, 5, and 15 gallons respectively
- Total water = 30 gallons
- When pouring, we lose 20% of the water
Binary Search Process:
Iteration 1:
- Search range:
left = 0,right = 15(max bucket value) mid = 7.5- Check if we can achieve 7.5 gallons in each bucket:
- Bucket 0 (10 gal): excess = 10 - 7.5 = 2.5 gallons
- Bucket 1 (5 gal): needs 7.5 - 5 = 2.5 gallons to arrive
- Must pour: 2.5 × 100/(100-20) = 2.5 × 1.25 = 3.125 gallons
- Bucket 2 (15 gal): excess = 15 - 7.5 = 7.5 gallons
- Total excess available: 2.5 + 7.5 = 10 gallons
- Total needed to pour: 3.125 gallons
- Since 10 ≥ 3.125, level 7.5 is feasible →
left = 7.5
Iteration 2:
- Search range:
left = 7.5,right = 15 mid = 11.25- Check if we can achieve 11.25 gallons in each bucket:
- Bucket 0 (10 gal): needs 11.25 - 10 = 1.25 gallons to arrive
- Must pour: 1.25 × 1.25 = 1.5625 gallons
- Bucket 1 (5 gal): needs 11.25 - 5 = 6.25 gallons to arrive
- Must pour: 6.25 × 1.25 = 7.8125 gallons
- Bucket 2 (15 gal): excess = 15 - 11.25 = 3.75 gallons
- Total excess available: 3.75 gallons
- Total needed to pour: 1.5625 + 7.8125 = 9.375 gallons
- Since 3.75 < 9.375, level 11.25 is NOT feasible →
right = 11.25
- Bucket 0 (10 gal): needs 11.25 - 10 = 1.25 gallons to arrive
Iteration 3:
- Search range:
left = 7.5,right = 11.25 mid = 9.375- Check if we can achieve 9.375 gallons:
- Bucket 0: excess = 10 - 9.375 = 0.625 gallons
- Bucket 1: needs 9.375 - 5 = 4.375 gallons to arrive
- Must pour: 4.375 × 1.25 = 5.46875 gallons
- Bucket 2: excess = 15 - 9.375 = 5.625 gallons
- Total excess: 0.625 + 5.625 = 6.25 gallons
- Total needed: 5.46875 gallons
- Since 6.25 ≥ 5.46875, level 9.375 is feasible →
left = 9.375
The binary search continues, narrowing the range until right - left ≤ 1e-5. The final answer converges to approximately 9.5 gallons per bucket.
Verification of the final answer (~9.5):
- Bucket 0: has 10 gal, excess = 0.5 gal
- Bucket 1: needs 4.5 gal to reach 9.5
- Must pour: 4.5 × 1.25 = 5.625 gal (4.5 arrives, 1.125 is lost)
- Bucket 2: has 15 gal, excess = 5.5 gal
- Total excess: 0.5 + 5.5 = 6 gal
- Total needed to pour: 5.625 gal
- Since 6 > 5.625, this level is achievable with a small margin
Time and Space Complexity
The time complexity is O(n × log M), where n is the length of the array buckets and M is the maximum value in the array buckets.
The analysis breaks down as follows:
- The binary search runs while
right - left > 1e-5. Since we start withleft = 0andright = max(buckets), the search space is initiallyM. The binary search continues until the difference is less than1e-5, which requiresO(log(M/1e-5))iterations, which simplifies toO(log M)iterations. - In each iteration of the binary search, the
feasible(v)function is called, which iterates through allnelements in thebucketsarray to calculate the water that can be poured out and the water needed to fill up to levelv. This takesO(n)time. - Therefore, the total time complexity is
O(n × log M).
The space complexity is O(1) as the algorithm only uses a constant amount of extra space for variables left, right, mid, firstTrueLevel, excessWater, and requiredWater, regardless of the input size.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Using Wrong Binary Search Template Variant
Pitfall: Using the while left < right with right = mid pattern instead of the standard boundary-finding template. This problem searches for a maximum value, so we need to track the last feasible level found.
Why it's wrong: Without tracking firstTrueLevel, you might return an incorrect boundary value when the precision threshold is reached.
Solution: Use the template pattern with explicit tracking:
firstTrueLevel = 0.0 while right - left > epsilon: mid = (left + right) / 2 if feasible(mid): firstTrueLevel = mid left = mid else: right = mid return firstTrueLevel
2. Incorrect Loss Calculation Formula
Pitfall: A common mistake is incorrectly calculating how much water needs to be poured to achieve a target amount after accounting for loss. Many developers mistakenly use:
water_needed += (target_level - current_water) / (1 - loss) # WRONG!
Why it's wrong: The loss parameter is given as a percentage (0-100), not as a decimal (0-1). Using (1 - loss) when loss = 20 would give -19 instead of 0.8.
Solution: Use the correct formula that treats loss as a percentage:
water_needed += (target_level - current_water) * 100 / (100 - loss)
3. Division by Zero When Loss = 100%
Pitfall: The formula (100 - loss) in the denominator causes division by zero when loss = 100, leading to a runtime error.
Solution: Add a special case check:
def feasible(target_level: float) -> bool:
if loss == 100:
# When loss is 100%, no water can be transferred
# Only achievable if all buckets already have >= target_level
return all(water >= target_level for water in buckets)
# Regular logic for loss < 100
water_available = 0.0
water_needed = 0.0
# ... rest of the function
4. Floating-Point Precision Issues
Pitfall: Using exact equality comparisons or too tight precision thresholds can cause issues:
while right - left > 1e-10: # Too tight precision might cause infinite loops
Why it's problematic: Floating-point arithmetic has inherent precision limitations. Setting epsilon too small might never satisfy the condition due to rounding errors.
Solution: Use an appropriate epsilon value (typically 1e-5 to 1e-9) and avoid exact comparisons:
epsilon = 1e-5 # Reasonable precision for this problem while right - left > epsilon: # binary search logic
5. Not Handling Edge Cases
Pitfall: Failing to handle special cases like:
- Empty bucket list
- Single bucket (no transfer needed)
- All buckets having the same amount initially
Solution: Add edge case handling:
def equalizeWater(self, buckets: List[int], loss: int) -> float:
if not buckets:
return 0.0
if len(buckets) == 1:
return float(buckets[0])
# Check if all buckets are already equal
if len(set(buckets)) == 1:
return float(buckets[0])
# Continue with binary search...
How does quick sort divide the problem into subproblems?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!