๐ค Why Minimax DP Matters: The Choose/Not-Choose Framework
Every two-player zero-sum game boils down to one question: "If I make this move, what's the best my opponent can do in response?"
Note
๐ง Aditya Verma's Choose/Not-Choose Framework: "At each number, I either pick it (score += num) or skip it โ my opponent plays optimally after me. So I maximize(current + min(opponent's options))."
This is the core insight that connects all 6 problems in this article. Whether you're picking stones from ends, guessing a number, or choosing integers from a set โ the framework is identical:
Define the state โ what does the current player need to know? (subarray i..j, range [i,j], bitmask of used numbers)
List the choices โ what actions can the current player take? (pick left/right, pick pivot k, pick number n)
Evaluate opponent's best response โ after each choice, what's the opponent's optimal result from the new state?
Pick the maximum โ I choose the action that maximizes my final outcome, assuming my opponent plays optimally.
The key formula that appears in every minimax DP:
Let's see how this plays out across 6 classic problems, ordered by increasing complexity:
#
Problem
DP Type
State
Choices
1
Predict the Winner
Interval DP
(i, j) subarray
Pick left or right
2
Guess Number Higher Lower II
Decision Tree DP
(i, j) range
Pick pivot k
3
Optimal Strategy for a Game
Interval DP
(i, j) subarray
Pick left or right
4
Sum Game
Interval DP
(i, j) subarray
Pick from ends
5
Can I Win
Bitmask DP
Bitmask of used numbers
Pick unused number
6
New 21 Game
Probability DP
Current score
Draw or stop
1. Predict the Winner โ Interval DP with Choose/Not-Choose
Problem: Given an integer array nums, two players pick from either end. Each picks the number and adds it to their score. The player with the higher score wins. Return true if the first player can win.
Note
๐ช Real-World Analogy: Imagine a row of treasure chests, each with a different amount of gold. You and a rival take turns picking from either end. Both of you are greedy AND smart โ you want the most gold, but you also know your rival will try to leave you with the least. How do you maximize your haul?
"I'm at subarray i..j. I have two choices: pick nums[i] (left) or nums[j] (right). After I pick, my opponent faces a smaller subarray and plays optimally. My net advantage is what I take minus what my opponent gets from the remainder."
๐ค The Choose/Not-Choice Pattern
At every subarray, I make a decision: do I pick the left element or the right element? This is the classic choose/not-choose pattern:
Choose left: I take nums[i] now. The opponent gets the best possible result from nums[i+1..j]. My net = nums[i] - opponent's best.
Choose right: I take nums[j] now. The opponent gets the best possible result from nums[i..j-1]. My net = nums[j] - opponent's best.
Decision: Pick the one that gives me the higher net advantage.
Key Insight
๐ Key Insight: The formula dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1])is the same for Predict the Winner, Optimal Strategy for a Game, and Stone Game I. These problems are IDENTICAL in their DP structure โ only the framing differs!
๐ Edge Cases to Consider
Single element: dp[i][i] = nums[i]. First player takes it. Win if nums[i] โฅ 0 (or > 0 depending on problem variant).
Two elements: dp[i][i+1] = max(nums[i] - nums[i+1], nums[i+1] - nums[i]) = |nums[i] - nums[i+1]|. The first player picks the larger one.
Even length array: The first player can force a win by controlling which elements they access (they can pick all odd-indexed or all even-indexed elements by choosing the appropriate end).
Zero scores: If all numbers are zero, dp[0][n-1] = 0. First player ties, not wins.
Negative numbers: The recurrence works with negatives. dp tracks net advantage, so negative values are fine.
Tip
โก Interview Tip: When you see "two players pick from ends" in an interview, immediately think interval DP with minimax. Say: "I'll define dp[i][j] as the maximum net advantage the current player can achieve from subarray i..j. At each step, I either pick left or right, and subtract the opponent's optimal response."
ProblemEmbed: predict-the-winner
2. Guess Number Higher Lower II โ Decision Tree Minimax
Problem: I'm guessing a number between 1 and n. Each time I guess wrong, I pay that amount. I need a strategy that minimizes the maximumamount I might have to pay (the "worst-case" cost).
Note
๐ช Real-World Analogy: You're playing "Guess the Number" with a sadistic host. Every wrong guess costs you money โ the amount of the wrong guess. You want a strategy that guarantees you won't lose more than X dollars, no matter what number the host picked. What's the minimum X you need?
๐ Real-Time Thinking โ Minimax on Decision Trees
"I pick a pivot k in [i, j]. If I guess k and it's wrong, I pay k and continue. The number is either lower (i..k-1) or higher (k+1..j). Since I want a guarantee, I prepare for the worst case โ I take the max of the two branches. But I can choose the pivot k that minimizes this worst-case cost."
This is minimax in its purest form:
Min โ I choose the pivot that minimizes my worst-case total cost
Max โ For each pivot, the worst case is the higher-cost branch (lower or higher)
The Choose/Not-Choose Pattern Here
This problem has a different flavor of choose/not-choose:
Choose pivot k: I decide which number to guess first. This determines the worst-case cost.
Not-choose is implicit: Every other number in the range is a potential pivot I'm testing.
But wait โ this is really "choose the best pivot": I test ALL pivots and pick the one that MINIMIZES the worst-case cost.
Key Insight
๐ Key Insight: Unlike Predict the Winner where I maximize my score, here I minimize the worst-case cost. The opponent is "nature" โ the worst possible outcome. The DP is min over k of (k + max(dp[i][k-1], dp[k+1][j])).
๐ Edge Cases to Consider
n = 1: dp[1][1] = 0. No wrong guesses needed. Only one possible number.
n = 2: Guess 1 first. If wrong (number is 2), pay 1 and guess 2 (correct). Total = 1. Guess 2 first: pay 2. So dp = 1.
n = 3: Best pivot is 2. dp = 2 + max(dp[1][1], dp[3][3]) = 2 + 0 = 2. If you guess 1: 1 + max(0, dp[2][3]) = 1 + 2 = 3. If you guess 3: 3 + max(dp[1][2], 0) = 3 + 1 = 4.
Large n: The optimal pivot tends toward the middle, but not always exactly โ the exact value depends on the cost asymmetry.
Warning
โ ๏ธ Common Mistake: This is a minimax problem, not a "minimize expected cost" problem. We don't take averages โ we prepare for the WORST case. The DP uses Math.max for the two branches (opponent chooses the worst branch), then Math.min over pivots (I choose the best pivot).
ProblemEmbed: guess-number-higher-lower-ii
3. Optimal Strategy & Sum Game โ More Interval DP Patterns
Optimal Strategy for a Game (GFG)
Problem: Same as Predict the Winner โ pick from ends, maximize your total sum. Compute the maximum sum the first player can collect.
The difference from Predict the Winner: instead of computing net advantage (which could be negative), this problem asks for the actual maximum sum the first player can guarantee. The recurrence adapts slightly:
Sum Game
Problem: A variant where the two players alternately pick elements from the ends. The goal is to maximize the sum difference (like Predict the Winner). Some versions add constraints on when you can pick (e.g., only if the sum of remaining is odd/even).
The unified framework for ALL interval-pick-from-ends games:
Tip
โก Pro Tip: There are only TWO distinct intervals DP patterns for "pick from ends" games:
Net advantage (Predict the Winner): dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1])
Absolute sum (Optimal Strategy): prefix sums + dp over remaining elements
Memorize BOTH โ interview problems can ask for either framing.
ProblemEmbed: optimal-strategy-game
ProblemEmbed: sum-game
4. Can I Win โ Bitmask DP with Minimax
Problem: Two players take turns picking numbers from 1 to maxChoosableInteger. Each number can be used at most once. The player who makes the cumulative sum reach or exceed desiredTotal wins. Can the first player force a win?
Note
๐ช Real-World Analogy: Imagine a game where you and a friend take turns picking numbers off a board. Each number can only be picked once. If you hit exactly the target total (or exceed it), you win. You need to figure out โ before the game starts โ whether you have a guaranteed winning strategy. This is like chess at the start: is there a forced win?
๐ Real-Time Thinking โ Choose/Not-Choose with Bitmask
"I have a set of available numbers. I pick one. If my cumulative score reaches the target, I win. Otherwise, it's my opponent's turn with a smaller set. I need to know: is there ANY number I can pick that makes my opponent lose? If yes, I win."
The state is a bitmask: an integer where bit i is 1 if number (i+1) has been used. This is the choose/not-choose pattern applied to picking from a set (not from ends):
Choose number n: Mark it as used (set bit n-1 in mask). Add n to cumulative score.
Not-choose: Every other available number is a potential choice I'm evaluating.
Decision: If ANY choice makes opponent lose, I win from this state.
Choose/Not-Choose with Bitmask
This is where the choose/not-choose framework meets bitmask DP:
State = bitmask: 2โฟ states where n = maxChoosableInteger. Each bit tells us if a number is used.
Choice = pick an unused number: For each 0-bit, we can set it to 1 and recurse.
Minimax check: "If I pick number i and the opponent can't force a win from the new state โ I win."
Memoization: dp[mask] stores whether the current player can force a win from this state.
Key Insight
๐ Key Insight: The recurrence is not about maximizing a score โ it's about existential choice: "Is there ANY move I can make that leaves my opponent in a losing position?" This is the classic recursive game theory approach: a state is winning if there exists a move to alosing state.
๐ Edge Cases to Consider
desiredTotal โค 0: First player wins immediately without making any move.
Sum of all numbers < desiredTotal: Nobody can reach the target. First player loses.
maxChoosableInteger โฅ desiredTotal: First player can pick desiredTotal directly and win in one move.
Large maxChoosableInteger (n > 20): 2โฟ states may be too large. But usually the problem constraints keep it manageable (n โค 20).
Even/odd parity: If all numbers can be paired to reach the target, the second player might have a mirroring strategy. Typical for desiredTotal = (maxChoosableInteger * (maxChoosableInteger + 1) / 2) where the player who goes second can mirror.
Warning
โ ๏ธ Performance Note: The bitmask approach is O(2โฟ ร n) in the worst case. For maxChoosableInteger = 20, that's about 20 million operations โ feasible in most languages. For n = 30, it's too slow. Look for the "desiredTotal <= 0" and "totalSum < desiredTotal" quick checks to short-circuit early.
ProblemEmbed: can-i-win
5. New 21 Game โ Probability DP with Optimal Stopping
Problem: You start with 0 points. You draw numbers from [1, maxPts] with equal probability until your score reaches or exceeds K. If your final score โค N, you win. What's the probability of winning?
Note
๐ช Real-World Analogy: You're playing a modified Blackjack where you MUST draw until you reach at least K points. If you go over N, you lose. The deck is infinite (replaced after each draw). What's your chance of landing in the sweet spot [K, N]?
๐ Real-Time Thinking โ Probability DP
"I'm at score x. I can draw any number from 1 to maxPts. Each has probability 1/maxPts. After drawing, my new score is x + i. If x + i โฅ K, I stop. I win if x + i โค N. If not, I continue drawing."
The Choose/Not-Choose Pattern Here
New 21 Game has a different flavor of minimax โ it's probability DP with optimal stopping:
Choose to draw: Must draw if score < K. The draw is random (1..maxPts), each equally likely.
Stop (not-choose): Once score โฅ K, you stop automatically and check if score โค N.
The "minimax" here is against nature: The opponent is randomness, not another player. We compute expected probability, not worst-case.
Key Insight
๐ Key Insight: The sliding window technique makes this O(n) instead of O(n ร maxPts). Instead of summing dp[i - 1] + dp[i - 2] + ... + dp[i - maxPts] at each step, we maintain a running sum and update it in O(1).
๐ Edge Cases to Consider
K = 0: Already stopped before starting. If 0 โค N, probability = 1.
N โฅ K + maxPts - 1: You can always reach K without exceeding N. Maximum possible score is K + maxPts - 1. If N โฅ that, you always win.
K = 1: You stop after exactly one draw. Probability = min(N, maxPts) / maxPts.
N < K: You can never win because you must reach โฅ K to stop, but that means score > N.
Large maxPts: The DP is O(n) regardless of maxPts thanks to sliding window.
Tip
โก Pro Tip: New 21 Game is not a two-player minimax problem โ it's a probability DP with optimal stopping. But it completes our minimax journey by showing how "choose/not-choose against nature" uses the same decision-making framework, just with probabilities instead of an adversarial opponent.
ProblemEmbed: new-21-game
๐ฏ Interview Cheat Sheet
Key Insight
Q1: How do I know if a problem is minimax DP? Look for: "two players", "alternating turns", "both play optimally", "guarantee win", "minimize maximum cost". If any of these appear, it's minimax.
Key Insight
Q2: What's the difference between interval DP and bitmask DP for games? Interval DP (i, j): Used when choices are from ends of a contiguous range. O(nยฒ) states. Filling order is by increasing interval length. Bitmask DP (mask): Used when choices are from a SET of items (pick any unused item). O(2โฟ) states. Ordering is typically memoized recursion.
Key Insight
Q3: When is a state "winning" vs "losing"? A state is winning if there exists a move that leads to a losing state for the opponent. A state is losing if ALL moves lead to winning states for the opponent. Base cases (terminal states) are trivial to classify.
Key Insight
Q4: How do I handle "net advantage" vs "absolute sum"? Net advantage: dp[i][j] = myScore - opponentScore. First player wins if dp[0][n-1] > 0. Absolute sum: requires prefix sums or tracking remaining sum. Net advantage is simpler โ use it when the problem asks "can first player win".
Key Insight
Q5: What's the general recurrence template? dp[state] = max over choices ( myGain - dp[opponentState] )for "I take, you take" games. dp[state] = min over k ( k + max(dp[left], dp[right]) )for "choose pivot, then worst case" games. And dp[mask] = exists choice ( !dp[mask | bit] )for "can I force a win" games.
Key Insight
Q6: How do I fill the DP table for interval DP? Always fill by increasing subarray length (gap). Start with len = 1 (base case: dp[i][i]), then len = 2, 3, ... up to n. Each dp[i][j] depends on dp[i+1][j] and dp[i][j-1] โ which are shorter intervals already computed.
๐ Key Takeaways
Minimax = "I maximize, opponent minimizes" โ always assume optimal play from both sides.
Aditya Verma's choose/not-choose: "At each number, I either pick it (score += num) or skip it โ my opponent plays optimally after me. So I maximize(current + min(opponent's options))."
"Pick from ends" โ interval DP โ dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1]). This covers Predict the Winner, Optimal Strategy, Stone Game I, Sum Game.
"Guess to minimize worst-case" โ decision tree DP โ dp[i][j] = min over k of (k + max(dp[i][k-1], dp[k+1][j])). Used in Guess Number Higher Lower II.
"Pick from set, each once" โ bitmask DP โ dp[mask] = exists unused number i such that !dp[mask | (1 << i)]. Used in Can I Win.
"Stop when reaching threshold" โ probability DP โ dp[i] = sum(dp[i-w]...dp[i-1]) / maxPts with sliding window. Used in New 21 Game.
Winning state: exists a move to a losing state for opponent.
Losing state: all moves lead to winning states for opponent.
Time: O(nยฒ) for interval DP, O(2โฟ ร n) for bitmask DP, O(n) for probability DP with sliding window.
Practice! โ The only way minimax becomes intuitive is by solving problems. The 6 problems here are your training ground ๐ฅ
Note
๐ฎ What's Next? Now that you've mastered minimax DP, check out the Stone Game Series for more complex minimax patterns with variable pick sizes and split-based games!
// General minimax recurrencedp[state] = max over each choice ( myImmediateGain + (totalRemaining - dp[opponentState]))// Simplified when game is "I take, you take from remainder":dp[state] = max over each choice ( myImmediateGain - dp[opponentState])// Because total = myGain + opponentGain, so opponentGain = total - myGain// And if dp tracks "net advantage": myGain - opponentGain = 2*myGain - total
// At subarray i..j, I either:// Option A: Pick left โ take nums[i], opponent gets dp[i+1][j] advantage// myNet = nums[i] - dp[i+1][j]//// Option B: Pick right โ take nums[j], opponent gets dp[i][j-1] advantage// myNet = nums[j] - dp[i][j-1]//// I want the MAXIMUM net advantage:// dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1])function predictTheWinner(nums) { const n = nums.length; const dp = Array.from({ length: n }, () => Array(n).fill(0)); // Base case: single element subarray for (let i = 0; i < n; i++) dp[i][i] = nums[i]; // Fill diagonally โ increasing subarray length for (let len = 2; len <= n; len++) { for (let i = 0; i <= n - len; i++) { const j = i + len - 1; const pickLeft = nums[i] - dp[i + 1][j]; const pickRight = nums[j] - dp[i][j - 1]; dp[i][j] = Math.max(pickLeft, pickRight); } } // If net advantage >= 0, first player can win return dp[0][n - 1] >= 0;}
function getMoneyAmount(n) { const dp = Array.from({ length: n + 1 }, () => Array(n + 1).fill(0)); // Fill by increasing range length for (let len = 2; len <= n; len++) { for (let i = 1; i <= n - len + 1; i++) { const j = i + len - 1; let minCost = Infinity; // Try every pivot k in [i, j] for (let k = i; k <= j; k++) { // Cost if I pick k: pay k, then continue in worst of two ranges const cost = k + Math.max( k > i ? dp[i][k - 1] : 0, // lower branch k < j ? dp[k + 1][j] : 0 // higher branch ); minCost = Math.min(minCost, cost); } dp[i][j] = minCost; } } return dp[1][n];}
// dp[i][j] = maximum sum current player can achieve from subarray i..j// (opponent will try to minimize our total, but we track absolute sum, not net)function optimalStrategy(arr) { const n = arr.length; const dp = Array.from({ length: n }, () => Array(n).fill(0)); // Base: single element for (let i = 0; i < n; i++) dp[i][i] = arr[i]; for (let len = 2; len <= n; len++) { for (let i = 0; i <= n - len; i++) { const j = i + len - 1; // Total sum of current subarray const total = arr[i] + (i + 1 < n ? dp[i + 1][j] : 0) // placeholder // Actually we need prefix sums for O(1) total calculation // If I pick left: I get arr[i] + (total - arr[i] - opponent's best from rest) // Simplified recurrence: const pickLeft = arr[i] + Math.min( i + 2 <= j ? dp[i + 2][j] : 0, i + 1 <= j - 1 ? dp[i + 1][j - 1] : 0 ); const pickRight = arr[j] + Math.min( i + 1 <= j - 1 ? dp[i + 1][j - 1] : 0, i <= j - 2 ? dp[i][j - 2] : 0 ); dp[i][j] = Math.max(pickLeft, pickRight); } } return dp[0][n - 1];}// Simpler recurrence using "net advantage" from Predict the Winner:// dp[i][j] = max(arr[i] - dp[i+1][j], arr[j] - dp[i][j-1])// then: firstPlayerSum = (totalSum + dp[0][n-1]) / 2// Because dp[0][n-1] = firstPlayerSum - secondPlayerSum// and totalSum = firstPlayerSum + secondPlayerSum// So firstPlayerSum = (totalSum + dp[0][n-1]) / 2
// UNIVERSAL PATTERN for "pick from ends"// =========================================// dp[i][j] = net advantage of current player//// dp[i][j] = max(nums[i] - dp[i+1][j], nums[j] - dp[i][j-1])// โ โ โ// โ โโ pick right, subtract opponent's advantage// โโ pick left, subtract opponent's advantage//// Base: dp[i][i] = nums[i] (only one element, take it)// Fill order: increasing subarray length (gap from 1 to n-1)// Answer: dp[0][n-1] > 0 โ first player wins
function canIWin(maxChoosableInteger, desiredTotal) { // Quick check: if sum of all numbers < desiredTotal, impossible const totalSum = maxChoosableInteger * (maxChoosableInteger + 1) / 2; if (totalSum < desiredTotal) return false; // If desiredTotal <= 0, first player automatically wins if (desiredTotal <= 0) return true; const memo = new Map(); // or array of size 2^n function canWin(mask, currentTotal) { const key = mask; if (memo.has(key)) return memo.get(key); for (let i = 1; i <= maxChoosableInteger; i++) { const bit = 1 << (i - 1); if ((mask & bit) === 0) { // number i is available // Choose i const newTotal = currentTotal + i; if (newTotal >= desiredTotal) { // I win immediately! memo.set(key, true); return true; } // Recurse: opponent plays from new state if (!canWin(mask | bit, newTotal)) { // Opponent cannot win โ I win! memo.set(key, true); return true; } } } // No winning move found memo.set(key, false); return false; } return canWin(0, 0);}
function new21Game(n, k, maxPts) { // Edge case: if K == 0 or N >= K + maxPts - 1, probability = 1 if (k === 0 || n >= k + maxPts) return 1.0; const dp = new Array(n + 1).fill(0); dp[0] = 1.0; // starting from score 0 let windowSum = 1.0; // sum of probabilities in the sliding window let result = 0.0; for (let i = 1; i <= n; i++) { // Probability of reaching score i dp[i] = windowSum / maxPts; // If i >= K, this is a terminal win state if (i >= k) { result += dp[i]; } // Maintain sliding window: add dp[i] if i < K (can still draw) if (i < k) { windowSum += dp[i]; } // Remove dp[i - maxPts] from window if it's out of range if (i - maxPts >= 0) { windowSum -= dp[i - maxPts]; } } return result;}