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, lat the beginning (left end) andrat the end (right end) of the array.
- While lis less thanrand the current power is less than the smallest token in the remaining tokens:- Increment lto try to gain more power.
 
- Increment 
- While lis less than or equal tor:- While lis less than or equal torand the current power is sufficient to pick the token atl:- Increment the score, spend power, and move lto the right.
 
- Increment the score, spend power, and move 
- While ris greater thanland 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 lis 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
