Maximum Points You Can Obtain from Cards
MedPrompt
There are several cards arranged in a row, and each card has an associated number of points. The points are given in the integer array cardPoints.
In one step, you can take one card from the beginning or from the end of the row. You have to take exactly k cards.
Your score is the sum of the points of the cards you have taken.
Given the integer array cardPoints and the integer k, return the maximum score you can obtain.
Example 1:
Input: cardPoints = [1,2,3,4,5,6,1], k = 3
Output: 12
Explanation: After the first step, your score will always be 1. However, choosing the rightmost card first will maximize your total score. The optimal strategy is to take the three cards on the right, giving a final score of 1 + 6 + 5 = 12.Example 2:
Input: cardPoints = [2,2,2], k = 2
Output: 4
Explanation: Regardless of which two cards you take, your score will always be 4.Example 3:
Input: cardPoints = [9,7,7,9,7,7,9], k = 7
Output: 55
Explanation: You have to take all the cards. Your score is the sum of points of all cards.
Constraints:
1 <= cardPoints.length <= 1051 <= cardPoints[i] <= 1041 <= k <= cardPoints.length
Approaches
3 approaches with complexity analysis and trade-offs.
This approach directly models the problem statement. At each step, we have two choices: take a card from the left end or the right end. We need to make k such choices. A recursive function can explore all possible sequences of choices and find the one that yields the maximum score.
Algorithm
- Define a recursive function
findMaxScore(points, left, right, k). - Base Case: If
k == 0, return 0. - Recursive Step:
scoreLeft = points[left] + findMaxScore(points, left + 1, right, k - 1).scoreRight = points[right] + findMaxScore(points, left, right - 1, k - 1).- Return
max(scoreLeft, scoreRight).
- Initial call:
findMaxScore(cardPoints, 0, n-1, k).
Walkthrough
We define a recursive function, say findMaxScore(points, left, right, k), which calculates the maximum score obtainable from the subarray points[left...right] by taking k more cards.
The base case for the recursion is when k becomes 0, meaning we have taken the required number of cards. In this case, the score to be added is 0, so we return 0.
In the recursive step, we explore the two possible moves:
- Take the card at the
leftindex: The score for this choice ispoints[left]plus the result of the recursive call on the smaller subarraypoints[left + 1...right]withk-1cards to take. - Take the card at the
rightindex: The score for this choice ispoints[right]plus the result of the recursive call on the smaller subarraypoints[left...right - 1]withk-1cards to take. The function returns the maximum of these two scores. The initial call to this function would befindMaxScore(cardPoints, 0, cardPoints.length - 1, k). This method explores a binary tree of choices of depthk, leading to an exponential number of computations.
class Solution { public int maxScore(int[] cardPoints, int k) { return findMaxScore(cardPoints, 0, cardPoints.length - 1, k); } private int findMaxScore(int[] points, int left, int right, int k) { // Base case: no more cards to take if (k == 0) { return 0; } // Option 1: Take the leftmost card int takeLeft = points[left] + findMaxScore(points, left + 1, right, k - 1); // Option 2: Take the rightmost card int takeRight = points[right] + findMaxScore(points, left, right - 1, k - 1); // Return the maximum of the two options return Math.max(takeLeft, takeRight); }}Complexity
Time
O(2^k). The recursion tree has a depth of `k`, and each node branches into two, resulting in `2^k` calls at the last level.
Space
O(k). The depth of the recursion stack can go up to `k`.
Trade-offs
Pros
It's a straightforward translation of the problem's decision-making process.
Simple to conceptualize and implement.
Cons
Extremely inefficient due to its exponential time complexity.
Will result in a 'Time Limit Exceeded' error for all but the smallest values of
k.
A more clever way to look at the problem is to realize that taking k cards from the ends is equivalent to leaving a contiguous subarray of n-k cards in the middle. To maximize the sum of the cards taken, we must minimize the sum of the cards left behind. This transforms the problem into finding the minimum sum subarray of a fixed size n-k.
Algorithm
- Calculate
totalSumof all elements incardPoints. - If
k == n, returntotalSum. - Set
windowSize = n - k. - Calculate the sum of the first window
cardPoints[0...windowSize-1]and store it inminSumandcurrentSum. - Iterate from
i = windowSizeton-1:- Update
currentSumby addingcardPoints[i]and subtractingcardPoints[i - windowSize]. - Update
minSum = min(minSum, currentSum).
- Update
- Return
totalSum - minSum.
Walkthrough
First, we calculate the total sum of all points in the cardPoints array.
The size of the subarray that will be left is windowSize = n - k. Our goal is to find a subarray of this size with the minimum possible sum.
We can use the sliding window technique. We start by calculating the sum of the first window of size windowSize (from index 0 to windowSize - 1). Let's call this currentSum, and initialize minSum with this value.
Then, we slide this window one element at a time to the right until it reaches the end of the array. For each slide, we update currentSum efficiently by subtracting the element that is leaving the window and adding the new element that is entering.
After each slide, we compare the currentSum with minSum and update minSum if the currentSum is smaller.
After iterating through all possible windows, minSum will hold the minimum sum of any contiguous subarray of size n-k.
The maximum score we can obtain is the totalSum minus this minSum.
A special case is when k is equal to n, in which case we must take all cards, and the score is simply the totalSum.
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; int totalSum = 0; for (int point : cardPoints) { totalSum += point; } if (k == n) { return totalSum; } int windowSize = n - k; int minSubarraySum = 0; // Calculate sum of the first window for (int i = 0; i < windowSize; i++) { minSubarraySum += cardPoints[i]; } int currentSubarraySum = minSubarraySum; // Slide the window to find the minimum sum subarray for (int i = windowSize; i < n; i++) { currentSubarraySum += cardPoints[i] - cardPoints[i - windowSize]; minSubarraySum = Math.min(minSubarraySum, currentSubarraySum); } return totalSum - minSubarraySum; }}Complexity
Time
O(n). We iterate through the array a constant number of times.
Space
O(1). We only use a few variables to store sums and indices.
Trade-offs
Pros
Very efficient with linear time complexity.
Space-efficient as it uses constant extra space.
Solves the problem within the given constraints.
Cons
The logic is indirect and requires reframing the problem, which might not be immediately obvious.
This approach directly calculates the sums of possible card combinations. We observe that any valid selection of k cards is composed of i cards from the left and k-i cards from the right. We can start with an initial selection (e.g., all k cards from the left) and then iteratively modify this selection.
Algorithm
- Calculate the sum of the first
kelements. Store this incurrentSumandmaxSum. - Iterate
ifrom0tok-1:- This loop simulates changing the pick from
(k-1-i)left cards and(i+1)right cards. - Update
currentSumby subtractingcardPoints[k-1-i](the card no longer taken from the left) and addingcardPoints[n-1-i](the new card taken from the right). - Update
maxSum = max(maxSum, currentSum).
- This loop simulates changing the pick from
- Return
maxSum.
Walkthrough
We can think of this as a sliding window of size k that wraps around the ends of the array.
First, calculate an initial sum by taking the first k cards from the left. This corresponds to taking k cards from the left and 0 from the right. Let this be currentSum, and initialize maxSum with this value.
Now, we iterate k times. In each iteration, we simulate taking one less card from the left and one more card from the right.
For example, in the first iteration, we change our selection from k left cards to k-1 left cards and 1 right card. To update the sum, we subtract the rightmost card of our initial left selection (cardPoints[k-1]) and add the rightmost card from the whole array (cardPoints[n-1]).
We continue this process: in each step i, we subtract cardPoints[k-1-i] and add cardPoints[n-1-i].
After each update to currentSum, we compare it with maxSum and update maxSum if currentSum is larger.
After k iterations, we will have considered all k+1 possible splits (from k left/0 right to 0 left/k right), and maxSum will hold the maximum possible score.
class Solution { public int maxScore(int[] cardPoints, int k) { int n = cardPoints.length; int currentSum = 0; // Calculate the initial sum of the first k cards (k from left, 0 from right) for (int i = 0; i < k; i++) { currentSum += cardPoints[i]; } int maxSum = currentSum; // Slide the window. In each step, we remove one card from the left end // of our selection and add one card from the right end of the array. for (int i = 0; i < k; i++) { // Remove cardPoints[k-1-i] and add cardPoints[n-1-i] currentSum = currentSum - cardPoints[k - 1 - i] + cardPoints[n - 1 - i]; maxSum = Math.max(maxSum, currentSum); } return maxSum; }}Complexity
Time
O(k). We have an initial loop to sum the first `k` elements, and a second loop that runs `k` times.
Space
O(1). Constant extra space is used for variables.
Trade-offs
Pros
The most time-efficient solution, as its runtime depends on
k, notn.Very space-efficient.
The logic directly manipulates the selections, which can be intuitive.
Cons
The indexing in the loop (
k-1-iandn-1-i) can be slightly tricky to get right initially.
Solutions
Solution
/** * @param {number[]} cardPoints * @param {number} k * @return {number} */ var maxScore = function (cardPoints, k) { const n = cardPoints.length; let s = cardPoints.slice(-k).reduce((a, b) => a + b); let ans = s; for (let i = 0; i < k; ++i) { s += cardPoints[i] - cardPoints[n - k + i]; ans = Math.max(ans, s); } return ans; };class Solution { public int maxScore ( int [] cardPoints , int k ) { int s = 0 , n = cardPoints . length ; for ( int i = n - k ; i < n ; ++ i ) { s += cardPoints [ i ]; } int ans = s ; for ( int i = 0 ; i < k ; ++ i ) { s += cardPoints [ i ] - cardPoints [ n - k + i ]; ans = Math . max ( ans , s ); } return ans ; } }public class Solution { public int MaxScore ( int [] cardPoints , int k ) { int n = cardPoints . Length ; int s = cardPoints [^ k ..]. Sum (); int ans = s ; for ( int i = 0 ; i < k ; ++ i ) { s += cardPoints [ i ] - cardPoints [ n - k + i ]; ans = Math . Max ( ans , s ); } return ans ; } }class Solution { public: int maxScore ( vector < int >& cardPoints , int k ) { int n = cardPoints . size (); int s = accumulate ( cardPoints . end () - k , cardPoints . end (), 0 ); int ans = s ; for ( int i = 0 ; i < k ; ++ i ) { s += cardPoints [ i ] - cardPoints [ n - k + i ]; ans = max ( ans , s ); } return ans ; } };class Solution : def maxScore ( self , cardPoints : List [ int ], k : int ) -> int : ans = s = sum ( cardPoints [ - k :]) for i , x in enumerate ( cardPoints [: k ]): s += x - cardPoints [ - k + i ] ans = max ( ans , s ) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.