Facebook Pixel

3100. Water Bottles II

MediumMathSimulation
Leetcode Link

Problem Description

You start with numBottles full water bottles and an exchange rate of numExchange empty bottles.

You can perform two types of operations:

  • Drink operation: Drink any number of full water bottles, converting them to empty bottles
  • Exchange operation: Trade exactly numExchange empty bottles for 1 full water bottle. After each exchange, the required number of empty bottles for the next exchange increases by 1 (so numExchange becomes numExchange + 1)

The key constraint is that each exchange must use the exact current value of numExchange empty bottles. You cannot save up bottles to exchange multiple batches at the same rate.

For example, if you have 3 empty bottles and numExchange = 1:

  • You can exchange 1 empty bottle for 1 full bottle (leaving you with 2 empty bottles)
  • Now numExchange = 2, so you'd need exactly 2 empty bottles for the next exchange
  • You can exchange your 2 empty bottles for 1 full bottle
  • Now numExchange = 3, and you only have 1 empty bottle, so no more exchanges are possible

The goal is to find the maximum total number of water bottles you can drink by strategically using these operations.

The solution simulates this process: Start by drinking all initial bottles, then repeatedly exchange empty bottles for full ones whenever possible (when you have at least numExchange empty bottles), drinking each newly obtained bottle and incrementing numExchange after each exchange. The process continues until you don't have enough empty bottles to make another exchange.

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

Intuition

The key insight is that we want to maximize the number of bottles we drink, which means we should exchange empty bottles for full ones whenever possible.

Since we start with numBottles full bottles, we can immediately drink all of them - there's no benefit to holding back because we'll get the same number of empty bottles either way. This gives us numBottles empty bottles to work with.

Now comes the interesting part: each exchange requires exactly numExchange empty bottles and gives us 1 full bottle back. After drinking that bottle, we have 1 new empty bottle. This means each exchange effectively "consumes" numExchange - 1 empty bottles from our collection (we use numExchange bottles but get 1 back).

The greedy approach works here because:

  1. We should exchange whenever we can (have enough empty bottles), since each exchange lets us drink one more bottle
  2. The exchange rate gets worse over time (numExchange increases after each exchange), so there's no advantage to waiting
  3. Each exchange reduces our empty bottle count by numExchange - 1, but also increases the cost of the next exchange by 1

The simulation naturally follows this logic:

  • Start with ans = numBottles (drink all initial bottles)
  • While we have at least numExchange empty bottles:
    • Use numExchange empty bottles to get 1 full bottle
    • Increase numExchange by 1 (cost goes up)
    • Drink the new bottle (ans += 1)
    • Add 1 empty bottle back to our collection

The loop continues until we can't afford the next exchange, at which point we've maximized the bottles drunk.

Learn more about Math patterns.

Solution Approach

The solution uses a simulation approach to directly model the drinking and exchanging process.

We initialize ans = numBottles to track the total bottles drunk, starting by drinking all the initial full bottles. This immediately gives us numBottles empty bottles to work with.

The main loop condition while numBottles >= numExchange checks if we have enough empty bottles to perform an exchange. Inside the loop, we simulate each exchange operation:

  1. Deduct the exchange cost: numBottles -= numExchange removes the empty bottles used for the exchange
  2. Increase the exchange rate: numExchange += 1 makes the next exchange more expensive
  3. Drink the new bottle: ans += 1 counts the newly obtained bottle as drunk
  4. Add the empty bottle back: numBottles += 1 adds the empty bottle from the one we just drank

The elegance of this approach is in how it handles the bottle accounting. When we exchange numExchange empty bottles for 1 full bottle and immediately drink it, the net effect is:

  • We lose numExchange empty bottles
  • We gain 1 empty bottle (from drinking the full one)
  • Net change: numBottles decreases by numExchange - 1

The loop continues until numBottles < numExchange, meaning we no longer have enough empty bottles to afford the next exchange. At this point, we've maximized the number of bottles drunk and return ans.

The algorithm has a time complexity of O(sqrt(numBottles)) because each exchange reduces our empty bottle count while increasing the cost, and the process stops when the increasing cost meets the decreasing bottle count. The space complexity is O(1) as we only use a few variables to track the state.

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

