Facebook Pixel

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 loss percentage
  • The goal is to maximize the final equal water level across all buckets
  • The answer should be accurate within 10^-5 of 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.

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

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 v are donors - they have excess water to give
  • Buckets with water below v are 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 Approach

The solution implements a binary search on floating-point numbers to find the maximum achievable water level.

Binary Search Setup:

  • Initialize search boundaries: l = 0 (minimum possible water level) and r = max(buckets) (maximum possible water level)
  • Use precision threshold 1e-5 as the stopping condition since we're dealing with floating-point numbers
  • Continue searching while r - l > 1e-5

The Check Function: For each candidate water level v, we need to verify if it's achievable:

def check(v):
    a = b = 0
    for x in buckets:
        if x >= v:
            a += x - v  # excess water available from this bucket
        else:
            b += (v - x) * 100 / (100 - loss)  # water needed (including spillage)
    return a >= b  # check if available water >= required water
  • Variable a accumulates the total excess water from buckets above level v
  • Variable b accumulates 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)
    • If we need (v - x) gallons to arrive, and lose loss% during transfer
    • We must pour (v - x) / (1 - loss/100) gallons from the source

Binary Search Logic:

while r - l > 1e-5:
    mid = (l + r) / 2
    if check(mid):
        l = mid  # mid is achievable, try higher
    else:
        r = mid  # mid is not achievable, try lower
  • Calculate midpoint mid = (l + r) / 2
  • If check(mid) returns True, the water level mid is achievable, so we update l = mid to search for potentially higher levels
  • If check(mid) returns False, the water level mid is too high, so we update r = mid to search lower levels
  • The search converges to the maximum achievable water level

Time Complexity: O(n * log(max_water / ε)) where n is the number of buckets 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 Evaluator

Example 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: l = 0, r = 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 achievable → l = 7.5

Iteration 2:

  • Search range: l = 7.5, r = 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 achievable → r = 11.25

Iteration 3:

  • Search range: l = 7.5, r = 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 achievable → l = 9.375

The binary search continues, narrowing the range until r - l ≤ 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

