Given an array arr[] containing an even number of elements, where each element represents the value of a coin placed in a row. Two players(We and our friend) are playing the game, and in every turn a player can pick either the first coin or the last coin from the row. Once a coin is taken, it cannot be chosen again.
Determine the maximum amount of money we can surely secure if we move first, assuming the opponent is also playing greedily.
Examples:
Input: arr[] = [5, 3, 7, 10]
Output: 15
Explanation: We collects the maximum value as 15 (10 + 5). It is guaranteed that we cannot get more than 15 by any possible moves.Input: arr[] = [8, 15, 3, 7]
Output: 22
Explanation: We collects the maximum value as 22 (7 + 15).
Table of Content
- Why greedy algorithm fail here?
- [Naive Approach] Using Recursion – O(2^n) Time and O(n) Space
- [Expected Approach 1] Using Top-Down DP (Memoization) – O(n*n) Time and O(n*n) Space
- [Expected Approach 2] Using Bottom-Up DP (Tabulation) – O(n*n) Time and O(n*n) Space
- [Space Optimised] O(n^2) Time and O(n) Space
Why greedy algorithm fail here?
Does choosing the best at each move give an optimal solution?
No. In the second example, this is how the game can be finished in two ways:
- we chooses 8.
The opponent chooses 15.
we chooses 7.
The opponent chooses 3.
The total value collected by us : 15(8 + 7)
- we chooses 7.
The opponent chooses 8.
we chooses 15.
The opponent chooses 3.
The total value collected by us : 22(7 + 15)
Note: If the user follows the second game state, the maximum value can be collected although the first move is not the best.
[Naive Approach] Using Recursion – O(2^n) Time and O(n) Space
Suppose if we have values [vi, v(i+1), . . . ., vj], and we have choices to pick from either i or j index so at any turn, we have two choices:
Pick the ith coin (Vi):
- Opponent chooses (i+1)th or jth coin to minimize our gain.
- Maximum we can collect: Vi+ min(maxAmount(i+2,j), maxAmount(i+1,j−1))
[i+2, j] if opponent picks (i+1)th, [i+1, j-1] if opponent picks jth.Pick the jth coin (Vj):
- Opponent chooses ith or (j-1)th coin optimally.
- Maximum we can collect: Vj+min(maxAmount(i+1,j−1), maxAmount(i,j−2))
[i+1, j-1] if opponent picks ith, [i, j-2] if opponent picks (j-1)th.Finally, we pick the choice that gives the maximum total.
//Driver Code Starts
#include <iostream>
#include<vector>
using namespace std;
//Driver Code Ends
int maxAmount(int i, int j, vector<int> &arr) {
// Base case: If i > j, no more elements are left to pick
if (i > j)
return 0;
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + min(maxAmount(i + 2, j, arr),
maxAmount(i + 1, j - 1, arr));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + min(maxAmount(i + 1, j - 1, arr),
maxAmount(i, j - 2, arr));
return max(takeFirst, takeLast);
}
int maximumAmount(vector<int> &arr) {
int n = arr.size();
int res = maxAmount(0, n - 1, arr);
return res;
}
//Driver Code Starts
int main() {
vector<int> arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
cout << res << endl;
return 0;
}
//Driver Code Ends
//Driver Code Starts
import java.util.Arrays;
public class GFG {
//Driver Code Ends
static int maxAmount(int i, int j, int[] arr) {
// Base case: If i > j, no more elements are left to pick
if (i > j)
return 0;
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + Math.min(maxAmount(i + 2, j, arr),
maxAmount(i + 1, j - 1, arr));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + Math.min(maxAmount(i + 1, j - 1, arr),
maxAmount(i, j - 2, arr));
return Math.max(takeFirst, takeLast);
}
static int maximumAmount(int[] arr) {
int n = arr.length;
int res = maxAmount(0, n - 1, arr);
return res;
}
//Driver Code Starts
public static void main(String[] args) {
int[] arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
System.out.println(res);
}
}
//Driver Code Ends
def maxAmount(i, j, arr):
# Base case: If i > j, no more elements are left to pick
if i > j:
return 0
# Option 1: Take the first element arr[i]
takeFirst = arr[i] + min(maxAmount(i + 2, j, arr),
maxAmount(i + 1, j - 1, arr))
# Option 2: Take the last element arr[j]
takeLast = arr[j] + min(maxAmount(i + 1, j - 1, arr),
maxAmount(i, j - 2, arr))
return max(takeFirst, takeLast)
def maximumAmount(arr):
n = len(arr)
res = maxAmount(0, n - 1, arr)
return res
#Driver Code Starts
if __name__ == "__main__":
arr = [5, 3, 7, 10]
res = maximumAmount(arr)
print(res)
#Driver Code Ends
//Driver Code Starts
using System;
public class GFG{
//Driver Code Ends
static int maxAmount(int i, int j, int[] arr) {
// Base case: If i > j, no more elements are left to pick
if (i > j)
return 0;
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + Math.Min(maxAmount(i + 2, j, arr),
maxAmount(i + 1, j - 1, arr));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + Math.Min(maxAmount(i + 1, j - 1, arr),
maxAmount(i, j - 2, arr));
return Math.Max(takeFirst, takeLast);
}
static int maximumAmount(int[] arr) {
int n = arr.Length;
int res = maxAmount(0, n - 1, arr);
return res;
}
//Driver Code Starts
public static void Main() {
int[] arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
Console.WriteLine(res);
}
}
//Driver Code Ends
function maxAmount(i, j, arr) {
// Base case: If i > j, no more elements are left to pick
if (i > j)
return 0;
// Option 1: Take the first element arr[i]
let takeFirst = arr[i] + Math.min(maxAmount(i + 2, j, arr),
maxAmount(i + 1, j - 1, arr));
// Option 2: Take the last element arr[j]
let takeLast = arr[j] + Math.min(maxAmount(i + 1, j - 1, arr),
maxAmount(i, j - 2, arr));
return Math.max(takeFirst, takeLast);
}
function maximumAmount(arr) {
let n = arr.length;
let res = maxAmount(0, n - 1, arr);
return res;
}
//Driver Code Starts
//Driver Code
let arr = [5, 3, 7, 10];
let res = maximumAmount(arr);
console.log(res);
//Driver Code Ends
Output
15
[Expected Approach 1] Using Top-Down DP (Memoization) – O(n*n) Time and O(n*n) Space
If we look carefully at the naive recursive approach, many subproblems are solved repeatedly, leading to redundant calculations and exponential time complexity. To overcome this, we use the memoization approach.
We create a 2D array dp[n][n], where n is the number of coins, and store the maximum amount that can be collected from the subarray arr[i…j]. Before computing, we check if dp[i][j] already contains a value. If it does, we simply return the stored result, avoiding unnecessary recursion.
//Driver Code Starts
#include <iostream>
#include<vector>
using namespace std;
//Driver Code Ends
int maxAmount(int i, int j, vector<int> &arr,
vector<vector<int>> &dp) {
// Base case: If i > j, no more elements
// are left to pick
if (i > j)
return 0;
// If the result is already computed, return
// from the dp table
if (dp[i][j] != -1)
return dp[i][j];
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + min(maxAmount(i + 2, j, arr, dp),
maxAmount(i + 1, j - 1, arr, dp));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + min(maxAmount(i + 1, j - 1, arr, dp),
maxAmount(i, j - 2, arr, dp));
// Store the maximum of the two choices
return dp[i][j] = max(takeFirst, takeLast);
}
int maximumAmount(vector<int> &arr) {
int n = arr.size();
vector<vector<int>> dp(n, vector<int>(n, -1));
int res = maxAmount(0, n - 1, arr, dp);
return res;
}
//Driver Code Starts
int main() {
vector<int> arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
cout << res << endl;
return 0;
}
//Driver Code Ends
//Driver Code Starts
import java.util.Arrays;
public class GFG {
//Driver Code Ends
static int maxAmount(int i, int j, int[] arr, int[][] dp) {
// Base case: If i > j, no more elements
// are left to pick
if (i > j)
return 0;
// If the result is already computed, return
// from the dp table
if (dp[i][j] != -1)
return dp[i][j];
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + Math.min(maxAmount(i + 2, j, arr, dp),
maxAmount(i + 1, j - 1, arr, dp));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + Math.min(maxAmount(i + 1, j - 1, arr, dp),
maxAmount(i, j - 2, arr, dp));
// Store the maximum of the two choices
return dp[i][j] = Math.max(takeFirst, takeLast);
}
static int maximumAmount(int[] arr) {
int n = arr.length;
int[][] dp = new int[n][n];
for (int[] row : dp) Arrays.fill(row, -1);
int res = maxAmount(0, n - 1, arr, dp);
return res;
}
//Driver Code Starts
public static void main(String[] args) {
int[] arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
System.out.println(res);
}
}
//Driver Code Ends
def maxAmount(i, j, arr, dp):
# Base case: If i > j, no more elements
# are left to pick
if i > j:
return 0
# If the result is already computed, return
# from the dp table
if dp[i][j] != -1:
return dp[i][j]
# Option 1: Take the first element arr[i]
takeFirst = arr[i] + min(maxAmount(i + 2, j, arr, dp),
maxAmount(i + 1, j - 1, arr, dp))
# Option 2: Take the last element arr[j]
takeLast = arr[j] + min(maxAmount(i + 1, j - 1, arr, dp),
maxAmount(i, j - 2, arr, dp))
# Store the maximum of the two choices
dp[i][j] = max(takeFirst, takeLast)
return dp[i][j]
def maximumAmount(arr):
n = len(arr)
dp = [[-1 for _ in range(n)] for _ in range(n)]
res = maxAmount(0, n - 1, arr, dp)
return res
#Driver Code Starts
if __name__ == "__main__":
arr = [5, 3, 7, 10]
res = maximumAmount(arr)
print(res)
#Driver Code Ends
//Driver Code Starts
using System;
public class GFG{
//Driver Code Ends
static int maxAmount(int i, int j, int[] arr, int[,] dp) {
// Base case: If i > j, no more elements
// are left to pick
if (i > j)
return 0;
// If the result is already computed, return
// from the dp table
if (dp[i, j] != -1)
return dp[i, j];
// Option 1: Take the first element arr[i]
int takeFirst = arr[i] + Math.Min(maxAmount(i + 2, j, arr, dp),
maxAmount(i + 1, j - 1, arr, dp));
// Option 2: Take the last element arr[j]
int takeLast = arr[j] + Math.Min(maxAmount(i + 1, j - 1, arr, dp),
maxAmount(i, j - 2, arr, dp));
// Store the maximum of the two choices
return dp[i, j] = Math.Max(takeFirst, takeLast);
}
static int maximumAmount(int[] arr) {
int n = arr.Length;
int[,] dp = new int[n, n];
for (int x = 0; x < n; x++)
for (int y = 0; y < n; y++)
dp[x, y] = -1;
int res = maxAmount(0, n - 1, arr, dp);
return res;
}
//Driver Code Starts
public static void Main() {
int[] arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
Console.WriteLine(res);
}
}
//Driver Code Ends
function maxAmount(i, j, arr, dp) {
// Base case: If i > j, no more elements
// are left to pick
if (i > j)
return 0;
// If the result is already computed, return
// from the dp table
if (dp[i][j] !== -1)
return dp[i][j];
// Option 1: Take the first element arr[i]
let takeFirst = arr[i] + Math.min(maxAmount(i + 2, j, arr, dp),
maxAmount(i + 1, j - 1, arr, dp));
// Option 2: Take the last element arr[j]
let takeLast = arr[j] + Math.min(maxAmount(i + 1, j - 1, arr, dp),
maxAmount(i, j - 2, arr, dp));
// Store the maximum of the two choices
dp[i][j] = Math.max(takeFirst, takeLast);
return dp[i][j];
}
function maximumAmount(arr) {
let n = arr.length;
let dp = Array.from({length: n}, () => Array(n).fill(-1));
let res = maxAmount(0, n - 1, arr, dp);
return res;
}
//Driver Code Starts
//Driver code
let arr = [5, 3, 7, 10];
let res = maximumAmount(arr);
console.log(res);
//Driver Code Ends
Output
15
[Expected Approach 2] Using Bottom-Up DP (Tabulation) – O(n*n) Time and O(n*n) Space
In this solution, we fill the DP table iteratively. First, we handle the base case:
If i == j, only one coin exists, so the player has no choice but to take it -> dp[i][j] = arr[i].
After filling these base values, we gradually solve larger subproblems. For every subarray arr[i…j], we have two choices:
- Pick the ith coin: After that, the opponent plays optimally and leaves us the minimum possible value-> arr[i] + min(dp[i+2][j], dp[i+1][j-1])
- Pick the jth coin: Again the opponent will make the move that minimizes our gain -> arr[j] + min(dp[i+1][j-1], dp[i][j-2])
//Driver Code Starts
#include <iostream>
#include<vector>
using namespace std;
//Driver Code Ends
int maximumAmount(vector<int> &arr) {
int n = arr.size();
vector<vector<int>>dp(n, vector<int>(n,0));
// Fill table using above
// recursive formula.
for (int gap = 0; gap < n; ++gap) {
for (int i = 0, j = gap; j < n; ++i, ++j) {
int x = ((i + 2) <= j) ? dp[i + 2][j] : 0;
int y = ((i + 1) <= (j - 1)) ? dp[i + 1][j - 1] : 0;
int z = (i <= (j - 2)) ? dp[i][j - 2] : 0;
dp[i][j] = max(arr[i] + min(x, y), arr[j] + min(y, z));
}
}
return dp[0][n - 1];
}
//Driver Code Starts
int main() {
vector<int> arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
cout << res;
return 0;
}
//Driver Code Ends
import java.util.Arrays;
public class GFG {
public static int maximumAmount(int[] arr) {
int n = arr.length;
int[][] dp = new int[n][n];
// Fill table using above
// recursive formula.
for (int gap = 0; gap < n; ++gap) {
for (int i = 0, j = gap; j < n; ++i, ++j) {
int x = ((i + 2) <= j) ? dp[i + 2][j] : 0;
int y = ((i + 1) <= (j - 1)) ? dp[i + 1][j - 1] : 0;
int z = (i <= (j - 2)) ? dp[i][j - 2] : 0;
dp[i][j] = Math.max(arr[i] + Math.min(x, y), arr[j] + Math.min(y, z));
}
}
return dp[0][n - 1];
}
public static void main(String[] args) {
int[] arr = {5, 3, 7, 10};
int res = maximumAmount(arr);
System.out.println(res);
}
}
def maximumAmount(arr):
n = len(arr)
dp = [[0] * n for _ in range(n)]
# Fill table using above
# recursive formula.
for gap in range(n):
for i in range(n - gap):
j = i + gap
x = dp[i + 2][j] if (i + 2) <= j else 0
y = dp[i + 1][j - 1] if (i + 1) <= (j - 1) else 0
z = dp[i][j - 2] if i <= (j - 2) else 0
dp[i][j] = max(arr[i] + min(x, y), arr[j] + min(y, z))
return dp[0][n - 1]
#Driver Code Starts
if __name__ == "__main__":
arr = [5, 3, 7, 10]
res = maximumAmount(arr)
print(res)
#Driver Code Ends
//Driver Code Starts
using System;
class GFG {
//Driver Code Ends
public static int maximumAmount(int[] arr) {
int n = arr.Length;
int[,] dp = new int[n, n];
// Fill table using above
// recursive formula.
for (int gap = 0; gap < n; ++gap) {
for (int i = 0, j = gap; j < n; ++i, ++j) {
int x = ((i + 2) <= j) ? dp[i + 2, j] : 0;
int y = ((i + 1) <= (j - 1)) ? dp[i + 1, j - 1] : 0;
int z = (i <= (j - 2)) ? dp[i, j - 2] : 0;
dp[i, j] = Math.Max(arr[i] + Math.Min(x, y), arr[j] + Math.Min(y, z));
}
}
return dp[0, n - 1];
}
//Driver Code Starts
static void Main(string[] args) {
int[] arr = { 5, 3, 7, 10 };
int res = maximumAmount(arr);
Console.WriteLine(res);
}
}
//Driver Code Ends
function maximumAmount(arr) {
let n = arr.length;
// Create a 2D dp array initialized with 0
let dp = Array.from({ length: n }, () => Array(n).fill(0));
// Fill table using above
// recursive formula.
for (let gap = 0; gap < n; ++gap) {
for (let i = 0, j = gap; j < n; ++i, ++j) {
let x = ((i + 2) <= j) ? dp[i + 2][j] : 0;
let y = ((i + 1) <= (j - 1)) ? dp[i + 1][j - 1] : 0;
let z = (i <= (j - 2)) ? dp[i][j - 2] : 0;
dp[i][j] = Math.max(arr[i] + Math.min(x, y), arr[j] + Math.min(y, z));
}
}
return dp[0][n - 1];
}
//Driver Code Starts
// Driver code
let arr = [5, 3, 7, 10];
let res = maximumAmount(arr);
console.log(res);
//Driver Code Ends
Output
15
[Space Optimised] O(n^2) Time and O(n) Space
In the previous approach, we used a 2D DP table. However, we can optimize this to a 1D DP array by observing that at every step we only need results from the current and previous ranges.
When the current player picks arr[i], their advantage becomes arr[i] – dp[j] because after this move the opponent plays optimally on the subarray (i+1…j) and dp[j] represents the lead the opponent can gain there. Similarly, if the player picks arr[j], their advantage becomes arr[j] – dp[j−1] since the opponent will now play on (i…j−1). Since we want to maximize our advantage we take the maximum of both.
At the end, dp[n-1] represents the maximum score difference the first player can guarantee over the opponent.
To convert this difference to the actual maximum coins collected,
we use: firstPlayerCoins= (totalSum + dp[n−1])/2
because totalSum = (first + second) and dp[n-1] = (first – second).
//Driver Code Starts
#include <iostream>
#include<vector>
using namespace std;
//Driver Code Ends
int maximumAmount(vector<int> arr) {
int n = arr.size();
int sum = 0;
vector<int> dp(n, 0);
for (int i = (n - 1); i >= 0; i--) {
// Calculating the sum of all the elements
sum += arr[i];
for (int j = i; j < n; j++) {
if (i == j) {
// If there is only one element
dp[j] = arr[j];
} else {
// Calculating the dp states using the relation
dp[j] = max(arr[i] - dp[j], arr[j] - dp[j - 1]);
}
}
}
// Return the final result
return (sum + dp[n - 1]) / 2;
}
//Driver Code Starts
int main() {
vector<int> arr = {5, 3, 7, 10};
cout<<maximumAmount(arr);
return 0;
}
//Driver Code Ends
//Driver Code Starts
import java.util.Arrays;
public class GFG {
//Driver Code Ends
public static int maximumAmount(int[] arr) {
int n = arr.length;
int sum = 0;
int[] dp = new int[n];
for (int i = (n - 1); i >= 0; i--) {
// Calculating the sum of all the elements
sum += arr[i];
for (int j = i; j < n; j++) {
if (i == j) {
// If there is only one element
dp[j] = arr[j];
} else {
// Calculating the dp states using the relation
dp[j] = Math.max(arr[i] - dp[j], arr[j] - dp[j - 1]);
}
}
}
// Return the final result
return (sum + dp[n - 1]) / 2;
}
//Driver Code Starts
public static void main(String[] args) {
int[] arr = {5, 3, 7, 10};
System.out.println(maximumAmount(arr));
}
}
//Driver Code Ends
def maximumAmount(arr):
n = len(arr)
sum_val = 0
dp = [0] * n
for i in range(n - 1, -1, -1):
# Calculating the sum of all the elements
sum_val += arr[i]
for j in range(i, n):
if i == j:
# If there is only one element
dp[j] = arr[j]
else:
# Calculating the dp states using the relation
dp[j] = max(arr[i] - dp[j], arr[j] - dp[j - 1])
# Return the final result
return (sum_val + dp[n - 1]) // 2
#Driver Code Starts
if __name__ == "__main__":
arr = [5, 3, 7, 10]
print(maximumAmount(arr))
#Driver Code Ends
//Driver Code Starts
using System;
using System.Collections.Generic;
class GFG {
//Driver Code Ends
static int maximumAmount(int[] arr) {
int n = arr.Length;
int sum = 0;
int[] dp = new int[n];
for (int i = (n - 1); i >= 0; i--) {
// Calculating the sum of all the elements
sum += arr[i];
for (int j = i; j < n; j++) {
if (i == j) {
// If there is only one element
dp[j] = arr[j];
} else {
// Calculating the dp states using the relation
dp[j] = Math.Max(arr[i] - dp[j], arr[j] - dp[j - 1]);
}
}
}
// Return the final result
return (sum + dp[n - 1]) / 2;
}
//Driver Code Starts
static void Main() {
int[] arr = { 5, 3, 7, 10 };
Console.WriteLine(maximumAmount(arr));
}
}
//Driver Code Ends
function maximumAmount(arr) {
let n = arr.length;
let sum = 0;
let dp = new Array(n).fill(0);
for (let i = n - 1; i >= 0; i--) {
// Calculating the sum of all the elements
sum += arr[i];
for (let j = i; j < n; j++) {
if (i === j) {
// If there is only one element
dp[j] = arr[j];
} else {
// Calculating the dp states using the relation
dp[j] = Math.max(arr[i] - dp[j], arr[j] - dp[j - 1]);
}
}
}
// Return the final result
return (sum + dp[n - 1]) / 2;
}
//Driver Code Starts
//Driver Code
let arr = [5, 3, 7, 10];
console.log(maximumAmount(arr));
//Driver Code Ends
Output
15