New to Rust? Grab our free Rust for Beginners eBook Get it free →
Optimal Game Strategy in C++

For a row of coins where each turn removes one end, the first player needs a way to guarantee a score against an opponent who also chooses well. The optimal game strategy problem uses interval dynamic programming to find that score.
I’ll show how to build the interval table and use the C++ solution to calculate the guaranteed score.
TL;DR
For a row of coins where each turn removes one end, interval dynamic programming returns the maximum score the first player can guarantee against optimal play.
- Let dp[i][j] be the best score available to the player whose turn it is on coins i through j.
- For each end, add its value to the opponent’s worse outcome, then take the better of your two choices.
- The full table takes O(n²) time and O(n²) space.
What is the optimal game strategy problem?
The optimal game strategy problem asks how many points the first player can guarantee when two players alternately take either the leftmost or rightmost coin from a row. Both players see every value and choose to maximize their own final score.
The opponent’s choice matters because a large coin taken now can expose a better sequence for them. A strategy that simply grabs the larger visible end does not account for what remains. The recurrence must model both players’ decisions, not just the first move.
Use an inclusive interval [i, j] to describe the remaining row. Define dp[i][j] as the maximum total the player to move can secure from that interval if both players respond optimally.
Why does the recurrence use min and max?
If you take coins[i], the opponent then chooses from [i+1, j]. Their two possible picks leave [i+2, j] or [i+1, j-1] for your next turn. Since they play well, your future score is the smaller of those results.
Taking coins[j] leaves [i, j-1] for the opponent, who can leave you [i, j-2] or [i+1, j-1]. You add the right coin to the smaller future score. You then choose the better of the left and right plans:
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])
);
Whenever an interval is empty, its score is zero. A one-coin interval scores that coin. In implementation, bounds checks handle these short intervals before reading the table.
How to solve the coin game with interval DP
Fill answers from shorter intervals toward longer ones, so each state reads only subintervals already computed. This bottom-up order avoids recursion and makes the table dependencies explicit.
Step 1: Define and initialize the table
For n coins, allocate an n by n vector of scores. Set dp[i][i] to coins[i], since the player takes the only coin left. Empty intervals are represented as zero when a transition checks their bounds.
The one-coin initialization matters: leaving the diagonal at a default zero would incorrectly say that a player gets nothing from a single remaining coin. Dynamic vectors also size storage from the input instead of relying on a fixed second dimension.
Step 2: Evaluate both end choices
Loop over interval lengths from two through n. For each left endpoint, calculate the matching right endpoint, evaluate the left-pick and right-pick expressions, and store their maximum.
For the sample [8, 15, 3, 7], the first player’s best guaranteed total is 22. Taking 7 first can lead to taking 15 next, while taking 8 first allows the opponent to constrain the later choice. The best move is therefore not necessarily the largest coin currently visible.
Step 3: Run the C++ solution
I compiled and ran the named C++ source using the sample coin row. The code below builds the interval table and returns the score guaranteed under optimal play.
#include <algorithm>
#include <iostream>
#include <vector>
long long bestScore(const std::vector<long long>& coins) {
const int n = static_cast<int>(coins.size());
if (n == 0) return 0;
std::vector<std::vector<long long>> dp(n, std::vector<long long>(n));
for (int i = 0; i < n; ++i) dp[i][i] = coins[i];
for (int length = 2; length <= n; ++length) {
for (int left = 0; left + length <= n; ++left) {
const int right = left + length - 1;
const long long afterLeft = std::min(
left + 2 <= right ? dp[left + 2][right] : 0LL,
left + 1 <= right - 1 ? dp[left + 1][right - 1] : 0LL);
const long long afterRight = std::min(
left + 1 <= right - 1 ? dp[left + 1][right - 1] : 0LL,
left <= right - 2 ? dp[left][right - 2] : 0LL);
dp[left][right] = std::max(coins[left] + afterLeft,
coins[right] + afterRight);
}
}
return dp[0][n - 1];
}
int main() {
const std::vector<long long> coins{8, 15, 3, 7};
std::cout << "Maximum guaranteed score: " << bestScore(coins) << '\n';
}
Run the compiled program to check the result. The output is “Maximum guaranteed score: 22”.

