1D Dynamic Programming

Solve problems along a line, one position at a time: best total with a rule about neighbours, and counting ways. Five steps, one table, then O(1) space.

16 minLesson 2 of 6
On this page
  1. The five steps
  2. Fill the table by hand
  3. The code, then less memory
  4. The same shape, counting instead
  5. Common mistakes
  6. Check yourself
  7. What to practise next

A café offers you shifts on five days in a row. Each day pays a different amount: 5, 9, 6, 1, 4. There is one rule: after a shift you need a day off, so you can never work two days in a row. Which days should you take to earn the most?

Grabbing the best-paid day first gets you the 9, which rules out the 5 and the 6. Add the 4 at the end and you have 13. But 5 + 6 + 4 = 15. Once again a choice that looked good locally blocked better choices next to it. You need to weigh every day against its neighbours, and a one-row table does exactly that.

The five steps

Use the recipe from Introduction to Dynamic Programming. The question to settle first is what one cell means.

  1. State. best[i] is the most you can earn using only days 0 to i. Ignore the future; just answer for this stretch of days.
  2. Recurrence. On day i you either rest or work. If you rest, day i adds nothing and you keep best[i − 1]. If you work, you earn pay[i], but day i − 1 must be free, so you add best[i − 2]. Take the larger: best[i] = max(best[i − 1], pay[i] + best[i − 2]).
  3. Base cases. best[0] = pay[0], and best[1] = max(pay[0], pay[1]), because you can take only one of the first two days.
  4. Order. Each cell reads the two cells to its left, so fill left to right.
  5. Answer. best[n − 1], the best over all the days.

The recurrence works because the two options cover every possible plan. Any plan either uses day i or it does not. If it does not, it is just a plan for the earlier days, and the best of those is best[i − 1]. If it does, the rest of it is a plan for days 0 to i − 2, and the best of those is best[i − 2].

best[0] best[1] best[2] best[3] best[4] work: pay[3] + best[1] rest: best[2]
Each cell looks back at exactly two earlier cells: one for resting today, one for working today.

Fill the table by hand

Best pay for days 5, 9, 6, 1, 4Use the arrows or ← → keys
best[0] = pay[0]best[1] = max(pay[0], pay[1])for i from 2 to n - 1:  rest = best[i - 1]  work = pay[i] + best[i - 2]  best[i] = max(rest, work)return best[n - 1]
  1. pay
    05i
    19
    26
    31
    44
    best
    05i
    1
    2
    3
    4

    1Base case. With only day 0 on offer, take it: best[0] = 5.

  2. pay
    05
    19i
    26
    31
    44
    best
    05
    19i
    2
    3
    4

    2Base case. Days 0 and 1 are next to each other, so you can work only one of them. Pick the better: best[1] = max(5, 9) = 9.

  3. pay
    05
    19
    26i
    31
    44
    best
    05
    19
    211i
    3
    4
    rest
    9
    work
    11

    3Day 2 pays 6. Rest, and keep best[1] = 9. Or work, earning 6 + best[0] = 6 + 5 = 11, since day 1 must then be free. Working wins: best[2] = 11.

  4. pay
    05
    19
    26
    31i
    44
    best
    05
    19
    211
    311i
    4
    rest
    11
    work
    10

    4Day 3 pays 1. Rest, and keep best[2] = 11. Or work, earning 1 + best[1] = 1 + 9 = 10, since day 2 must then be free. Resting wins: best[3] = 11. A 1 is not worth giving up day 2.

  5. pay
    05
    19
    26
    31
    44i
    best
    05
    19
    211
    311
    415i
    rest
    11
    work
    15

    5Day 4 pays 4. Rest, and keep best[3] = 11. Or work, earning 4 + best[2] = 4 + 11 = 15, since day 3 must then be free. Working wins: best[4] = 15.

  6. pay
    05
    19
    26
    31
    44
    best
    05
    19
    211
    311
    415
    answer
    15

    6The answer is best[4] = 15: work days 0, 2 and 4 for 5 + 6 + 4. Grabbing the biggest day first (the 9) would have earned only 13. Five days, five cells, at most one comparison each.

1 / 6

