3100. Water Bottles II
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 (sonumExchange
becomesnumExchange + 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.
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:
- We should exchange whenever we can (have enough empty bottles), since each exchange lets us drink one more bottle
- The exchange rate gets worse over time (
numExchange
increases after each exchange), so there's no advantage to waiting - 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
- Use
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:
- Deduct the exchange cost:
numBottles -= numExchange
removes the empty bottles used for the exchange - Increase the exchange rate:
numExchange += 1
makes the next exchange more expensive - Drink the new bottle:
ans += 1
counts the newly obtained bottle as drunk - 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 bynumExchange - 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 EvaluatorExample 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 remainingnumExchange = 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 remainingnumExchange = 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 remainingnumExchange = 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 bynumExchange - 1
(since we subtractnumExchange
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.
Which technique can we use to find the middle of a linked list?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
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 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
Want a Structured Path to Master System Design Too? Don’t Miss This!