cf.completefrontend
DYNAMIC PROGRAMMING · CORE

Linear DP

Walk along a list once, and at each position work out the best answer so far from the answers at the few positions before it.

Time O(n)Space O(n)17 practice problemsStep-through picture

The idea

Linear DP works on a sequence: an array, a string, or a row of days. The state is one position in the sequence. One cell, best[i], answers the question for the first i + 1 items, or for a sequence that ends at item i.

At each position you make a small decision, usually "take this item or skip it" or "start fresh here or continue". The answer for position i comes from one or two earlier positions, so a single left-to-right pass is enough.

The template below solves a classic version. You have a row of values and want the largest total, but you may never pick two neighbors.

An everyday example

Picking stalls at a market without neighbors

You can visit stalls along a street, and each stall gives you some coins, but the owners don't allow you to visit two stalls side by side. Walking down the street, at each stall you ask: is it better to skip this stall and keep my best total so far, or take its coins plus my best total from two stalls back? You never need to remember more than that.

How the example maps to the code
In the exampleIn the code
The row of stallsThe values array
The best total after looking at stall ibest[i]
Skipping the stallskip = best[i - 1]
Taking the stall, which rules out its left neighbortake = values[i] + best[i - 2]
Choosing the better planbest[i] = Math.max(skip, take)

From the slow way to linear dp

  1. Start with brute force: try every way to pick items with no two neighbors, add up each choice, and keep the largest. The number of such choices grows exponentially, by about 1.6 times per extra item: a row of 30 items has 2,178,309 valid choices, and a row of 40 has 267,914,296. Each also needs a sum, so this is far too slow.
  2. Simple rules don't work either. "Take every other item" picks a fixed pattern and ignores the values. "Take the biggest item, cross out its neighbors, repeat" can give up two medium items for one big one. The only safe way is to compare real options, but without listing every choice.
  3. Walk along the row and ask about one item at a time. For item i, there are only two options: skip it, or take it. If you skip it, the best you can have is whatever was best up to item i - 1. If you take it, item i - 1 is off limits, so you add values[i] to the best up to item i - 2. Nothing else about the earlier choices matters.
  4. So store one number per position: best[i], the largest total using items 0 to i. Each cell is Math.max(skip, take) from two cells you already have. One pass from left to right fills it, and the last cell is the answer. That loop is linear DP.

How it works

  1. Define best[i]: the largest total you can get from values[0] to values[i] without picking two neighbors.
  2. Base case: with one item, best[0] is values[0], or 0 if that value is negative and you would rather pick nothing.
  3. For each later i, compute skip = best[i - 1]: leave item i alone and keep the best from before.
  4. Compute take = values[i] + best[i - 2]: pick item i, which rules out item i - 1, so add the best up to i - 2 (or 0 if there is none).
  5. Set best[i] = Math.max(skip, take).
  6. The answer is best[n - 1], the best total over the whole row.

Watch it run

This picture runs the code below on a real input. Press Step to move one step at a time, or Play to watch. The highlighted line is the one that just ran. Change the input to see other cases.

Loading the picture…
See every step at onceThe same run, laid out side by side
Drawing every step…

The code

Point at a note to light up the lines it explains.

  1. function bestNonAdjacent(values) {
  2. const n = values.length;
  3. if (n === 0) return 0;
  4. const best = new Array(n).fill(0);
  5. best[0] = Math.max(0, values[0]);
  6. for (let i = 1; i < n; i++) {
  7. const skip = best[i - 1];
  8. const take = values[i] + (i >= 2 ? best[i - 2] : 0);
  9. best[i] = Math.max(skip, take);
  10. }
  11. return best[n - 1];
  12. }

How fast it is

TimeO(n)
Extra memoryO(n)

Each position is visited once with two additions and a comparison. The table has one cell per position, and it can be cut to two variables because only the last two cells are ever read.

  • Best caseO(1)

    An empty row: line 3 returns 0 before any table is made.

  • Average caseO(n)

    Any row of n items: one pass, two additions and a comparison per item.

  • Worst caseO(n)

    The same as the average. The values never change how many cells are read.