The table gives the best total, not the days. To find the days, walk back from the last cell. If a cell equals the one to its left, that day was a rest day: step one cell left. If it is bigger, that day was worked: write it down and jump two cells left. best[4] = 15 is bigger than best[3] = 11, so day 4 is worked and you jump to best[2]. 11 is bigger than best[1] = 9, so day 2 is worked and you jump to best[0], which is day 0. Days 0, 2 and 4.

The code, then less memory

One row of cells
function maxPay(pay) {
  const n = pay.length;
  if (n === 0) return 0;
  if (n === 1) return pay[0];
  const best = new Array(n);
  best[0] = pay[0];
  best[1] = Math.max(pay[0], pay[1]);
  for (let i = 2; i < n; i++) {
    const rest = best[i - 1];
    const work = pay[i] + best[i - 2];
    best[i] = Math.max(rest, work);
  }
  return best[n - 1];
}
 
maxPay([5, 9, 6, 1, 4]); // 15
def max_pay(pay):
    n = len(pay)
    if n == 0:
        return 0
    if n == 1:
        return pay[0]
    best = [0] * n
    best[0] = pay[0]
    best[1] = max(pay[0], pay[1])
    for i in range(2, n):
        rest = best[i - 1]
        work = pay[i] + best[i - 2]
        best[i] = max(rest, work)
    return best[n - 1]
 
max_pay([5, 9, 6, 1, 4])  # 15

Look at what each step reads: only the two most recent cells. Everything further left is never touched again. So you can keep two variables instead of a whole array. Starting both at 0 also removes the special cases, because "the best before any days" is 0.

Two variables instead of a table
function maxPayLowMemory(pay) {
  let twoBack = 0; // best up to day i - 2
  let oneBack = 0; // best up to day i - 1
  for (const p of pay) {
    const current = Math.max(oneBack, p + twoBack);
    twoBack = oneBack; // slide the window right by one day
    oneBack = current;
  }
  return oneBack;
}
def max_pay_low_memory(pay):
    two_back = 0  # best up to day i - 2
    one_back = 0  # best up to day i - 1
    for p in pay:
        current = max(one_back, p + two_back)
        two_back = one_back  # slide the window right by one day
        one_back = current
    return one_back
ApproachTimeSpaceWhy
TableO(n)O(n)n cells, one comparison each.
Two variablesO(n)O(1)Same work. Only the last two cells are ever read, so only they are kept.

Write the table version first. Shrink it only once it works, and only when you can name which cells the recurrence reads. You also lose the ability to walk back and list the chosen days, since the old cells are gone.

The same shape, counting instead

The recurrence does not have to take a max. Suppose you climb a staircase of 6 steps and each stride covers 1, 2 or 3 steps. How many different stride sequences get you to the top?

State: ways[i] is the number of ways to stand on step i. Recurrence: your last stride came from step i − 1, i − 2 or i − 3, and those groups never overlap, so add them: ways[i] = ways[i − 1] + ways[i − 2] + ways[i − 3], skipping any index below 0. Base case: ways[0] = 1. Order: upward. Answer: ways[6].

step i0123456
ways[i]112471324
Counting stride sequences
function countClimbs(steps) {
  const ways = new Array(steps + 1).fill(0);
  ways[0] = 1; // standing at the bottom: one way, do nothing
  for (let i = 1; i <= steps; i++) {
    for (const stride of [1, 2, 3]) {
      if (i - stride >= 0) ways[i] += ways[i - stride];
    }
  }
  return ways[steps];
}
 
countClimbs(6); // 24
def count_climbs(steps):
    ways = [0] * (steps + 1)
    ways[0] = 1  # standing at the bottom: one way, do nothing
    for i in range(1, steps + 1):
        for stride in (1, 2, 3):
            if i - stride >= 0:
                ways[i] += ways[i - stride]
    return ways[steps]
 
count_climbs(6)  # 24

Common mistakes

Check yourself

Quick check

Shifts pay [2, 7, 9, 3, 1], no two days in a row. What is best[4]?

Strides can be 1 or 2 steps. How many ways are there to climb 4 steps?

Why can the shift problem keep just two variables instead of a whole array?

What to practise next

Practise both shapes: Sunny Orchard Row uses the no-two-in-a-row rule, and Stepping Stones Across the Pond counts ways. Then DP on Grids moves the same recipe into two dimensions, where each cell looks up and to the left.