Skip to main content

Dynamic Programming

Introduction

In the previous section, we explored greedy algorithms, strategies that build solutions by making locally optimal choices at each step. Greedy algorithms are often fast and simple, but they sometimes fail; some problems require us to consider multiple possible choices before we know which is best.

Dynamic programming (DP) breaks a problem into overlapping subproblems and solve each one only once, storing results for reuse. This caching/storing often reduces exponential time to polynomial. Unlike greedy algorithms, DP explores the full space of subproblem solutions rather than committing to a single choice at each step, making it more broadly applicable at the cost of additional time and space.

Types of Dynamic Programming

Top-down (memoization)

This form of DP starts at the “top”, recursively decomposing the problem until hitting the base cases. Results are cached/stored to avoid redundant computation. For example, calculating the nnth Fibonacci number begins with fib(n)fib(n), which recurses into fib(n1)fib(n - 1) and fib(n2)fib(n - 2), bottoming out at our base cases n=0n = 0 and n=1n = 1.

Bottom-up (tabulation)

This form of DP starts at the "bottom", iteratively building up solutions to larger subproblems from smaller ones. We fill a table or array in order, so that each entry is already by the time we need it. For example, when calculating the nnth Fibonacci number, we start from the bases cases fib(0)=0fib(0) = 0 and fib(1)=1fib(1) = 1, then iteratively compute fib(i)=fib(i1)+fib(i2)fib(i) = fib(i - 1) + fib(i - 2) for increasing ii until we reach fib(n)fib(n).

General Framework

Dynamic programming encompasses a wide variety of problem types, but most can be solved by working through the same core questions. The following tips and techniques address each one.

1. Identify Subproblems and State

Often, one of the hardest parts of DP is figuring out what a "subproblem" looks like. You should ask yourself: what information do I need to fully describe the state of the problem at any point?

Some common subproblem structures include:

  • Including/excluding an element: should I take element ii or not?
  • Prefix/suffix: optimal solution on the first ii elements
  • Interval: optimal solution on subarray [i,j][i, j]
  • Remaining capacity: items and budget/weight left (e.g. knapsack)

Once you've identified the subproblem structure, you should make it precise! Write a one/two sentence definition of OPT(i)OPT(i); this forces you to commit to exactly what the subproblem is optimizing, which makes the recurrence much easier to write correctly.

2. Write the recurrence relation and base case

After defining OPT(i)OPT(i), you should ask yourself: how does OPT(i)OPT(i) relate to strictly smaller subproblems? This gives your recurrence. Don't forget to consider all cases (e.g. edge cases, zero capacity)!

Base cases are where the recurrence "bottoms out". You should ask yourself: what is the smallest valid input to your table, and what is the answer there by definition (not by recurrence)?

3. Computation Order

You should compute each subproblem only after all subproblem it depends on. You can determine the dependency direction from your reccurence. Think about which subproblems need to be solved first, and which ones depend on others.

4. Correctness