Common mistakes

  • Using a greedy rule like "pick every other item" or "pick the biggest first". For [2, 1, 1, 2] the best is 2 + 2 = 4, which neither rule finds.
  • Reading best[i - 2] when i is 1, which is best[-1] and gives undefined. Handle the start of the row on its own.
  • Mixing up "best among the first i items" with "best ending exactly at item i". Write the meaning down; the transition and the final answer depend on it.
  • Returning best[n] when the table has only n cells. Match the final index to how you sized the table.

When to use it

Look for these clues in a problem:

  • The input is one array or string and the answer is a best total, a count, or a yes or no.
  • Each item is taken or skipped, with a rule about neighbors, such as "no two in a row".
  • The answer for a prefix (the first i items) can be built from the answers for slightly shorter prefixes.
  • A greedy choice (always take the biggest item) fails on some small example.

Check yourself

Answer each one in your head first, then open it.

Why is best[0] set to Math.max(0, values[0]) instead of values[0]?

Because picking nothing is allowed. If the first value is negative, the best total using only it is to leave it, which is 0.

Why does take read best[i - 2] and not best[i - 1]?

Taking item i rules out its neighbor i - 1. best[i - 1] might include that neighbor, while best[i - 2] only uses items that are safe to combine with i.

Can best[i] ever be smaller than best[i - 1]?

No. skip equals best[i - 1], and best[i] is the larger of skip and take, so it is at least best[i - 1].

In the trace, why does the last item, 1, get skipped?

Taking it gives 1 + best[3] = 12, because it can't sit next to the 10. Skipping keeps 17, which is larger.

Practice problems

Stuck on one? Tuto is in the playground too, and gives you hints, never the answer.

Go further

You don't need these to start practicing. Open one when you want more.

Why it works

The reasoning that makes it always right

The table keeps one promise: best[i] is the largest total you can get from values[0] to values[i] with no two neighbors picked. For i = 0, that is either the one value or nothing, whichever is bigger, which is exactly line 5.

Each new cell keeps the promise because the best choice up to i falls into one of two cases. If it doesn't use item i, it is a valid choice from items 0 to i - 1, so it can be no better than best[i - 1], and best[i - 1] itself is reachable. If it does use item i, it can't use i - 1, so the rest is a valid choice from items 0 to i - 2, worth at most best[i - 2]. The larger of the two cases is the answer, and the code computes both.

Filling from left to right means best[i - 1] and best[i - 2] are always ready. The loop visits each index once and stops after n - 1, so the promise says best[n - 1] is the best for the whole row. Notice that best never goes down, since skip is always one of the options.

Going deeper

3 more ideas

Why looking back two places is enough

Any good selection either uses the last item or does not. If it doesn't, it is a good selection of the items before it, which is best[i - 1]. If it does, the item before it is not picked, so the rest is a good selection of items up to i - 2, which is best[i - 2]. Those two cases cover every possibility.

How much work it does

One pass with a constant amount of work per item: O(n) time. The table uses O(n) space, but each cell only reads the two before it, so two variables (prev and prevPrev) are enough for O(1) space.

Other shapes of linear DP

"Ends exactly at i" states are common too. For the largest sum of a contiguous run, best[i] is the largest sum of a run that ends at i: either values[i] alone or values[i] + best[i - 1]. Some problems look further back, such as best[i - 7] for a weekly pass, but the idea is the same.

Work it by hand

Every step in a table, with why

Pick values from a row of 6, never two neighbors, for the largest total. The first value is negative.

values = [-2, 7, 3, 4, 10, 1]

Stepivalues[i]skiptakebest[i]What happensWhy
10-2--0Base: pick nothingMath.max(0, -2) is 0. Taking a negative value only lowers the total, so the best with one item is to pick none.
21707 + 0 = 77Take winsThere is no best[-1], so take adds 0 for the earlier part (line 8).
32373 + 0 = 37Skip winsTaking 3 means giving up the 7 next to it. Keeping 7 is better.
43474 + 7 = 1111Take wins4 is not next to 7, so it can join the best from up to index 1.
54101110 + 7 = 1717Take winsTaking 10 rules out the 4, so it adds best[2], which is 7 (the 7 at index 1).
651171 + 11 = 1217Skip wins; loop endsTaking 1 would mean dropping the 10. The last cell is filled, and line 11 returns it.

