Hard Problemsoptimizationinterval

Optimal Strategy for a Game

Coins of varying values sit in a row. Two players alternate turns, each turn taking either end coin of the remaining row, and both play to maximize their own total. Find the maximum total the first player can guarantee regardless of how the second player plays.

Do this lesson first: house robber

Example input

coins = [8, 15, 3, 7]

Expected output

22

Break it down

Answer each question out loud before you open it. Getting it wrong here is the useful part. A revealed answer you never guessed at teaches you nothing.

Fill the table

The table pauses before each cell you have to supply. Type the value the recurrence gives, and the write animation confirms it.

Step not started

Press start. The animation stops at every cell YOUR recurrence must fill.

dp[i][i] = coins[i] on the diagonal: with one coin left, the player to move takes it and the game ends. Every other cell reads exactly two cells, each one coin shorter: dp[i + 1][j], the stretch without its left coin, directly below, and dp[i][j - 1], the stretch without its right coin, directly to the left. S is added up from the coins themselves, not read from the table. Filling by length, every pair of coins, then every run of three, and so on, has both reads final before the cell needs them, which on the grid is a sweep along the diagonals out to the top-right corner.

Rows ↓
Cols →
0: 8
1: 15
2: 3
3: 7
0: 8
8
15
11
22
1: 15
·
15
15
18
2: 3
·
·
3
7
3: 7
·
·
·
7

Rows ↓row i: the first coin still on the table

Cols →column j: the last coin still on the table, so the cell is the most the player to move collects from coins i..j

optimal-strategy-for-a-game.ts
  1. 1function firstPlayerTotal(coins) {
  2. 2 const n = coins.length;
  3. 3 const pre = [0];
  4. 4 for (const c of coins) pre.push(pre[pre.length - 1] + c);
  5. 5 const dp = Array.from({ length: n }, () => new Array(n).fill(0));
  6. 6 for (let i = 0; i < n; i++) dp[i][i] = coins[i];
  7. 7 for (let len = 2; len <= n; len++) {
  8. 8 for (let i = 0; i + len - 1 < n; i++) {
  9. 9 const j = i + len - 1;
  10. 10 const sum = pre[j + 1] - pre[i];
  11. 11 dp[i][j] = Math.max(sum - dp[i + 1][j], sum - dp[i][j - 1]);
  12. 12 }
  13. 13 }
  14. 14 return dp[0][n - 1];
  15. 15}
Base caseComputedBeing readAnswer

The code, the trap, the variations

optimal-strategy-for-a-game.ts
  1. 1function firstPlayerTotal(coins) {
  2. 2 const n = coins.length;
  3. 3 const pre = [0];
  4. 4 for (const c of coins) pre.push(pre[pre.length - 1] + c);
  5. 5 const dp = Array.from({ length: n }, () => new Array(n).fill(0));
  6. 6 for (let i = 0; i < n; i++) dp[i][i] = coins[i];
  7. 7 for (let len = 2; len <= n; len++) {
  8. 8 for (let i = 0; i + len - 1 < n; i++) {
  9. 9 const j = i + len - 1;
  10. 10 const sum = pre[j + 1] - pre[i];
  11. 11 dp[i][j] = Math.max(sum - dp[i + 1][j], sum - dp[i][j - 1]);
  12. 12 }
  13. 13 }
  14. 14 return dp[0][n - 1];
  15. 15}

Where people go wrong

Taking the larger end coin every turn. It feels safe, a bigger coin now is a bigger coin, and on this page's coins it is right at 2, 3, 5 and 7 coins. At 4 it fails at once: on 8, 15, 3, 7 it takes the 8, because 8 beats 7, and that hands the 15 to your opponent; you finish with 8 + 7 = 15, where taking the 7 first guarantees 22. Played against an opponent who answers well, greedy also falls short at 6, 8 and 9 coins: 21 against 24, 25 against 30, 27 against 34. The coin you take matters less than the coin you expose. The other common slip forgets the opponent altogether: dp[i][j] = max(coins[i] + dp[i + 1][j], coins[j] + dp[i][j - 1]) treats the rest of the row as yours too, so every cell comes out as the sum of its coins, 33 at 4 coins, as if you were playing alone.

  • Store the margin instead of the total: how far ahead the player to move finishes.

    diff[i][j] = max(coins[i] - diff[i + 1][j], coins[j] - diff[i][j - 1]), with diff[i][i] = coins[i]. The coin you take counts for you and the opponent's best margin on the rest counts against you, so no sum is needed. The triangle, the two reads and the fill by length do not change. The first player's total comes back as (S + diff) / 2: at 4 coins the margin is 11 and (33 + 11) / 2 = 22. When the question is only whether the first player can at least tie, that is the margin being 0 or more.

  • Look two moves ahead instead of subtracting from S.

    The textbook form: dp[i][j] = max(coins[i] + min(dp[i + 2][j], dp[i + 1][j - 1]), coins[j] + min(dp[i + 1][j - 1], dp[i][j - 2])). You take an end, the opponent takes whichever end leaves you less, and you move again on what is left. It reads three cells two coins shorter, and an empty stretch has to count as 0. The state and the answers do not change: it gives the same total at every slider size. It just spells out the opponent's reply that S - dp[...] keeps folded away.

  • With an even number of coins, only guarantee at least half.

    No table is needed. Number the coins from 0: the first player can take every even-numbered coin, or every odd-numbered one, because after each of your moves both ends left to the opponent have the other parity. So you can always collect the larger of the two parity sums, which is at least half. That is a guarantee, not the optimum: on 5, 3, 7, 10 the parity sums are 12 and 13, while the table finds 15, starting by taking the 10.

  • Return the moves, not just the total.

    The table is unchanged; replay the game against it. On a stretch i..j, the mover takes the left coin if S - dp[i + 1][j] is at least S - dp[i][j - 1], otherwise the right one, and the stretch shrinks. At 4 coins the line is: you take the 7, the opponent takes the 8, you take the 15, the opponent takes the 3. The opponent's move is a tie on that table, dp[0][2]'s two ends are worth 11 each, so they could take the 3 instead and you still collect the 15.