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 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
57
1class 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}
63
1class 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};
41
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    /**
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}
48

Solution 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_available accumulates the total excess water from buckets above level v
  • Variable water_needed 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)

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 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: 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

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 with left = 0 and right = 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 feasible(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 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...
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

How does quick sort divide the problem into subproblems?


Recommended Readings

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

Load More