Problem of The Day: Bag of Tokens
Problem Statement
Brute Force - TLE
class Solution:
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
N = len(tokens)
def dfs(curr_power, curr_score, tokens_arr):
tokens_arr = list(tokens_arr)
if set(tokens_arr) == 1 and '#' in tokens_arr:
return curr_score
res = 0
for i in range(N):
if tokens_arr[i] != '#':
token = tokens_arr[i]
tokens_arr[i] = '#'
if curr_power >= token:
res = max(res, dfs(curr_power - token, curr_score + 1, tuple(tokens_arr)))
if curr_score >= 1:
res = max(res, dfs(curr_power + token, curr_score - 1, tuple(tokens_arr)))
tokens_arr[i] = token
return max(res, curr_score)
return dfs(power, 0, tuple(tokens))
- Time complexity: O(2^N) since we have two choices at each element
- Space complexity: O(2^N) since in the worst case scenario we need to generate all possible combinations.
Memoization - TLE
class Solution:
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
N = len(tokens)
def dfs(curr_power, curr_score, tokens_arr):
if (curr_power, curr_score, tokens_arr) in memo:
return memo[(curr_power, curr_score, tokens_arr)]
tokens_arr = list(tokens_arr)
if set(tokens_arr) == 1 and '#' in tokens_arr:
return curr_score
res = 0
for i in range(N):
if tokens_arr[i] != '#':
token = tokens_arr[i]
tokens_arr[i] = '#'
if curr_power >= token:
res = max(res, dfs(curr_power - token, curr_score + 1, tuple(tokens_arr)))
if curr_score >= 1:
res = max(res, dfs(curr_power + token, curr_score - 1, tuple(tokens_arr)))
tokens_arr[i] = token
memo[(curr_power, curr_score, tuple(tokens_arr))] = max(res, curr_score)
return max(res, curr_score)
memo = defaultdict(tuple)
return dfs(power, 0, tuple(tokens))
- Time complexity: O(2^N) since we have two choices at each element
- Space complexity: O(2^N) since in the worst case scenario we need to generate all possible combinations.
Two pointers Approach - Accepted
Intuition
I approach this problem using a greedy strategy. The idea is to sort the tokens initially and then iterate through them, trying to maximize the score. We maintain two pointers, one starting from the left end of the sorted array and the other from the right end. We try to spend power to gain more points and, if needed, gain power by spending points. The goal is to find the maximum score achievable.
Approach
- Sort the tokens array.
- Initialize two pointers,
l
at the beginning (left end) andr
at the end (right end) of the array. - While
l
is less thanr
and the current power is less than the smallest token in the remaining tokens:- Increment
l
to try to gain more power.
- Increment
- While
l
is less than or equal tor
:- While
l
is less than or equal tor
and the current power is sufficient to pick the token atl
:- Increment the score, spend power, and move
l
to the right.
- Increment the score, spend power, and move
- While
r
is greater thanl
and there is at least one point gained and the current power is insufficient to pick the token atl
:- Gain power by spending the largest token at
r
, decrementr
, and decrement the current score.
- Gain power by spending the largest token at
- Break if
l
is greater than or equal tor
.
- While
- Return the maximum score obtained.
Complexity
-
Time complexity: O(n log n)
-
Space complexity: O(1)
Code
class Solution:
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
N = len(tokens)
tokens.sort()
l, r = 0, N - 1
score = 0
curr_score = 0
while l < r and power < tokens[l]:
l += 1
while l <= r:
while l <= r and power >= tokens[l]:
curr_score += 1
power -= tokens[l]
l +=1
score = max(score, curr_score)
while r > l and curr_score >= 1 and power < tokens[l]:
power += tokens[r]
r -= 1
curr_score -= 1
if l >= r:
break
return score
Editorial Solution
Implementation 1: Two Pointer
class Solution:
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
low = 0
high = len(tokens) - 1
score = 0
tokens.sort()
while low <= high:
# When we have enough power, play lowest token face-up
if power >= tokens[low]:
score += 1
power -= tokens[low]
low += 1
# We don't have enough power to play a token face-up
# If there is at least one token remaining,
# and we have enough score, play highest token face-down
elif low < high and score > 0:
score -= 1
power += tokens[high]
high -= 1
# We don't have enough score, power, or tokens
# to play face-up or down and increase our score
else:
return score
return score
Implementation 2: Deque
class Solution(object):
def bagOfTokensScore(self, tokens: List[int], power: int) -> int:
score = 0
tokens.sort()
deque = collections.deque(tokens)
while deque:
# When we have enough power, play token face-up
if power >= deque[0]:
power -= deque.popleft()
score += 1
# We don't have enough power to play a token face-up
# When there is at least one token remaining,
# and we have enough score, play token face-down
elif len(deque) > 2 and score > 0:
power += deque.pop()
score -= 1
# We don't have enough score, power, or tokens
# to play face-up or down and increase our score
else:
return score
return score
- Time complexity: O(n log n)
- Space complexity: O(n) in python due to
sort