Initial State:

  • Start by drinking all 10 full bottles
  • ans = 10 (total drunk so far)
  • numBottles = 10 (empty bottles we now have)
  • numExchange = 3 (current exchange rate)

First Exchange:

  • Check: Do we have at least 3 empty bottles? Yes (10 ≥ 3)
  • Use 3 empty bottles to get 1 full bottle
  • numBottles = 10 - 3 = 7 empty bottles remaining
  • numExchange = 3 + 1 = 4 (exchange rate increases)
  • Drink the new bottle: ans = 10 + 1 = 11
  • Add the newly empty bottle: numBottles = 7 + 1 = 8

Second Exchange:

  • Check: Do we have at least 4 empty bottles? Yes (8 ≥ 4)
  • Use 4 empty bottles to get 1 full bottle
  • numBottles = 8 - 4 = 4 empty bottles remaining
  • numExchange = 4 + 1 = 5 (exchange rate increases)
  • Drink the new bottle: ans = 11 + 1 = 12
  • Add the newly empty bottle: numBottles = 4 + 1 = 5

Third Exchange:

  • Check: Do we have at least 5 empty bottles? Yes (5 ≥ 5)
  • Use 5 empty bottles to get 1 full bottle
  • numBottles = 5 - 5 = 0 empty bottles remaining
  • numExchange = 5 + 1 = 6 (exchange rate increases)
  • Drink the new bottle: ans = 12 + 1 = 13
  • Add the newly empty bottle: numBottles = 0 + 1 = 1

Check for Fourth Exchange:

  • Do we have at least 6 empty bottles? No (1 < 6)
  • Stop the process

Result: Maximum bottles drunk = 13

The key insight is that each exchange effectively reduces our empty bottle count by (numExchange - 1) while the cost keeps increasing, eventually making further exchanges impossible.

Solution Implementation

1class Solution:
2    def maxBottlesDrunk(self, numBottles: int, numExchange: int) -> int:
3        # Initialize total bottles drunk with initial full bottles
4        total_drunk = numBottles
5      
6        # Keep exchanging while we have enough empty bottles
7        while numBottles >= numExchange:
8            # Use numExchange empty bottles for exchange
9            numBottles -= numExchange
10          
11            # Exchange cost increases by 1 for next exchange
12            numExchange += 1
13          
14            # We get 1 full bottle from the exchange, drink it
15            total_drunk += 1
16          
17            # After drinking, we have 1 more empty bottle
18            numBottles += 1
19      
20        return total_drunk
21
1class Solution {
2    public int maxBottlesDrunk(int numBottles, int numExchange) {
3        // Initialize the total number of bottles drunk with the initial bottles
4        int totalBottlesDrunk = numBottles;
5      
6        // Keep exchanging while we have enough empty bottles for the current exchange rate
7        while (numBottles >= numExchange) {
8            // Use empty bottles for exchange (subtract the required amount)
9            numBottles -= numExchange;
10          
11            // After each exchange, the required number of bottles for next exchange increases
12            numExchange++;
13          
14            // We get 1 full bottle from the exchange, which we drink immediately
15            totalBottlesDrunk++;
16          
17            // The drunk bottle becomes an empty bottle available for future exchanges
18            numBottles++;
19        }
20      
21        // Return the total number of bottles drunk
22        return totalBottlesDrunk;
23    }
24}
25
1class Solution {
2public:
3    int maxBottlesDrunk(int numBottles, int numExchange) {
4        // Initialize total bottles drunk with initial full bottles
5        int totalBottlesDrunk = numBottles;
6      
7        // Keep exchanging empty bottles for full ones while possible
8        while (numBottles >= numExchange) {
9            // Exchange empty bottles for one full bottle
10            numBottles -= numExchange;  // Use empty bottles for exchange
11            ++numExchange;               // Exchange rate increases by 1 each time
12            ++totalBottlesDrunk;         // Drink the newly obtained full bottle
13            ++numBottles;                // Add the empty bottle from drinking
14        }
15      
16        return totalBottlesDrunk;
17    }
18};
19
1/**
2 * Calculates the maximum number of bottles that can be drunk
3 * @param numBottles - Initial number of full bottles
4 * @param numExchange - Initial number of empty bottles needed for exchange
5 * @returns Total number of bottles drunk
6 */
7function maxBottlesDrunk(numBottles: number, numExchange: number): number {
8    // Initialize total bottles drunk with initial full bottles
9    let totalBottlesDrunk = numBottles;
10  
11    // Keep exchanging while we have enough empty bottles
12    while (numBottles >= numExchange) {
13        // Exchange empty bottles for one full bottle
14        numBottles -= numExchange;
15      
16        // Exchange rate increases by 1 after each exchange
17        numExchange++;
18      
19        // Drink the newly obtained bottle
20        totalBottlesDrunk++;
21      
22        // Add the newly drunk bottle to empty bottles count
23        numBottles++;
24    }
25  
26    return totalBottlesDrunk;
27}
28

