Coin Game

Coin Game

You and a friend are playing a game with n coins arranged in a straight line, where each coin has a distinct value given by coins[i]. Starting with you, both players take turns, picking one coin from either end of the line and adding its value to their individual scores. Once a coin is picked up, it's removed from the line.

Given that your friend plays optimally to achieve the highest score, determine your maximum possible score.

Input

  • coins: A list of the coins.

Output

Your maximum possible score, provided that you go first and your friend plays perfectly.

Examples

Example 1:

Input:

1coins = [4, 4, 9, 4]

Output: 13

Explanation:

The coins start like this:

4, 4, 9, 4

You always go first, so you take the 4 from the left side:

4, 9, 4

Your friend takes any of the 4s (it doesn't matter)

9, 4

Now you take the 9, and your friend takes the remaining 4.

Your score in this case, is 4 + 9 = 13.

Constraints

  • 1 <= len(coins) <= 1000
  • 1 <= coins[i] <= 5 * 10^5

Try it yourself

Solution

Brute Force

A brute force solution would enumerate through all possibilities. For each of the n turns, we either choose the left-most coin or the right-most coin and check which option maximizes our score.

The 2 cases mentioned above are described as follows:

  • Case 1: We take coin l
    • Coins in the range [l + 1, r] are left
    • Since our opponent plays optimally, they will gain points equal to maxScore(l + 1, r)
    • Since we get all other coins, our score will be sum(l, r) - maxScore(l + 1, r)
  • Case 2: We take coin r
    • Coins in range [l, r - 1] are left
    • Since our opponent plays optimally, they will gain points equal to maxScore(l, r - 1)
    • Since we get all other coins, our score will be sum(l, r) - maxScore(l, r - 1)

Next, we choose the case that gives us the greatest score, or minimizes the opponent's score. Therefore, the solution is either:

  • maxScore(l, r) = max(sum(l, r) - maxScore(l + 1, r), sum(l, r) - maxscore(l, r - 1)) or
  • maxScore(l, r) = sum(l, r) - min(maxScore(l + 1, r), maxScore(l, r - 1))

Note that a common pitfall is to try to keep tracking of which player the current recursive call is for using an extra state variable. This is unnecessary because it doesn't really matter which user the recursive call is for because the current player always tries to minimize the other player's score (each user "plays perfectly in such a way that maximizes their score"). Each recursive call returns the best possible score the current player can get. The outermost call will return the best score for the first player.

Since there are n turns, 2 possibilities each turn, and takes O(n) to calculate the sum from l to r, the final runtime is O(n * 2^n).

Here's a visual representation of the idea:

The following is an implementation of the idea above:

1int max_score(vector<int> coins, int l, int r) {
2    // Base case: If only one coin is present, return its value
3    if (l == r) {
4        return coins[r];
5    }
6
7    // Calculate the sum of the coins in the range [l, r]
8    int sum = 0;
9    for (int i = l; i <= r; i++) {
10        sum += coins[i];
11    }
12
13    // Calculate the score when choosing the leftmost or rightmost coin
14    int left_pick = max_score(coins, l + 1, r);
15    int right_pick = max_score(coins, l, r - 1);
16
17    // Return the best score after choosing the optimal coin
18    return sum - min(left_pick, right_pick);
19}
20
21int coin_game(vector<int> coins) {
22    int n = coins.size();
23    return max_score(coins, 0, n - 1);
24}
25
1public static int maxScore(List<Integer> coins, int l, int r) {
2    // Base case: If only one coin is present, return its value
3    if (l == r) {
4        return coins.get(r);
5    }
6
7    // Calculate the sum of the coins in the range [l, r]
8    int sum = 0;
9    for (int i = l; i <= r; i++) {
10        sum += coins.get(i);
11    }
12
13    // Calculate the score when choosing the leftmost or rightmost coin
14    int leftPick = maxScore(coins, l + 1, r);
15    int rightPick = maxScore(coins, l, r - 1);
16
17    // Return the best score after choosing the optimal coin
18    return sum - Math.min(leftPick, rightPick);
19}
20
21public static int coinGame(List<Integer> coins) {
22    int n = coins.size();
23    return maxScore(coins, 0, n - 1);
24}
25
1function maxScore(coins, l, r) {
2    // Base case: If only one coin is present, return its value
3    if (l === r) {
4        return coins[r];
5    }
6
7    // Calculate the sum of the coins in the range [l, r]
8    var sum = 0;
9    for (let i = l; i <= r; i++) {
10        sum += coins[i];
11    }
12
13    // Calculate the score when choosing the leftmost or rightmost coin
14    var leftPick = maxScore(coins, l + 1, r);
15    var rightPick = maxScore(coins, l, r - 1);
16
17    // Return the best score after choosing the optimal coin
18    return sum - Math.min(leftPick, rightPick);
19}
20
21function coinGame(coins) {
22    let n = coins.length;
23    return maxScore(coins, 0, n - 1);
24}
25
1def max_score(coins, l, r):
2    # Base case: If only one coin is present, return its value
3    if l == r:
4        return coins[r]
5
6    # Calculate the sum of the coins in the range [l, r]
7    sum = 0
8    for i in range(l, r + 1):
9        sum += coins[i]
10
11    # Calculate the score when choosing the leftmost or rightmost coin
12    left_pick = max_score(coins, l + 1, r)
13    right_pick = max_score(coins, l, r - 1)
14
15    # Return the best score after choosing the optimal coin
16    return sum - min(left_pick, right_pick)
17
18def coin_game(coins):
19    n = len(coins)
20    return max_score(coins, 0, n - 1)
21

Slight Optimization

So far, for each recursive call we are spending O(n) time to calculate the sum between the range [l, r]. However, instead of using O(n) every time we need the sum we can use a prefix sum array to get the range sum in O(1) time and a single O(n) pre-computation.

1
1
1int max_score(vector<int> coins, int l, int r) {
2
+
1  int sum = coins[r] - coins[l - 1]; // query sum from [l, r] in O(1)
2
3
1  if (l == r) {
3
-
1    return coins[r];
4
+
1    return sum;
4
5
1  }
5
6
6
-
1  int sum = 0;
7
-
1  for (int i = l; i <= r; i++) {
8
-
1    sum += coins[i];
9
-
1  }
10
-
1 
11
7
1  int left_pick = max_score(coins, l + 1, r);
12
8
1  int right_pick = max_score(coins, l, r - 1);
13
9
1  return sum - min(left_pick, right_pick);
14
10
1}
15
11
1int coin_game(vector<int> coins) {
16
12
1  int n = coins.size();
17
-
1  return max_score(coins, 0, n - 1);
13
+
1  vector<int> prefix_sum(n + 1, 0); // precompute prefix sum
14
+
1  for (int i = 1; i <= n; i++) {
15
+
1    prefix_sum[i] = prefix_sum[i - 1] + coins[i -1];
16
+
1  }
17
+
18
+
1  return max_score(prefix_sum, 1, n);
18
19
1}
1
1
1public static int maxScore(List<Integer> coins, int l, int r) {
2
+
1  int sum = coins.get(r) - coins.get(l - 1); // query sum from [l, r] in O(1)
2
3
1  if (l == r) {
3
-
1    return coins.get(r);
4
+
1    return sum;
4
5
1  }
5
-
1  int sum = 0;
6
-
1  for (int i = l; i <= r; i++) {
7
-
1    sum += coins.get(i);
8
-
1  }
9
-
1 
10
6
1  int leftPick = maxScore(coins, l + 1, r);
11
7
1  int rightPick = maxScore(coins, l, r - 1);
12
8
1  return sum - Math.min(leftPick, rightPick);
13
9
1}
14
10
1public static int coinGame(List<Integer> coins) {
15
11
1  int n = coins.size();
16
-
1  return maxScore(coins, 0, n - 1);
12
+
1  List<Integer> prefixSum = new ArrayList<>(); // precompute prefix sums
13
+
1  prefixSum.add(0);
14
+
1  for (int i = 1; i <= n; i++) {
15
+
1    prefixSum.add(prefixSum.get(i - 1) + coins.get(i - 1));
16
+
1  }
17
+
1  return maxScore(prefixSum, 1, n);
17
18
1}
1
1
1function maxScore(coins, l, r) {
2
+
1  var sum = coins[r] - coins[l - 1];
2
3
1  if (l === r) {
3
-
1    return coins[r];
4
+
1    return sum;
4
5
1  }
5
6
6
-
1  var sum = 0;
7
-
1  for (let i = l; i <= r; i++) {
8
-
1    sum += coins[i];
9
-
1  }
10
-
1 
11
7
1  var leftPick = maxScore(coins, l + 1, r);
12
8
1  var rightPick = maxScore(coins, l, r - 1);
13
9
1  return sum - Math.min(leftPick, rightPick);
14
10
1}
Expand 1 lines ...
16
12
1function coinGame(coins) {
17
13
1  let n = coins.length;
18
-
1  return maxScore(coins, 0, n - 1);
14
+
1  var prefixSum = [0];
15
+
1  for (let i = 1; i <= n; i++) {
16
+
1    prefixSum.push(prefixSum[i - 1] + coins[i - 1]);
17
+
1  }
18
+
1  return maxScore(prefixSum, 1, n);
19
19
1}
1
1
1def max_score(coins, l, r):
2
+
1  sum = coins[r] - coins[l - 1] # query sum from [l, r] in O(1)
2
3
1  if l == r:
3
-
1    return coins[r]
4
+
1    return sum
4
5
5
6
6
-
1  sum = 0
7
-
1  for i in range(l, r + 1):
8
-
1    sum += coins[i]
9
-
1 
10
7
1  left_pick = max_score(coins, l + 1, r)
11
8
1  right_pick = max_score(coins, l, r - 1)
12
9
1  return sum - min(left_pick, right_pick)
13
10
1def coin_game(coins):
14
11
1  n = len(coins)
15
-
1  return max_score(coins, 0, n - 1)
12
+
1  prefix_sum = [0 for i in range(n + 1)] # precompute prefix sum
13
+
1  for i in range(1, n + 1):
14
+
1    prefix_sum[i] = prefix_sum[i - 1] + coins[i - 1]
15
+
1  return max_score(prefix_sum, 1, n)

Top-down DP

In essence, the DP top-down solution is the brute force solution with memoization. You may have noticed maxScore(l, r) can be be memoized.

Let’s formalize our idea above by specifying our DP state (i.e. what’s important that can lead us to our answer). In this case, let dp(l, r) be the maximum score we can achieve if coins in the range [l, r] are the only ones present and we go first.

Furthermore, our base case is: dp(l, r) = v_r if l = r. That is, since the range contains only one coin, we simply take that coin.

Now we deal with the transition. Exactly like our brute force solution, we consider two cases of picking either the left or right coin.

Next, we choose the case that gives us the greatest score or minimizes the opponent's score. Therefore, the solution is either:

  • dp(l, r) = max(sum(l, r) - dp(l + 1, r), sum(l, r) - dp(l, r - 1)) or
  • dp(l, r) = sum(l, r) - min(dp(l + 1, r), dp(l, r - 1))

The following is a top-down solution to the problem:

1int max_score(vector<vector<int>> &dp, vector<int> &prefix_sum, int l, int r) {
2    // Return memoized result
3    if (dp[l][r] != 0) {
4        return dp[l][r];
5    }
6
7    // Calculate total coins value from l to r
8    int sum = prefix_sum[r] - prefix_sum[l - 1];
9
10    // If only one coin is present, return its value
11    if (l == r) {
12        dp[l][r] = sum;
13    } else {
14        // Consider left and right coin and choose the better option
15        dp[l][r] = sum - min(max_score(dp, prefix_sum, l + 1, r), max_score(dp, prefix_sum, l, r - 1));
16    }
17
18    return dp[l][r];
19}
20
21int coin_game(vector<int> coins) {
22    int n = coins.size();
23
24    // Initialize the DP table with zeroes
25    vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
26
27    // Compute prefix sum of coins
28    vector<int> prefix_sum(n + 1, 0);
29    for (int i = 1; i <= n; i++) {
30        prefix_sum[i] = prefix_sum[i - 1] + coins[i - 1];
31    }
32
33    return max_score(dp, prefix_sum, 1, n);
34}
35
1public static int maxScore(int[][] dp, int[] prefixSum, int l, int r) {
2    // Return memoized result
3    if (dp[l][r] != 0) {
4        return dp[l][r];
5    }
6
7    // Calculate total coins value from l to r
8    int sum = prefixSum[r] - prefixSum[l - 1];
9
10    // If only one coin is present, return its value
11    if (l == r) {
12        dp[l][r] = sum;
13    } else {
14        // Consider left and right coin and choose the better option
15        dp[l][r] = sum - Math.min(maxScore(dp, prefixSum, l + 1, r), maxScore(dp, prefixSum, l, r - 1));
16    }
17
18    return dp[l][r];
19}
20
21public static int coinGame(List<Integer> coins) {
22    int n = coins.size();
23    int[][] dp = new int[n + 1][n + 1];
24    int[] prefixSum = new int[n + 1];
25    prefixSum[0] = 0;
26    // Compute prefix sum of coins
27    for (int i = 1; i <= n; i++) {
28        prefixSum[i] = prefixSum[i - 1] + coins.get(i - 1);
29    }
30    
31    return maxScore(dp, prefixSum, 1, n);
32}
33
1function maxScore(dp, prefixSum, l, r) {
2    // Return memoized result
3    if (dp[l][r] != 0) {
4        return dp[l][r];
5    }
6
7    // Calculate total coins value from l to r
8    var sum = prefixSum[r] - prefixSum[l - 1];
9
10    // If only one coin is present, return its value
11    if (l == r) {
12        dp[l][r] = sum;
13    } else {
14        // Consider left and right coin and choose the better option
15        dp[l][r] = sum - Math.min(maxScore(dp, prefixSum, l + 1, r), maxScore(dp, prefixSum, l, r - 1));
16    }
17
18    return dp[l][r];
19}
20
21function coinGame(coins) {
22    let n = coins.length;
23    var dp = new Array(n + 1);
24
25    // Initialize everything to be zero
26    for (let i = 0; i <= n; i++) {
27        dp[i] = new Array(n + 1);
28        for (let j = 0; j <= n; j++) {
29            dp[i][j] = 0;   
30        }
31    }
32
33    // Compute prefix sum of coins
34    var prefixSum = new Array(n + 1);
35    prefixSum[0] = 0;
36    for (let i = 1; i <= n; i++) {
37        prefixSum[i] = prefixSum[i - 1] + coins[i - 1];
38    }
39    
40    return maxScore(dp, prefixSum, 1, n);
41}
42
1import sys
2sys.setrecursionlimit(1500)
3
4def max_score(dp, prefix_sum, l, r):
5    # Return memoized result
6    if dp[l][r] != 0:
7        return dp[l][r]
8
9    # Calculate total coins value from l to r
10    sum = prefix_sum[r] - prefix_sum[l - 1]
11
12    # If only one coin is present, return its value
13    if l == r:
14        dp[l][r] = sum
15