948. Bag of Tokens
Problem Description
You have a bag of tokens, each with a specific value, and you start with some initial power and a score of 0. Your goal is to maximize your score by strategically playing these tokens.
Each token can be played in one of two ways (but only once per token):
-
Face-up: If you have enough power (at least
tokens[i]
), you can spendtokens[i]
power to gain 1 score point. -
Face-down: If you have at least 1 score point, you can spend 1 score point to gain
tokens[i]
power.
The strategy involves finding the optimal way to play tokens to achieve the maximum possible score. You can play any number of tokens (including none) and in any order.
For example, if you have tokens [100, 200, 300, 400]
and initial power of 200
:
- You could play token
100
face-up (spend 100 power, gain 1 score), leaving you with 100 power and 1 score - Then play token
400
face-down (spend 1 score, gain 400 power), leaving you with 500 power and 0 score - Then play tokens
200
and300
face-up to end with 2 score
The solution uses a greedy approach with two pointers after sorting. It tries to gain score by playing smaller tokens face-up (using minimal power) and when power runs low, it plays larger tokens face-down (trading score for maximum power gain). This ensures we get the most score possible from our available resources.
Intuition
The key insight is recognizing that we want to maximize score while being strategic about power management. Since playing face-up costs power but gains score, and playing face-down costs score but gains power, we need to find the optimal trade-off.
Think of it like buying and selling: we want to "buy" score points as cheaply as possible (using minimal power) and when we need more power, we want to "sell" score points for maximum power gain.
This naturally leads to a greedy strategy:
- When gaining score (face-up), use the tokens that cost the least power
- When gaining power (face-down), use the tokens that give the most power
By sorting the tokens, we create this optimal arrangement: smallest values on the left (cheap to play face-up) and largest values on the right (valuable to play face-down).
The two-pointer approach emerges from this insight. We maintain pointers at both ends:
- Left pointer (
i
) tracks the cheapest unplayed token for gaining score - Right pointer (
j
) tracks the most valuable unplayed token for gaining power
At each step, we make a greedy decision:
- If we have enough power for
tokens[i]
, play it face-up to gain score (this is always beneficial since we're using the cheapest available token) - If we don't have enough power but have score to spare, play
tokens[j]
face-down to gain maximum power (this gives us the best "exchange rate" for our score point) - If we can't do either, we're stuck and should stop
We track the maximum score achieved at any point because sometimes it's optimal to end with leftover power rather than trading more score for power that won't be fully utilized.
Learn more about Greedy, Two Pointers and Sorting patterns.
Solution Approach
The implementation follows a greedy strategy with sorting and two pointers:
-
Sort the tokens array: First, we sort
tokens
in ascending order. This arrangement allows us to access the cheapest tokens from the left and the most valuable tokens from the right. -
Initialize variables:
ans
tracks the maximum score achieved at any pointscore
tracks the current scorei
andj
are two pointers starting at the beginning and end of the sorted array respectively
-
Main loop with two-pointer technique: While
i <= j
(meaning we still have unplayed tokens):a. Try to play face-up (gain score): If
power >= tokens[i]
, we have enough power to play the cheapest available token:- Decrease power by
tokens[i]
- Increase score by 1
- Move left pointer
i
forward - Update
ans = max(ans, score)
to track the maximum score
b. Try to play face-down (gain power): If we don't have enough power for the cheapest token but have at least 1 score point:
- Increase power by
tokens[j]
(the most valuable token) - Decrease score by 1
- Move right pointer
j
backward
c. Cannot play: If we have neither enough power for the cheapest token nor any score to trade, break out of the loop
- Decrease power by
-
Return the maximum score: Return
ans
, which holds the highest score achieved during the process.
The algorithm ensures optimal play by always:
- Spending the least amount of power when gaining score (using smallest tokens)
- Gaining the maximum amount of power when trading score (using largest tokens)
- Tracking the peak score since the final score might not be the maximum (we might trade score for power that doesn't get fully utilized)
Time complexity: O(n log n)
for sorting, where n
is the number of tokens.
Space complexity: O(1)
if we don't count the sorting space.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with tokens = [100, 200, 300, 400]
and power = 250
.
Step 1: Sort the tokens
Tokens are already sorted: [100, 200, 300, 400]
Step 2: Initialize variables
ans = 0
(maximum score tracker)score = 0
(current score)i = 0
(left pointer at token 100)j = 3
(right pointer at token 400)power = 250
Step 3: Main loop iterations
Iteration 1: i = 0, j = 3
- Check: Can play face-up?
power (250) >= tokens[0] (100)
? Yes! - Play token 100 face-up:
power = 250 - 100 = 150
,score = 0 + 1 = 1
- Move
i
to 1 - Update
ans = max(0, 1) = 1
Iteration 2: i = 1, j = 3
- Check: Can play face-up?
power (150) >= tokens[1] (200)
? No - Check: Can play face-down?
score (1) > 0
? Yes! - Play token 400 face-down:
power = 150 + 400 = 550
,score = 1 - 1 = 0
- Move
j
to 2
Iteration 3: i = 1, j = 2
- Check: Can play face-up?
power (550) >= tokens[1] (200)
? Yes! - Play token 200 face-up:
power = 550 - 200 = 350
,score = 0 + 1 = 1
- Move
i
to 2 - Update
ans = max(1, 1) = 1
Iteration 4: i = 2, j = 2
- Check: Can play face-up?
power (350) >= tokens[2] (300)
? Yes! - Play token 300 face-up:
power = 350 - 300 = 50
,score = 1 + 1 = 2
- Move
i
to 3 - Update
ans = max(1, 2) = 2
Loop ends: i (3) > j (2)
Result: Maximum score = 2
The strategy worked by first playing the cheapest token (100) to gain initial score, then trading that score for maximum power (400), and finally using that power to play the remaining tokens (200 and 300) to maximize the final score.
Solution Implementation
1class Solution:
2 def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
3 # Sort tokens in ascending order for greedy approach
4 tokens.sort()
5
6 # Initialize variables
7 max_score = 0 # Track the maximum score achieved
8 current_score = 0 # Current score in the game
9 left = 0 # Left pointer for smallest tokens
10 right = len(tokens) - 1 # Right pointer for largest tokens
11
12 # Use two-pointer technique to maximize score
13 while left <= right:
14 # If we have enough power, play the smallest token face-up
15 # This gains us 1 score and costs the least power
16 if power >= tokens[left]:
17 power -= tokens[left]
18 current_score += 1
19 left += 1
20 # Update maximum score achieved
21 max_score = max(max_score, current_score)
22
23 # If we don't have enough power but have score to spend,
24 # play the largest token face-down to gain maximum power
25 elif current_score > 0:
26 power += tokens[right]
27 current_score -= 1
28 right -= 1
29
30 # If we have neither enough power nor score, we're done
31 else:
32 break
33
34 return max_score
35
1class Solution {
2 public int bagOfTokensScore(int[] tokens, int power) {
3 // Sort tokens in ascending order to optimize the greedy approach
4 Arrays.sort(tokens);
5
6 // Track the maximum score achieved and current score
7 int maxScore = 0;
8 int currentScore = 0;
9
10 // Use two pointers: left for smallest tokens, right for largest tokens
11 int left = 0;
12 int right = tokens.length - 1;
13
14 // Process tokens while there are still tokens to consider
15 while (left <= right) {
16 // Case 1: If we have enough power, play the smallest token face-up
17 // This gains us 1 score and costs the least power
18 if (power >= tokens[left]) {
19 power -= tokens[left];
20 left++;
21 currentScore++;
22 // Update maximum score achieved so far
23 maxScore = Math.max(maxScore, currentScore);
24 }
25 // Case 2: If we don't have enough power but have score,
26 // play the largest token face-down to gain maximum power
27 else if (currentScore > 0) {
28 power += tokens[right];
29 right--;
30 currentScore--;
31 }
32 // Case 3: No power to play face-up and no score to play face-down
33 // No more moves possible
34 else {
35 break;
36 }
37 }
38
39 return maxScore;
40 }
41}
42
1class Solution {
2public:
3 int bagOfTokensScore(vector<int>& tokens, int power) {
4 // Sort tokens in ascending order for greedy approach
5 sort(tokens.begin(), tokens.end());
6
7 // Initialize variables
8 int maxScore = 0; // Track the maximum score achieved
9 int currentScore = 0; // Current score during the process
10
11 // Use two pointers: left for smallest tokens, right for largest tokens
12 int left = 0;
13 int right = tokens.size() - 1;
14
15 // Process tokens while valid tokens remain
16 while (left <= right) {
17 // Case 1: Have enough power to play smallest token face-up
18 // Spend power to gain score (greedy: use smallest token)
19 if (power >= tokens[left]) {
20 power -= tokens[left];
21 left++;
22 currentScore++;
23 maxScore = max(maxScore, currentScore);
24 }
25 // Case 2: Not enough power but have score to trade
26 // Trade score for power (greedy: gain power from largest token)
27 else if (currentScore > 0) {
28 power += tokens[right];
29 right--;
30 currentScore--;
31 }
32 // Case 3: No power and no score to trade
33 // Cannot make any more moves
34 else {
35 break;
36 }
37 }
38
39 return maxScore;
40 }
41};
42
1/**
2 * Calculates the maximum score achievable from a bag of tokens game.
3 * Rules:
4 * - Face up: Spend token value from power to gain 1 score
5 * - Face down: Gain token value to power by spending 1 score
6 *
7 * @param tokens - Array of token values
8 * @param power - Initial power available
9 * @returns Maximum score achievable
10 */
11function bagOfTokensScore(tokens: number[], power: number): number {
12 // Sort tokens in ascending order for greedy strategy
13 tokens.sort((a: number, b: number) => a - b);
14
15 // Initialize two pointers for smallest and largest tokens
16 let leftIndex: number = 0;
17 let rightIndex: number = tokens.length - 1;
18
19 // Track maximum score achieved and current score
20 let maxScore: number = 0;
21 let currentScore: number = 0;
22
23 // Process tokens using two-pointer technique
24 while (leftIndex <= rightIndex) {
25 // If we have enough power, play smallest token face up
26 if (power >= tokens[leftIndex]) {
27 power -= tokens[leftIndex];
28 leftIndex++;
29 currentScore++;
30 // Update maximum score if current is higher
31 maxScore = Math.max(maxScore, currentScore);
32 }
33 // If we don't have enough power but have score, play largest token face down
34 else if (currentScore > 0) {
35 power += tokens[rightIndex];
36 rightIndex--;
37 currentScore--;
38 }
39 // No valid moves available, exit loop
40 else {
41 break;
42 }
43 }
44
45 return maxScore;
46}
47
Time and Space Complexity
Time Complexity: O(n log n)
The time complexity is dominated by the sorting operation tokens.sort()
which takes O(n log n)
time where n
is the number of tokens. After sorting, the while loop runs with two pointers i
and j
moving towards each other. In the worst case, each token is visited at most once, taking O(n)
time. Therefore, the overall time complexity is O(n log n) + O(n) = O(n log n)
.
Space Complexity: O(log n)
The space complexity comes from the sorting algorithm. Python's sort()
method uses Timsort, which has a worst-case space complexity of O(n)
but typically uses O(log n)
space for the recursion stack in practice. The algorithm itself only uses a constant amount of extra space for variables (ans
, score
, i
, j
), which is O(1)
. Therefore, the overall space complexity is O(log n)
due to the sorting operation.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Returning Current Score Instead of Maximum Score
A frequent mistake is returning current_score
at the end instead of tracking and returning the maximum score achieved during the process. The current score at the end might be lower than the peak score reached during gameplay.
Incorrect Implementation:
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
tokens.sort()
current_score = 0
left, right = 0, len(tokens) - 1
while left <= right:
if power >= tokens[left]:
power -= tokens[left]
current_score += 1
left += 1
elif current_score > 0:
power += tokens[right]
current_score -= 1
right -= 1
else:
break
return current_score # Wrong! Returns final score, not maximum
Why This Fails: Consider tokens = [100, 200] with power = 150:
- Play token 100 face-up: score = 1, power = 50
- Can't play token 200 face-up (insufficient power)
- Play token 200 face-down: score = 0, power = 250
- Final score is 0, but maximum achieved was 1
Correct Approach:
Always track the maximum score with max_score = max(max_score, current_score)
after gaining points, and return max_score
instead of current_score
.
2. Updating Maximum Score at the Wrong Time
Another pitfall is updating the maximum score after both face-up and face-down plays, or only at the end of the loop.
Incorrect Implementation:
while left <= right:
if power >= tokens[left]:
power -= tokens[left]
current_score += 1
left += 1
elif current_score > 0:
power += tokens[right]
current_score -= 1
right -= 1
max_score = max(max_score, current_score) # Wrong timing!
else:
break
Why This Fails: The maximum score should only be updated when we gain score (face-up play), not when we lose score (face-down play). Updating after face-down plays would miss capturing the peak score before it decreases.
Correct Approach:
Only update max_score
immediately after increasing current_score
in the face-up branch:
if power >= tokens[left]:
power -= tokens[left]
current_score += 1
left += 1
max_score = max(max_score, current_score) # Update only here
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Tech Interview Pattern Two Pointers Introduction If you prefer videos here's a super quick introduction to Two Pointers div class responsive iframe iframe src https www youtube com embed xZ4AfXHQ1VQ title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture allowfullscreen iframe div Two pointers is a common interview
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
Want a Structured Path to Master System Design Too? Donβt Miss This!