Time and Space Complexity

Time Complexity: O(√numBottles)

The algorithm runs a while loop that continues as long as numBottles >= numExchange. In each iteration:

  • numBottles decreases by numExchange - 1 (since we subtract numExchange and add back 1)
  • numExchange increases by 1

Let's say the loop runs k times. After k iterations:

  • numBottles will have decreased by approximately (numExchange) + (numExchange + 1) + ... + (numExchange + k - 1) - k
  • This simplifies to k * numExchange + (0 + 1 + 2 + ... + (k-1)) - k = k * numExchange + k(k-1)/2 - k
  • Which equals k * numExchange + k²/2 - 3k/2

The loop terminates when the total reduction approximately equals the initial numBottles. Since the dominant term in the reduction is k²/2, we have:

  • k²/2 ≈ numBottles
  • Therefore, k ≈ √(2 * numBottles)

This gives us a time complexity of O(√numBottles).

Space Complexity: O(1)

The algorithm only uses a constant amount of extra space for the variables ans, and modifies the input parameters numBottles and numExchange in place. No additional data structures are created that grow with the input size.

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

Common Pitfalls

1. Misunderstanding the Exchange Rate Increment

Pitfall: A common mistake is forgetting that numExchange increases after each individual exchange, not after all exchanges at the current rate. Some might incorrectly try to perform multiple exchanges at the same rate if they have enough bottles.

Incorrect approach:

# Wrong: Trying to exchange multiple times at the same rate
while numBottles >= numExchange:
    exchanges_possible = numBottles // numExchange
    numBottles = numBottles % numExchange + exchanges_possible
    total_drunk += exchanges_possible
    numExchange += 1  # Only incrementing once!

Correct approach: The solution correctly increments numExchange after each single exchange operation.

2. Incorrect Initial State Management

Pitfall: Forgetting to drink all initial bottles first, or incorrectly tracking the initial empty bottles.

Incorrect approach:

# Wrong: Not drinking initial bottles first
total_drunk = 0  # Should be numBottles
# Or keeping full and empty bottles separate unnecessarily
full_bottles = numBottles
empty_bottles = 0

Correct approach: The solution immediately drinks all initial bottles (total_drunk = numBottles) and uses the same variable to track empty bottles, simplifying the logic.

3. Off-by-One Error in Bottle Accounting

Pitfall: Incorrectly handling the empty bottle obtained from drinking the exchanged full bottle. Some might forget to add this empty bottle back to the count.

Incorrect approach:

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

This would undercount available empty bottles and terminate the loop prematurely.

Correct approach: The solution properly accounts for the empty bottle by adding 1 back to numBottles after drinking the exchanged bottle.

4. Using Greedy Mathematical Formula Instead of Simulation

Pitfall: Trying to derive a closed-form mathematical formula instead of simulating the process. The increasing exchange rate makes this a sequential dependency problem that requires simulation.

Incorrect approach:

# Wrong: Trying to use a formula
total = numBottles
remaining = numBottles
while remaining > 0:
    # This doesn't account for the increasing exchange rate correctly
    total += remaining // numExchange
    remaining = remaining % numExchange

Correct approach: The solution uses simulation to handle the dynamic exchange rate, ensuring each exchange is processed with the correct rate.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which technique can we use to find the middle of a linked list?


Recommended Readings

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

Load More