Prove correctness by strong induction on the subproblem "size" (however you have defined it in your solution). With the inductive structure, first verify that the smallest subproblems are corrctly computed with the base cases. After, assume OPT(i)OPT(i') is correct for all i<ii' < i (inductive hypothesis). Show that the recurrence for OPT(i)OPT(i) produces the correct answer.

5. Runtime

You should analyze the runtime by considering the number of subproblems ×\times the work per subproblem. In addition, you should consider any precomputations done before filling in the table entries (e.g. computing prefix sums), as well as any steps required for solution reconstruction.

6. Solution Reconstruction

Often, the answer for your DP problem can be found by simply looking into an OPTOPT table value. Other times, you may need to iterate over your table and take the optimum across select entries. Another common pattern for solution reconstruction is backtracking; sometimes, we don't just care about the numerical value of our solution, but rather the steps (for example, after running Bellman-Ford, we might want to return the total cost AND the nodes along the least-cost path). To do this, you should:

  • During the forward pass, store a point at each table entry that records which case the recurrence takes.
  • Trace back from the final state following these pointers (backtracking from your final solution to recover the path).

Remember to include these steps in your runtime analysis!

Bellman-Ford

Introduction

Consider the single-source shortest path problem: given a weighted directed graph G=(V,E)G=(V,E) and a source node ss, find the shortest path from ss to every other vertex. Dijkstra's algorithm solves this greedily, but fails on graphs with negative edge weights. Consider the following graph, where we let our source node be SS.

Dijkstra counterexample with a negative edgeUndirected graph with edges S A weight 2, S B weight 5, and A B weight negative 4.25-4SAB

If we use Dijkstra's algorithm to find the shortest path from SS to AA, we would greedily select the edge from SS to AA with weight 22 and be done. However, we observe that the least cost path is SBAS \rightarrow B \rightarrow A (total cost 5+(4)=15 + (-4) = 1).

This failure of Dijkstra's algorithm motivates the Bellman-Ford algorithm, which handles negative edge weights by reformulating shortest paths as a DP problem. We add the constraint that the input graph GG has no negative weight cycles (otherwise there is no shortest path, as we could repeatedly take the cycle path to lower our overall cost).

The Algorithm

We first observe that any shortest path in the graph with n=Vn = |V| nodes uses at most n1n - 1 edges. A simple path cannot repeat vertices; a non-sample path would contain a cycle, which either has positive cost (and can be removed) or negative cost (contradicting our negative-cost cycle assumption).

We define the subproblem OPT(i,v)OPT(i, v) to be cost of the shortest path from source node ss to vv using at most ii edges. The previous observation gives us a finite bound (n1)(n - 1) on the number of edges any shortest path can use.

Our base cases are OPT(0,s)=0OPT(0, s) = 0 and OPT(0,v)=OPT(0, v) = \infty for all vsv \ne s (impossible to reach node vsv \ne s using 00 edges). For our recurrence, the shortest path can use at most i1i - 1 edges (no change), or it arrives at vv via some edge (u,v)(u, v) using at most i1i - 1 edges to reach uu:

OPT(i,v)=min(OPT(i1,v),min(u,v)E[OPT(i1,u)+w(u,v)])\begin{align*} OPT(i, v) = \min\left(OPT(i - 1, v), \min_{(u, v) \in E} \left[ OPT(i - 1, u) + w(u, v) \right] \right) \end{align*}

To fill out the table, we can fill it out for each of the n1n - 1 rounds, iterating through each node to update its minimum-path cost. We output OPT(n1,v)OPT(n - 1, v) for all vv as our final answer.

The table has dimensions n×Vn \times |V|, so there are O(nV)O(n \cdot |V|) cells to fill. The cost per cell isn't O(1)O(1); to compute OPT(i,v)OPT(i, v), we take the minimum over all incoming edges to vv, which varies by vertex. If we sum the work across all vertices in a single round ii:

vVdeg(v)=O(E)\begin{align*} \sum_{v \in V} \deg(v) = O(|E|) \end{align*}

since each edge (u,v)E(u, v) \in E contributes one or two computations (depending on an undirected or directed graph) for nodes uu and vv. Since we drop constants, each round requires O(E)O(|E|) work, giving a total time complexity of O(VE)O(|V| \cdot |E|).

Bellman-Ford DP Visualizer

Initialize base case

The base case sets the source distance to 0 before any edges are used.

Step 1 / 16
dp[i][v] = min(dp[i - 1][v], min(dp[i - 1][u] + w(u, v)))
45-234sabc

Full DP table

Row 0 initializes s to 0 and every other entry to ∞. Later rows fill in as the simulation progresses.

isabc
00---
1----
2----
3----

Segmented Least Squares

Introduction

Given points in the plane, we often want to describe their trend by fitting a line. The classic least squares line has a clean closed form of minimizing the squared distance between points and the line, but a single line isn't always a good model: data that rises, levels off, then climbs again is captured far better by a sequence of line segments.

This creates a tradeoff. More segments always lower the total error (with nn point and n1n - 1 segments connecting each consecutive pair of points, our squared error is zero), but such a fit just memorizes the data. We want few segments and low error at the same time. The segmented least squares problem formalizes and solves this balance.

For points (x1,y1),,(xn,yn)(x_1, y_1), \dots, (x_n, y_n) and a line L:y=ax+bL : y = ax + b, the least squares error is the sum of squared vertical distances:

Error(L)=i=1n(yiaxib)2.\text{Error}(L) = \sum_{i = 1}^{n} \left(y_i - a x_i - b\right)^2.

Setting the partials with respect to aa and bb to zero gives the minimizing line in closed form:

a=nixiyi(ixi)(iyi)nixi2(ixi)2,b=iyiaixin.a = \frac{n \sum_i x_i y_i - \left(\sum_i x_i\right)\left(\sum_i y_i\right)}{n \sum_i x_i^2 - \left(\sum_i x_i\right)^2}, \qquad b = \frac{\sum_i y_i - a \sum_i x_i}{n}.

The main takeaway from this is that for any group of points, the minimum single-line error can be computed quickly.

We are given points P={(x1,y1),,(xn,yn)}P = \{(x_1, y_1), \dots, (x_n, y_n)\} with x1<<xnx_1 < \dots < x_n. A solution partitions them into contiguous segments, fitting one line per segment. Writing e(S)e(S) for the minimum single-line error on segment SS, and charging a fixed penalty C>0C > 0 per segment, the cost of a partition is

segments Se(S)  +  C(number of segments).\sum_{\text{segments } S} e(S) \; + \; C \cdot (\text{number of segments}).

Design a polynomial time algorithm to find a partition minimizing this cost. Observe that having large CC favors a coarse fit, while small CC tolerates more segments. In addition, the brute force algorithm requires trying all 2n12^n - 1 partitions, which is far too slow.

The Algorithm

Let pip_i refer to point (xi,yi)(x_i, y_i). Consider the last point pnp_n. It sits in some segment, which (by contiguity) must be pi,,pnp_i, \dots, p_n for some ini \le n. Once we fix where that segment starts, the points p1,,pi1p_1, \dots, p_{i-1} form an independent instance of the same problem. That substructure gives the DP relation.

Let ei,je_{i,j} be the minimum single-line error on pi,,pjp_i, \dots, p_j, and let OPT(j)\text{OPT}(j) be the optimal cost for the first jj points. We try every possible starting point ii for the final segment:

OPT(j)=min1ij(ei,j+C+OPT(i1)),OPT(0)=0.\text{OPT}(j) = \min_{1 \le i \le j} \Big( e_{i,j} + C + \text{OPT}(i-1) \Big), \qquad \text{OPT}(0) = 0.

We fill in the table from j=1,nj = 1, \dots n, and output OPT(n)\text{OPT}(n) as our answer. To recover the segments, store the argmin ii at each jj and trace back from nn.

Precomputing all ei,je_{i,j}: there are O(n2)O(n^2) pairs, and with prefix sums of xkx_k, yky_k, xkykx_k y_k, xk2x_k^2, each error is an O(1)O(1) lookup, so we take O(n2)O(n^2) time for the precomputations. Filling the table costs O(j)O(j) per entry, which again O(n2)O(n^2) across all nn OPT entries. Thus, the overall runtime of our algorithm is O(n2)O(n^2).

Knapsack Problem

Introduction

The knapsack problem is a classic dynamic programming example. We have nn items with weight wiw_i and value viv_i with an overall weight constraint WW. We want to select some set of items S{1,,n}S \subseteq \{1, \dots, n\} such that the total weight iSwiW\sum_{i \in S} w_i \le W and the total value iSvi\sum_{i \in S} v_i is maximized.

We can't solve this greedily; taking the highest-value items or best value-to-weight ratio first can both lead to suboptimal solutions (often times a combination of smaller, lower-ratio items fits better and gives more total value). This forces us to consider some subsets; however, there are 2n2^n of them. Dynamic programming gives us a way to avoid this exponential blowup by identifying the right subproblem structure.

The Algorithm

We observe that deciding whether to include item ii is a binary choice: we either take it or leave it. If we take it, we use wiw_i of our capacity and gain viv_i in value, leaving us with a smaller knapsack on the remaining items. If we choose not to take the item, the problem reduces to the same capacity with one fewer item to consider.

Thus, we let our OPT[i][w]OPT[i][w] be the max value possible with items 1,,i1, \dots, i and weight limit ww. For our base cases, we let OPT[i][0]=0OPT[i][0] = 0 for all ii and OPT[0][w]=0OPT[0][w] = 0 for all ww. Intuitively, a weight limit of zero leaves no room for any item, and considering zero items leaves nothing to select; in both cases, the achievable value is 0.

Using our observation, we have two cases to consider in our recurrence OPT[i][w]OPT[i][w].

  • Leave item ii: item ii is not included, so the optimal value is just OPT[i1][w]OPT[i - 1][w], which is the best we can do with the first i1i - 1 items and the same capacity.
  • Take item ii: item ii is included, contributing viv_i to our value and consuming wiw_i of our capacity. The remaining capacity wwiw - w_i must be allocated optimally among the first i1i - 1 items, giving vi+OPT[i1][wwi]v_i + OPT[i - 1][w - w_i]. This case is valid when wiww_i \le w.

Taking the better of the two choices, we have the recurrence:

OPT[i][w]={OPT[i1][w]if wi>wmax(OPT[i1][w], vi+OPT[i1][wwi])otherwise\begin{align*} OPT[i][w] = \begin{cases} OPT[i-1][w] & \text{if } w_i > w \\ \max(OPT[i-1][w],\ v_i + OPT[i-1][w - w_i]) & \text{otherwise} \end{cases} \end{align*}

To fill out the OPT table, we let our outer loop iterate from 1,,n1, \dots, n and inner loop iterate from 1,W1, \cdots W (this way, each subproblem OPT[i][w]OPT[i][w] is computed only after OPT[i1][w]OPT[i - 1][w] and OPT[i1][wwi]OPT[i - 1][w - w_i] are already filled). After filling out the entire table, we output OPT[n][W]OPT[n][W] as our final answer.

The table has (n+1)×(W+1)(n + 1) \times (W + 1) entries, each of which are filled in O(1)O(1) time using our recurrence (only 1 or 2 choices per table entry). Our total runtime is O(nW)O(nW).

note

This is a pseudo-polynomial algorithm: it is polynomial in the numeric value of W, but W can be exponential in the number of bits needed to represent it. The algorithm is efficient when WW is small, but can be exponentially slow when given a large WW.

Practice

Problem 1. [Longest Common Subsequence] Given two lists of integers, find the length of their longest common subsequence (LCS). A subsequence is a new sequence derived from an existing sequence by deleting some or all of its elements without changing the relative order of the remaining elements. A common subsequence appears in both subsequences. You don't have to output the LCS, just its length.

Example:

  • List A: 3 1 3 2 7 4 8 2
  • List B: 6 5 1 2 3 4

The longest common subsequence of lists A and B is 1 2 4 (or 1 3 4), so the answer is 3.

Solution

We use dynamic programming to solve this question. Let the length of list AA be nn and the length of list BB be mm. We create an n×mn \times m OPT table, where OPT[i][j] represents the longest common subsequence that can be formed using the sequences ai,,ana_i, \dots, a_n and bj,,bmb_j, \dots, b_m.

For any i>ni > n or j>mj > m, let OPT[i][j]=0OPT[i][j] = 0. We construct our OPT table using the following relation for 1in1 \le i \le n, 1jm 1 \le j \le m.

OPT[i][j]={1+OPT[i+1][j+1]if ai=bjmax(OPT[i+1][j],OPT[i][j+1])otherwiseOPT[i][j] = \begin{cases} 1 + OPT[i + 1][j + 1] & \text{if } a_i = b_j \\ \max(OPT[i + 1][j], OPT[i][j + 1]) & \text{otherwise} \end{cases}

For our final answer, we output OPT[1][1]OPT[1][1].

Here is a pseudocode implementation:

initialize (n + 1) x (m + 1) OPT table
Let OPT[n + 1][j] and OPT[i][m + 1] be 0 for all i, j.
for i = n down to 1:
for j = m down to 1:
OPT[i][j] = recurrence relation
output OPT[1][1]

Correctness:

Claim: OPT[i][j]OPT[i][j] equals the length of the longest common subsequence of ai,,ana_i, \dots, a_n and bj,,bmb_j, \dots, b_m.

Proof: By induction on (ni)+(mj)(n - i) + (m - j)

Base case: When i>ni > n or j>mj > m, at least one of the sequences is empty, so the LCS has length 0=OPT[i][j]0 = OPT[i][j].

Inductive Step: Let 1in1 \le i \le n and 1jm1 \le j \le m, and assume the claim holds for all (i,j)(i', j') with (ni)+(mj)<(ni)+(mj)(n - i') + (m - j') < (n - i) + (m - j). In particular, it holds for (i+1,j+1)(i + 1, j + 1), (i+1,j)(i + 1, j), and (i,j+1)(i, j + 1).

Let LL denote the true length of the LCS of ai,,ana_i, \dots, a_n and bj,,bmb_j, \dots, b_m.

Case 1: ai=bja_i = b_j. We claim L=1+OPT[i+1][j+1]L = 1 + OPT[i+1][j+1]. By the inductive hypothesis, OPT[i+1][j+1]OPT[i+1][j+1] equals the LCS length of ai+1,,ana_{i+1}, \dots, a_n and bj+1,,bmb_{j+1}, \dots, b_m, so 1+OPT[i+1][j+1]1 + OPT[i+1][j+1] is achievable by prepending the match (ai,bj)(a_i, b_j). Conversely, any common subsequence of ai,,ana_i, \dots, a_n and bj,,bmb_j, \dots, b_m has length at most 1+OPT[i+1][j+1]1 + OPT[i+1][j+1], since after optionally matching aia_i with bjb_j, the remainder is a common subsequence of ai+1,,ana_{i+1}, \dots, a_n and bj+1,,bmb_{j+1}, \dots, b_m. Thus L=1+OPT[i+1][j+1]L = 1 + OPT[i+1][j+1].

Case 2: aibja_i \neq b_j. Since aia_i and bjb_j cannot be matched, any common subsequence either excludes aia_i or excludes bjb_j (or both). Therefore L=max(L1,L2)L = \max(L_1, L_2), where L1L_1 is the LCS length of ai+1,,ana_{i+1}, \dots, a_n and bj,,bmb_j, \dots, b_m, and L2L_2 is the LCS length of ai,,ana_i, \dots, a_n and bj+1,,bmb_{j+1}, \dots, b_m. By the inductive hypothesis, L1=OPT[i+1][j]L_1 = OPT[i+1][j] and L2=OPT[i][j+1]L_2 = OPT[i][j+1], so L=max(OPT[i+1][j],OPT[i][j+1])L = \max(OPT[i+1][j], OPT[i][j+1]).

In both cases, OPT[i][j]=LOPT[i][j] = L, completing the induction.

Runtime:

To find our solution, we fill in our n×mn \times m sized OPT table. We fill out each entry using our recurrence relation, which takes O(1)O(1) time to evaluate. Thus, our overall runtime is O(mn)O(mn).

Problem 2. [Lunch Selection] David loves sushi! Fortuantely, right outside of his house, there is a restaurant that sells both salmon and tuna rolls. Every day for lunch across nn days, he must choose to eat exactly one of the tuna or salmon rolls. He will gain a satisfaction reward si>0s_i > 0 for eating a salmon roll and ti>0t_i > 0 for eating a tuna roll on day 1in1 \le i \le n.

Unfortunately, the restaurant's salmon supply is limited, so David refuses to eat salmon on two consecutive days: if he eats a salmon roll on day ii, he MUST eat a tuna roll on day i+1i + 1 (there is no such restriction on tuna). Subject to this constraint, he wants to maximize his total satisfaction ri(xi)\sum_{r_i(x_i)}, where xi{S,T}x_i \in \{S, T\} is his choice on day ii, ri(S)=sir_i(S) = s_i, and ri(T)=tir_i(T) = t_i.

Given an algorithm that takes as input s1,,sns_1, \dots, s_n and t1,,tnt_1, \dots, t_n and outputs the maximum total satisfiaction David can achieve subject to the constraint above. Your algorithm should run in O(n)O(n) time.

Solution

Subproblems. Let dp[i][S]\text{dp}[i][S] be the max total satisfaction over days 1,,i1,\dots,i given that David eats salmon on day ii, and dp[i][T]\text{dp}[i][T] the max total satisfaction over days 1,,i1,\dots,i given that he eats tuna on day ii.

For our recurrence, note that eating salmon on day ii forces tuna on day i1i-1:

dp[i][S]=dp[i1][T]+si\begin{align*} \text{dp}[i][S] = \text{dp}[i-1][T] + s_i \end{align*}

Tuna on day ii has no restriction on day i1i-1, so David could have consumed either salmon or tuna:

dp[i][T]=max(dp[i1][S], dp[i1][T])+ti\begin{align*} \text{dp}[i][T] = \max\big(\text{dp}[i-1][S],\ \text{dp}[i-1][T]\big) + t_i \end{align*}

We initialize our DP table with base cases dp[1][S]=s1\text{dp}[1][S] = s_1 and dp[1][T]=t1\text{dp}[1][T] = t_1. We fill the table by iterating from i=1i = 1 up to i=ni = n, and output max(dp[n][S], dp[n][T])\max\big(\text{dp}[n][S],\ \text{dp}[n][T]\big) as our final answer.

Correctness. Every valid choice sequence corresponds to exactly one path through these states, and each transition enumerates exactly the valid predecessors (salmon only follows tuna; tuna follows either). Since the reward from day ii onward depends only on the choice at day ii, not on the earlier history, optimal substructure holds: an optimal solution restricted to the first ii days is itself optimal among sequences ending in that day's choice.

Runtime. Each of the 2n2n states is computed in O(1)O(1) from the previous day's two values, so the algorithm runs in O(n)O(n) time and O(1)O(1) space (since we only need day i1i - 1's values to compute day ii's values, we can simply keep two variables that are overwritten at each iteration). If instead we wanted to reconstruct the sequence of optimal choices David made, we would need to store a full array (which is O(n)O(n) space).

Problem 3. [Increasing Subsequence] Given an array of nn integers a1,,ana_1, \dots, a_n, determine the longest increasing subsequency in the array, i.e. the longest subsequence where every element is strictly greater than the previous one.

Recall that a subsequence is a sequence that can be derived from an array by deleting some elements without changing the order of the remaining elements.

Output a polynomial time algorithm that finds longest increasing subsequence of an array. If multiple answers exist, output any one of them.

Example: 7 3 5 3 6 2 9 8

The longest increasing subsequence of this array is 3 5 6 9 (other subsequences of length 4 exist).

Solution

For our subproblem, we let OPT[i]OPT[i] be the length of longest increasing subsequence ending with element aia_i.

For our recurrence, we note that any increasing subsequence ending at aia_i is formed by appending aia_i to some increasing subsequence ending at an earlier element aja_j with j<ij < i and aj<aia_j < a_i (the dummy element a0a_0 covers the case where aia_i starts a fresh subsequence). We pick the predecessor jj maximizing OPT[j]OPT[j]:

OPT[i]=max0ji1,  aj<ai(OPT[j])+1OPT[i] = \max_{0 \le j \le i - 1,\; a_j < a_i} \left( OPT[j] \right) + 1

We let our base case be OPT[0]=0OPT[0] = 0 with dummy element a0=min(a1,,an)1a_0 = \min(a_1, \dots, a_n) - 1 (this ensures that we can create a new increasing subsequence of length 11 with each element aia_i).

OPT[i]OPT[i] is the length of the best increasing subsequence ending at aia_i, so the length of the overall longest increasing subsequence is

max1inOPT[i].\max_{1 \le i \le n} OPT[i].

To output an actual subsequence and not just its length (reconstruct the sequence), store the maximizing predecessor for each ii:

prev[i]=arg max0ji1,  aj<aiOPT[j].\mathrm{prev}[i] = \operatorname*{arg\,max}_{0 \le j \le i-1,\; a_j < a_i} OPT[j].

Let i=arg maxiOPT[i]i^\star = \operatorname*{arg\,max}_{i} OPT[i]. Follow the chain i, prev[i], prev[prev[i]], i^\star,\ \mathrm{prev}[i^\star],\ \mathrm{prev}[\mathrm{prev}[i^\star]],\ \dots until reaching the dummy index 00, collect the real elements, and reverse them.

Running time. We fill OPT[1],,OPT[n]OPT[1], \dots, OPT[n] in increasing order of ii, and each OPT[i]OPT[i] scans all j<ij < i, giving i=1nO(i)=O(n2)\sum_{i=1}^{n} O(i) = O(n^2) total, which is polynomial. Reconstruction adds O(n)O(n).

Faster algorithm

The O(n2)O(n^2) DP already satisfies the polynomial-time requirement, but there is a standard O(nlogn)O(n \log n) improvement. Maintain an array tt where t[]t[\ell] is the smallest possible tail value among all increasing subsequences of length \ell found so far. For each aia_i, binary search for the first entry ai\ge a_i (strict increase) and overwrite it, or append aia_i if no such entry exists. The array tt stays sorted throughout, and its final length is the LIS length; predecessor pointers recover the subsequence as before.

Problem 4. [Coin Game] Caleb and David play a game with a row of nn coins with values a1,,ana_1, \dots, a_n. Players alternate turns, with Caleb going first. On a turn, the current player removes either the leftmost or the rightmost remaining coin and adds its value to their score. Play continues until no coins remain.

Assuming both players play optimally (each maximizes their own final score), give an algorithm that outputs Caleb's final score.

Example: 4 9 1 3

Caleb takes the 3, then David is forced to choose from 4 9 1. Regardless of what David chooses, Caleb gets the 9 on his next turn. Thus, Caleb's optimal score is 12.

Hint

The set of remaining coins is always a contiguous block, so can we index our subproblem by intervals?

Solution

Coming soon!