Skip to content
dsa450for five year olds

Dynamic Programming · 49 of 60

Optimal Strategy for a Game

Two smart players take turns grabbing a coin from either end of a row, and you work out the most money the first player can be sure to get.

Simple way
time O(2^n)
room O(n)
Clever way
time O(n^2)
room O(n^2)

The problem, simply

Coins lie in a row, usually an even number of them. You and a friend take turns. On your turn you take one coin from the left end or from the right end. You go first, and your friend is just as smart as you: they always play their best.

Give back the largest total you can be sure to collect.

A story to remember it

Picture it

arraycoins
012381537rightleft

Four coins. You may take only from the ends, 8 or 7. Taking 8 looks greedy and good, but it uncovers 15 for your friend.

  • checking

Examples

A normal row

In
[5, 3, 7, 10]
Out
15

Take 10. Your friend takes 7. You take 5. Your friend takes 3. You have 15.

Greedy loses

In
[8, 15, 3, 7]
Out
22

If you grab 8 (the bigger end), your friend grabs 15 and you end with 15. Take 7 first instead. Whatever your friend takes, you get 15 next, for 22.

All the same

In
[2, 2, 2, 2]
Out
4

Every choice is equal. You always get half.

Six coins

In
[20, 30, 2, 2, 2, 10]
Out
42

You end up with 30, 10 and 2. Planning several turns ahead matters.

Two coins

In
[3, 9]
Out
9

Take the bigger one and the game is over.

Way 1: the simple way

Write a helper f(i, j): the most the player to move can get from coins i to j.

  1. One coin left: take it. No coins left: 0.
  2. If you take the left coin a[i], your friend then moves on i+1..j. They will leave you either f(i+2, j) (they took the left) or f(i+1, j-1) (they took the right). They play well, so you get the smaller one.
  3. Same for taking the right coin a[j]: you get a[j] + min(f(i+1, j-1), f(i, j-2)).
  4. Answer the bigger of the two.
treef(i, j) for 6 coins
LLLLLRRLRRLRLLLRRLRRRLLLLRRLRRRRLLLRRLRR0,52,54,53,43,42,31,43,42,32,31,21,43,42,32,31,20,32,31,21,20,1againagainagainagainagainagainagainagainagainagainagainagain

The helper calls for 6 coins, two levels deep. LL means you took left, then your friend took left. The highlighted calls, like (1,4) and (2,3), are solved again and again.

  • checking
java
class Solution {
    public int optimalStrategy(int[] arr) {
        return f(arr, 0, arr.length - 1);
    }

    private int f(int[] a, int i, int j) {
        if (i > j) return 0;          // no coins left
        if (i == j) return a[i];      // one coin: take it
        // take the left coin, then the friend leaves us the worse option
        int takeLeft = a[i] + Math.min(f(a, i + 2, j), f(a, i + 1, j - 1));
        // take the right coin
        int takeRight = a[j] + Math.min(f(a, i + 1, j - 1), f(a, i, j - 2));
        return Math.max(takeLeft, takeRight);
    }
}

How long it takes: each call makes up to four calls on a row that is two coins shorter, so about O(2^n). Extra room: the call chain, O(n).

Way 2: the clever way

The helper only ever looks at a stretch i..j of the row. There are about n * n / 2 stretches. Fill a table best[i][j] for every stretch, short stretches first, so every answer you need is already there.

  1. One coin: best[i][i] = a[i].
  2. Two coins: the bigger one.
  3. Longer stretches: best[i][j] = max(a[i] + min(best[i+2][j], best[i+1][j-1]), a[j] + min(best[i+1][j-1], best[i][j-2])).
  4. The answer is best[0][n-1], the top right corner.

Fill the table for the coins 8, 15, 3, 7

01 / 08

gridbest[i][j] for the player to move
j=0j=1j=2j=3i=0i=1i=2i=381537
arraycoins
012381537

One coin left: the player to move takes it. So the diagonal holds the coins themselves.

  • looking now
  • checking
  • answer
  • just added
java
class Solution {
    public int optimalStrategy(int[] arr) {
        int n = arr.length;
        if (n == 0) return 0;
        int[][] best = new int[n][n];
        for (int gap = 0; gap < n; gap++) {            // stretch length minus 1
            for (int i = 0; i + gap < n; i++) {
                int j = i + gap;
                if (gap == 0) { best[i][j] = arr[i]; continue; }
                int a = (i + 2 <= j) ? best[i + 2][j] : 0;      // friend took left after us
                int b = (i + 1 <= j - 1) ? best[i + 1][j - 1] : 0; // the middle stretch
                int c = (i <= j - 2) ? best[i][j - 2] : 0;      // friend took right after us
                int takeLeft = arr[i] + Math.min(a, b);
                int takeRight = arr[j] + Math.min(b, c);
                best[i][j] = Math.max(takeLeft, takeRight);
            }
        }
        return best[0][n - 1];
    }
}

How long it takes: about n * n / 2 cells, each in a few steps, O(n^2). Extra room: the table, O(n^2).

Another way: count the lead

A neat version that interviewers like: let lead[i][j] be how far ahead the player to move can finish on coins i..j. If you take a[i], your friend then leads by lead[i+1][j], so your lead is a[i] - lead[i+1][j]. Take the better end. At the end your total is (sum + lead) / 2.

java
class Solution {
    public int optimalStrategy(int[] arr) {
        int n = arr.length;
        if (n == 0) return 0;
        int[] lead = new int[n];      // lead[j] holds lead for stretch i..j
        int sum = 0;
        for (int i = n - 1; i >= 0; i--) {
            sum += arr[i];
            lead[i] = arr[i];                        // one coin
            for (int j = i + 1; j < n; j++) {
                // lead[j] still holds i+1..j, lead[j-1] already holds i..j-1
                lead[j] = Math.max(arr[i] - lead[j], arr[j] - lead[j - 1]);
            }
        }
        return (sum + lead[n - 1]) / 2;
    }
}

This uses one row of n numbers, so O(n) extra room and still O(n^2) time.

Watch out for

  • Greedy (always take the bigger end) is wrong. [8, 15, 3, 7] shows it.
  • Index checks: best[i+2][j] and best[i][j-2] do not exist when the stretch has two coins. Treat them as 0.
  • Fill by stretch length. A plain row by row loop from the top reads cells that are not ready yet.

What interviewers ask next

  • Can the first player always win with an even number of coins? Yes, never lose. Color the coins in turns, odd and even positions. The first player can always take all odd ones or all even ones, whichever sum is bigger. That only proves you never lose, and the table gives the true best.
  • Print the moves, not only the total. Keep which end gave the max in each cell, then follow it from best[0][n-1].
  • What if a player may take one, two or three coins from the front only? A similar table over how many coins are left.
  • Can you do it in O(n) space? Yes, with the lead version above.

Remember this

Grown-up words: planning for the friend's best move is minimax, a stretch of coins is an interval, the table is interval dynamic programming, and the lead version is a zero sum game score difference.