The capture shows the program’s actual result and exit status. It verifies this input, not every possible coin row.
How does the sample reach a score of 22?
The sample row contains 8, 15, 3, and 7. The first player cannot take the second coin directly, so the decision is about which end to remove and what that choice lets the opponent do next. The table evaluates complete sequences rather than scoring one move in isolation.
Taking 8 leaves [15, 3, 7]. The opponent can take 15 or 7. If they take 15, the remaining [3, 7] leaves the first player no more than 7.
If they take 7, the remaining [15, 3] leaves no more than 15. The opponent chooses the response that limits the first player’s eventual score. The recurrence accounts for the first player’s later answer in each subinterval.
Taking 7 instead leaves [8, 15, 3]. The opponent can take 8 or 3, and each reply leads to a different interval on the next turn.
The table compares the guaranteed results for both initial choices and selects the larger one. For this row, that choice yields 22. A move is valued by the result it guarantees after the response, not by the coin’s face value alone.
Why does the table fill by interval length?
Each state for [i, j] depends on intervals that are shorter by one or two positions. Filling all intervals of length one first, then length two, and continuing upward means those dependencies are ready before a longer interval is calculated. The order is a topological ordering of the recurrence: every value is computed after the values it reads.
For a fixed length, the left endpoint moves across the row and determines the right endpoint. There are n minus length plus one valid intervals of that length.
After the loop finishes, [0, n-1] covers the original row, so dp[0][n-1] is the requested score. An arbitrary index order could read cells before their values are ready.
What does the recurrence assume about the players?
The recurrence models two players who know the full row and each choose to maximize their own final total. Its inner minimum is a compact way to represent the opponent choosing the reply that leaves the current player with the worse future score. Its outer maximum represents the current player choosing the better of the two available ends.
The result is a guarantee under the stated rules, not a prediction of a particular person’s move. A greedy opponent who takes the larger visible end may respond differently, changing the game’s outcome. The optimal-play assumption makes the minimax recurrence appropriate.
How do the C++ bounds checks map to intervals?
The expressions afterLeft and afterRight can refer to empty intervals when two coins remain. Each conditional returns zero when the left endpoint passes the right endpoint.
Otherwise it reads the matching table cell. This implements the empty-interval base case without sentinel rows or columns.
The checks prevent negative indexing in terms that subtract two from the right endpoint. A two-coin interval can use the recurrence directly.
The opponent takes the remaining coin, leaving an empty interval worth zero. Handling the boundary at access keeps the table exactly n by n.
Edge cases and complexity
An empty input row returns zero, and a one-coin row returns that coin’s value. The transition checks whether each smaller interval exists before accessing dp, which covers the two-coin base transition without negative indexes.
The table has n² cells and each cell performs constant work, so time is O(n²) and space is O(n²). This is the cost of retaining every interval answer. A one-dimensional input array does not make the table itself O(n).
For very large coin values, choose a type wide enough for the sum of all values. The example uses long long, but input parsing or a different platform’s integer limits may still require validation against the problem’s constraints.
Common mistakes
These mistakes change the game being solved or break the interval boundaries, so checking them against the recurrence helps isolate a wrong result.
- Using max for the opponent’s response: from your point of view, the opponent chooses the branch that leaves you the smaller future score.
- Using a zero value to mean “not computed”: zero can be a valid result for empty intervals or zero-valued coins. The bottom-up version needs no memoization sentinel.
- Allocating a fixed-width table: inputs beyond that width can access memory out of bounds. Size the vector from n.
- Applying the result to greedy opponents: the recurrence assumes both players optimize their eventual totals, rather than grabbing the larger exposed coin.
A tiny interval is a practical audit case for an implementation. With two coins, either first choice leaves one coin to the opponent.
The current player’s guaranteed total is therefore the larger end value. With three coins, taking one end leaves a two-coin interval already represented in the table. These cases make the alternating decisions visible and can expose an incorrect index.
The interval formulation stores scores for every contiguous slice because a future turn can leave many different slices. It needs no move sequence to answer the score question.
If a task also asks for the chosen sequence, the implementation must retain or reconstruct which end produced each stored maximum. This distinction matters when converting a score-only solution into a witness-producing one. The table already has the choices implicit in its comparisons, but the current program discards them after storing the score.
Conclusion
For this coin game, each interval state measures what the current player can guarantee. The inner minimum models the opponent’s reply, and the outer maximum models your choice. That distinction produces a complete O(n²) C++ solution.
For the related interval-DP formulation, compare the Optimal Strategy for a Game explanation. For the container used here, see the C++ vector reference.
FAQ
These answers clarify what the recurrence guarantees and what its complexity measures.
Why does the optimal game strategy recurrence use min?
After your move, an optimal opponent chooses the response that leaves you with the smaller future score. The recurrence takes that minimum, then maximizes your own two end choices.
What is the time complexity of the optimal game strategy solution?
The interval table has n squared states and each state takes constant work, so the bottom-up solution runs in O(n^2) time and uses O(n^2) space.
Does this solution assume a greedy opponent?
No. It assumes both players choose optimally. A greedy opponent follows a different decision rule and needs a different recurrence.




