Facebook Pixel

1518. Water Bottles

EasyMathSimulation
Leetcode Link

Problem Description

You start with numBottles full water bottles. After drinking a bottle, it becomes empty. You can trade numExchange empty bottles at the market to get 1 new full bottle.

The goal is to find the maximum total number of bottles you can drink.

For example, if you start with numBottles = 9 and numExchange = 3:

  • First, drink all 9 bottles (total drunk: 9, empty bottles: 9)
  • Exchange 9 empty bottles for 3 new full bottles (since 9 ÷ 3 = 3)
  • Drink these 3 bottles (total drunk: 12, empty bottles: 3)
  • Exchange 3 empty bottles for 1 new full bottle (since 3 ÷ 3 = 1)
  • Drink this 1 bottle (total drunk: 13, empty bottles: 1)
  • Can't exchange anymore since 1 < 3

The solution uses a clever approach: instead of simulating the full exchange process, it recognizes that each exchange of numExchange empty bottles gives us 1 full bottle, which effectively reduces our empty bottle count by numExchange - 1 (we lose numExchange empties but gain 1 that becomes empty after drinking). The loop continues exchanging and drinking while we have enough empty bottles to trade.

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

Intuition

The key insight is understanding what happens during each exchange cycle. When we trade numExchange empty bottles for 1 full bottle, we're essentially converting numExchange empties into 1 empty (after drinking the full one we got). This means we net a reduction of numExchange - 1 empty bottles while drinking 1 more bottle.

Think of it this way: if we have exactly numExchange empty bottles, we can:

  1. Trade them for 1 full bottle
  2. Drink that bottle (adding 1 to our answer)
  3. End up with 1 empty bottle

So we went from numExchange empties to 1 empty, a reduction of numExchange - 1.

This pattern continues as long as we have at least numExchange empty bottles to trade. Each iteration:

  • Decreases our empty bottle count by numExchange - 1
  • Increases our drunk bottle count by 1