It returns 17, from picking 7 and 10. "Every other item" would give -2 + 3 + 10 = 11 or 7 + 4 + 1 = 12, both worse. The loop ends after the last index.

Compared with other ways

When to pick something else
ApproachTimeExtra memoryChoose it when
Linear DPO(n)O(n)The clear first version: the table makes each step easy to check and to walk back through.
Two rolling variablesO(n)O(1)When you only need the total, not the list of picked items.
Try every valid choiceO(2^n)O(n)Only to check the DP on rows of 15 items or fewer.
Greedy, biggest value firstO(n log n)O(n)Never for this problem. It can give up two medium values for one big one, which is wrong when the two together are worth more.

Extra memory: O(n) for the best array. Only the last two cells are ever read, so two variables, often named prev and prevPrev, give O(1) space.

How the steps grow

1101001k10k100k1M116324864input size n
Steps as the input grows, for Linear DP and the other approaches, on a scale where each line up is 10 times more steps.
  • Linear DP, Two rolling variables O(n)
  • Try every valid choice O(2^n)
  • Greedy, biggest value first O(n log n)
About how many steps for bigger inputs
Approachn = 10n = 1,000n = 1,000,000
Linear DP101,0001 million
Two rolling variables101,0001 million
Try every valid choice1,024more than a quadrillionmore than a quadrillion
Greedy, biggest value first339,96620 million

Inputs that need care

5 tricky inputs
An empty row: values = []
Line 3 returns 0. No items means no total.
One item: values = [9]
The loop never runs, and it returns best[0], which is 9.
Only negative values: values = [-4, -1, -6]
Every cell stays 0, so it returns 0: picking nothing is allowed. If the problem says you must pick at least one item, the answer should be the largest value, -1, and the base case must change.
The best choice uses both ends: values = [5, -1, -1, 5]
best becomes [5, 5, 5, 10], so it returns 10. Skipping two items in a row is allowed, and the table handles it with no special rule.
Equal options, where skip and take tie
Math.max returns the same number either way, so the total is right. If you also rebuild which items were picked, decide which way ties go and stick to it.

Variations and follow-ups

Twists interviewers add

A circular row

If the first and last items are neighbors, they can't both be picked. Run the same loop twice: once on items 0 to n - 2, and once on items 1 to n - 1. The larger of the two answers is right, because any valid choice leaves out the first item or the last.

Best run that ends at i

For the largest sum of a contiguous run, the state changes to "the best run that ends exactly at i": either values[i] alone, or values[i] added to the best run ending at i - 1. The answer is the largest cell, not the last one. This is Kadane's algorithm.

A wider gap

If picked items must be at least k apart, take reads best[i - k] instead of best[i - 2]. The table and the loop stay the same; only how far back you look changes.

Listing the picked items

Walk back from the end: if best[i] equals best[i - 1], item i was skipped, so move to i - 1. Otherwise it was taken, so record it and jump to i - 2. Skipped negative first items need the same check at index 0.

In an interview

Spot it, explain it, answer follow-ups
  • Spot it when the input is one sequence, you decide something at each position, and the rule only links an item to its near neighbors: "no two adjacent", "at most one per day", "must rest after".
  • Say the state in one sentence and say whether it means "best among the first i + 1 items" or "best ending exactly at i". The transition and the place you read the answer both depend on that choice.
  • Test four tiny rows by hand: empty, one item, two items where the second is bigger, and a row whose answer skips two in a row, like [5, -1, -1, 5].
  • Expect two follow-ups: "do it in O(1) space" (keep the last two cells in variables) and "what if the row is a circle?" (run it twice, without the first item and without the last).

Where frontend code uses it

The same idea in real UI code
  • Choosing which posts to feature in a feed when two featured posts may not sit next to each other is this exact take-or-skip DP.
  • Analytics dashboards that show "best streak" or "largest gain over a period" compute it in one pass with a linear DP like Kadane's algorithm.
  • Planning which days to buy a 1-day, 7-day, or 30-day plan for a paid API is a linear DP over the days of the month.

Related algorithms