This demonstrates how the binary search efficiently finds the maximum water level where the available excess water can exactly compensate for both the deficits and the spillage losses.

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        Args:
7            buckets: List of initial water amounts in each bucket
8            loss: Percentage of water lost during transfer (0 to 100)
9      
10        Returns:
11            Maximum achievable water level after equalization
12        """
13      
14        def can_achieve_level(target_level: float) -> bool:
15            """
16            Check if a target water level can be achieved across all buckets.
17          
18            Water can be poured from buckets above the target level to those below.
19            During transfer, (loss)% of water is lost.
20          
21            Args:
22                target_level: The water level we want to achieve
23          
24            Returns:
25                True if the target level is achievable, False otherwise
26            """
27            # Total water available from buckets above target level
28            water_available = 0.0
29            # Total water needed for buckets below target level (accounting for loss)
30            water_needed = 0.0
31          
32            for current_water in buckets:
33                if current_water >= target_level:
34                    # This bucket can provide water
35                    water_available += current_water - target_level
36                else:
37                    # This bucket needs water (accounting for transfer loss)
38                    # To receive (target_level - current_water) units, we need to pour more
39                    water_needed += (target_level - current_water) * 100 / (100 - loss)
40          
41            # Check if we have enough water to satisfy all needs
42            return water_available >= water_needed
43      
44        # Binary search for the maximum achievable water level
45        left = 0.0
46        right = float(max(buckets))
47        epsilon = 1e-5  # Precision threshold
48      
49        # Binary search until we reach desired precision
50        while right - left > epsilon:
51            mid = (left + right) / 2
52          
53            if can_achieve_level(mid):
54                # If this level is achievable, try a higher level
55                left = mid
56            else:
57                # If this level is not achievable, try a lower level
58                right = mid
59      
60        return left
61
1class Solution {
2    /**
3     * Equalizes water across buckets with a given loss percentage during transfer.
4     * Uses binary search to find the maximum achievable water level.
5     * 
6     * @param buckets Array containing initial water amounts in each bucket
7     * @param loss Percentage of water lost during transfer (0-100)
8     * @return Maximum water level achievable after redistribution
9     */
10    public double equalizeWater(int[] buckets, int loss) {
11        // Binary search boundaries: minimum is 0, maximum is the highest water level
12        double left = 0;
13        double right = Arrays.stream(buckets).max().getAsInt();
14      
15        // Binary search with precision of 1e-5
16        while (right - left > 1e-5) {
17            double mid = (left + right) / 2;
18          
19            // Check if target water level 'mid' is achievable
20            if (canAchieveLevel(buckets, loss, mid)) {
21                left = mid;  // If achievable, try higher levels
22            } else {
23                right = mid; // If not achievable, try lower levels
24            }
25        }
26      
27        return left;
28    }
29
30    /**
31     * Checks if a target water level is achievable given the loss percentage.
32     * 
33     * @param buckets Array containing initial water amounts
34     * @param loss Percentage of water lost during transfer
35     * @param targetLevel Target water level to check
36     * @return true if the target level is achievable, false otherwise
37     */
38    private boolean canAchieveLevel(int[] buckets, int loss, double targetLevel) {
39        double excessWater = 0;    // Total water available from buckets above target level
40        double requiredWater = 0;  // Total water needed for buckets below target level (accounting for loss)
41      
42        for (int currentAmount : buckets) {
43            if (currentAmount > targetLevel) {
44                // Bucket has excess water that can be poured out
45                excessWater += currentAmount - targetLevel;
46            } else {
47                // Bucket needs water; account for loss during transfer
48                // If we need X water and lose 'loss'%, we need to pour X * 100 / (100 - loss)
49                requiredWater += (targetLevel - currentAmount) * 100 / (100 - loss);
50            }
51        }
52      
53        // Check if we have enough excess water to meet the requirements
54        return excessWater >= requiredWater;
55    }
56}
57
1class Solution {
2public:
3    double equalizeWater(vector<int>& buckets, int loss) {
4        // Binary search bounds: minimum possible water level and maximum bucket value
5        double left = 0.0;
6        double right = *max_element(buckets.begin(), buckets.end());
7      
8        // Lambda function to check if target water level is achievable
9        // Returns true if we have enough excess water to fill deficits after accounting for loss
10        auto canAchieveLevel = [&](double targetLevel) {
11            double excessWater = 0.0;  // Water available from buckets above target level
12            double deficitWater = 0.0; // Water needed to fill buckets below target level (accounting for loss)
13          
14            for (int bucketAmount : buckets) {
15                if (bucketAmount > targetLevel) {
16                    // This bucket has excess water that can be poured out
17                    excessWater += bucketAmount - targetLevel;
18                } else {
19                    // This bucket needs water, accounting for loss during pouring
20                    // If we need X units after loss, we must pour X * 100 / (100 - loss) units
21                    deficitWater += (targetLevel - bucketAmount) * 100.0 / (100.0 - loss);
22                }
23            }
24          
25            // We can achieve this level if excess water covers the required deficit
26            return excessWater >= deficitWater;
27        };
28      
29        // Binary search for the maximum achievable water level
30        const double EPSILON = 1e-5;
31        while (right - left > EPSILON) {
32            double mid = (left + right) / 2.0;
33          
34            if (canAchieveLevel(mid)) {
35                // Can achieve this level, try for a higher level
36                left = mid;
37            } else {
38                // Cannot achieve this level, lower the target
39                right = mid;
40            }
41        }
42      
43        return left;
44    }
45};
46
1/**
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    // Binary search bounds: minimum possible level and maximum current level
9    let leftBound = 0;
10    let rightBound = Math.max(...buckets);
11  
12    /**
13     * Checks if a target water level is achievable
14     * @param targetLevel - The water level to check
15     * @returns true if the target level can be achieved, false otherwise
16     */
17    const canAchieveLevel = (targetLevel: number): boolean => {
18        let excessWater = 0;  // Water available from buckets above target level
19        let requiredWater = 0; // Water needed for buckets below target level (accounting for loss)
20      
21        for (const currentAmount of buckets) {
22            if (currentAmount >= targetLevel) {
23                // This bucket has excess water that can be transferred
24                excessWater += currentAmount - targetLevel;
25            } else {
26                // This bucket needs water, calculate amount considering transfer loss
27                // If we need X units after loss, we must transfer X * 100 / (100 - loss) units
28                requiredWater += ((targetLevel - currentAmount) * 100) / (100 - loss);
29            }
30        }
31      
32        // Check if we have enough excess water to meet the requirements
33        return excessWater >= requiredWater;
34    };
35  
36    // Binary search for the maximum achievable water level
37    const PRECISION = 1e-5;
38    while (rightBound - leftBound > PRECISION) {
39        const midLevel = (leftBound + rightBound) / 2;
40      
41        if (canAchieveLevel(midLevel)) {
42            // If this level is achievable, try a higher level
43            leftBound = midLevel;
44        } else {
45            // If this level is not achievable, try a lower level
46            rightBound = midLevel;
47        }
48    }
49  
50    return leftBound;
51}
52

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 r - l > 1e-5. Since we start with l = 0 and r = max(buckets), the search space is initially M. The binary search continues until the difference is less than 1e-5, which requires O(log(M/1e-5)) iterations, which simplifies to O(log M) iterations.
  • In each iteration of the binary search, the check(v) function is called, which iterates through all n elements in the buckets array to calculate the water that can be poured out and the water needed to fill up to level v. This takes O(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 l, r, mid, a, b, x, and v, regardless of the input size.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. 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)

2. 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 can_achieve_level(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

3. Floating-Point Precision Issues

Pitfall: Using exact equality comparisons or too tight precision thresholds can cause issues:

while r - l > 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

4. Integer Division in Python 2 vs Python 3

Pitfall: In older Python versions or when mixing integer and float operations:

mid = (left + right) / 2  # Might cause issues in Python 2

Solution: Ensure floating-point division:

mid = (left + right) / 2.0  # Explicit float division
# OR
from __future__ import division  # For Python 2 compatibility

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...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the following is a min heap?


Recommended Readings

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

Load More