Instead of tracking empty and full bottles separately through multiple exchanges and drinking cycles, we can simplify: start with drinking all initial bottles (that's our starting ans = numBottles), then keep exchanging and drinking one at a time by reducing our bottle count by numExchange - 1 each round.

The elegance of numBottles -= numExchange - 1 is that it directly computes how many empty bottles remain after one complete exchange-and-drink cycle, avoiding the need to simulate the actual exchange process step by step.

Learn more about Math patterns.

Solution Approach

The implementation uses a greedy simulation approach with a mathematical optimization.

Algorithm Steps:

  1. Initialize the answer: Start with ans = numBottles since we can immediately drink all the initial bottles.

  2. Simulation loop: While we have enough empty bottles to make an exchange (numBottles >= numExchange):

    • Perform one exchange cycle by updating numBottles -= numExchange - 1
      • This represents trading numExchange empties for 1 full bottle, drinking it, and keeping the resulting empty
    • Increment the answer by 1 for the bottle we just drank: ans += 1
  3. Return the result: Once we can't exchange anymore (when numBottles < numExchange), return ans

Why this works:

The formula numBottles -= numExchange - 1 is the mathematical representation of one complete exchange cycle:

  • We start with numBottles empty bottles
  • We use numExchange of them to get 1 full bottle
  • After drinking, we have numBottles - numExchange + 1 empty bottles
  • This simplifies to numBottles - (numExchange - 1)

Time Complexity: O(numBottles / numExchange) - The loop runs approximately numBottles / (numExchange - 1) times since each iteration reduces bottles by numExchange - 1.

Space Complexity: O(1) - We only use a constant amount of extra space for variables.

The beauty of this solution is that it avoids explicitly tracking full and empty bottles separately, instead maintaining just the count of bottles available for exchange after each drinking cycle.

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 trace through an example with numBottles = 10 and numExchange = 3:

Initial State:

  • Start with 10 full bottles
  • Drink all 10 bottles immediately → ans = 10
  • Now we have 10 empty bottles → numBottles = 10

First Exchange Cycle:

  • Check: Can we exchange? 10 >= 3 ✓ Yes
  • Trade 3 empties for 1 full, drink it, keep the empty
  • Update: numBottles = 10 - (3 - 1) = 8 empty bottles remain
  • Update: ans = 10 + 1 = 11 total drunk

Second Exchange Cycle:

  • Check: Can we exchange? 8 >= 3 ✓ Yes
  • Trade 3 empties for 1 full, drink it, keep the empty
  • Update: numBottles = 8 - (3 - 1) = 6 empty bottles remain
  • Update: ans = 11 + 1 = 12 total drunk

Third Exchange Cycle:

  • Check: Can we exchange? 6 >= 3 ✓ Yes
  • Trade 3 empties for 1 full, drink it, keep the empty
  • Update: numBottles = 6 - (3 - 1) = 4 empty bottles remain
  • Update: ans = 12 + 1 = 13 total drunk

Fourth Exchange Cycle:

  • Check: Can we exchange? 4 >= 3 ✓ Yes
  • Trade 3 empties for 1 full, drink it, keep the empty
  • Update: numBottles = 4 - (3 - 1) = 2 empty bottles remain
  • Update: ans = 13 + 1 = 14 total drunk

Termination:

  • Check: Can we exchange? 2 >= 3 ✗ No
  • Stop the loop
  • Final answer: 14 bottles drunk

The key insight: Each cycle effectively converts numExchange empty bottles into 1 drunk bottle plus 1 empty bottle, reducing our empty count by exactly numExchange - 1.

Solution Implementation

1class Solution:
2    def numWaterBottles(self, numBottles: int, numExchange: int) -> int:
3        # Initialize total drunk bottles with initial full bottles
4        total_drunk = numBottles
5      
6        # Keep exchanging while we have enough empty bottles
7        while numBottles >= numExchange:
8            # Exchange numExchange empty bottles for 1 full bottle
9            # Net effect: lose (numExchange - 1) bottles from our count
10            numBottles -= numExchange - 1
11          
12            # Add the newly obtained full bottle to our total
13            total_drunk += 1
14      
15        return total_drunk
16
1class Solution {
2    public int numWaterBottles(int numBottles, int numExchange) {
3        // Total bottles drunk, starting with initial full bottles
4        int totalBottlesDrunk = numBottles;
5      
6        // Keep exchanging while we have enough empty bottles
7        // Each iteration: exchange empty bottles for 1 new full bottle
8        while (numBottles >= numExchange) {
9            // Drink one more bottle (increment total)
10            totalBottlesDrunk++;
11          
12            // Exchange numExchange empty bottles for 1 full bottle
13            // Net effect: reduce empty bottles by (numExchange - 1)
14            numBottles = numBottles - (numExchange - 1);
15        }
16      
17        return totalBottlesDrunk;
18    }
19}
20
1class Solution {
2public:
3    int numWaterBottles(int numBottles, int numExchange) {
4        // Initialize the answer with the initial number of full bottles (all can be drunk)
5        int totalDrunk = numBottles;
6      
7        // Keep exchanging empty bottles for new full bottles while we have enough empties
8        // Each iteration: we exchange 'numExchange' empty bottles for 1 full bottle
9        // This means we effectively reduce empty bottles by (numExchange - 1)
10        // The new full bottle gets drunk immediately, so we increment totalDrunk
11        while (numBottles >= numExchange) {
12            // Reduce empty bottles by (numExchange - 1) since we trade numExchange for 1
13            numBottles -= (numExchange - 1);
14            // Increment total drunk bottles by 1 (the new bottle we got from exchange)
15            totalDrunk++;
16        }
17      
18        return totalDrunk;
19    }
20};
21
1/**
2 * Calculates the total number of water bottles that can be drunk
3 * given initial bottles and exchange rate for empty bottles.
4 * 
5 * @param numBottles - Initial number of full water bottles
6 * @param numExchange - Number of empty bottles needed to exchange for one full bottle
7 * @returns Total number of bottles that can be drunk
8 */
9function numWaterBottles(numBottles: number, numExchange: number): number {
10    // Initialize answer with initial bottles (all can be drunk)
11    let totalBottlesDrunk: number = numBottles;
12  
13    // Keep exchanging while we have enough empty bottles
14    // Each iteration represents one exchange operation
15    while (numBottles >= numExchange) {
16        // Increment total by 1 (we get 1 new bottle from exchange)
17        totalBottlesDrunk++;
18      
19        // Update remaining bottles after exchange
20        // We use numExchange empty bottles and get 1 full bottle back
21        // Net reduction: numExchange - 1
22        numBottles = numBottles - (numExchange - 1);
23    }
24  
25    return totalBottlesDrunk;
26}
27

Time and Space Complexity

Time Complexity: O(numBottles / numExchange) or simplified to O(n/m) where n is the initial number of bottles and m is the exchange rate.

The while loop continues as long as numBottles >= numExchange. In each iteration, numBottles decreases by (numExchange - 1). The number of iterations can be calculated as approximately numBottles / (numExchange - 1). Since numExchange ≥ 2 (based on problem constraints), the worst case would be when numExchange = 2, giving us O(numBottles) iterations. In the general case, it's O(numBottles / numExchange).

Space Complexity: O(1)

The algorithm uses only a constant amount of extra space. It modifies the input variable numBottles and maintains one additional variable ans to track the total number of bottles drunk. No additional data structures or recursive calls are used, so the space complexity remains constant regardless of input size.

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

Common Pitfalls

1. Integer Overflow in Different Approaches

A common alternative approach involves calculating the total mathematically using the formula: numBottles + (numBottles - 1) / (numExchange - 1). This can cause integer overflow for large values of numBottles when numExchange = 2, since we're essentially computing numBottles + numBottles - 1.

Pitfall Example:

# Problematic mathematical approach
def numWaterBottles(numBottles: int, numExchange: int) -> int:
    if numExchange == 2:
        return numBottles * 2 - 1  # Can overflow for large numBottles
    return numBottles + (numBottles - 1) // (numExchange - 1)

Solution: Stick with the iterative simulation approach which naturally handles large numbers without overflow risks.

2. Incorrect Loop Termination Condition

Developers might mistakenly use numBottles > numExchange instead of numBottles >= numExchange, missing the case where we have exactly numExchange empty bottles.

Pitfall Example:

while numBottles > numExchange:  # Wrong! Misses the equality case
    numBottles -= numExchange - 1
    total_drunk += 1

Solution: Use >= to ensure we exchange when we have exactly numExchange bottles:

while numBottles >= numExchange:  # Correct
    numBottles -= numExchange - 1
    total_drunk += 1

3. Misunderstanding the Exchange Logic

Some might incorrectly implement the exchange as dividing bottles by numExchange and adding all obtained bottles at once, forgetting that newly obtained bottles also become empty and can be exchanged again.

Pitfall Example:

def numWaterBottles(numBottles: int, numExchange: int) -> int:
    total = numBottles
    empty = numBottles
    new_bottles = empty // numExchange  # Gets all possible exchanges at once
    total += new_bottles  # Wrong! Doesn't account for recursive exchanges
    return total

Solution: Process exchanges one at a time or properly handle the recursive nature:

def numWaterBottles(numBottles: int, numExchange: int) -> int:
    total = numBottles
    empty = numBottles
  
    while empty >= numExchange:
        new_bottles = empty // numExchange
        total += new_bottles
        empty = empty % numExchange + new_bottles  # Correctly track remaining empties
  
    return total

4. Edge Case: numExchange = 1

While typically not a valid input (would lead to infinite bottles), if numExchange = 1 appears, the current solution would enter an infinite loop since numBottles never decreases.

Solution: Add validation or handle this edge case explicitly:

def numWaterBottles(numBottles: int, numExchange: int) -> int:
    if numExchange == 1:
        return float('inf')  # Or handle according to problem constraints
  
    total_drunk = numBottles
    while numBottles >= numExchange:
        numBottles -= numExchange - 1
        total_drunk += 1
  
    return total_drunk
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following is a good use case for backtracking?


Recommended Readings

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

Load More