There’s two variants of dymanic programming algorithms: top down, with recurrence and memorization, and bottom up, with iteration. In either case, we solve small subproblems and store them. This causes a space-time trade-off. The difference between Divide and Conquer algorithms and Dynamic Programming algorithms is that the subproblems in DP overlap, so it’s inefficient to recompute all of them.

Fibonacci

The recursive algorithm below takes exponential time with respect to n. It can be made faster by storing intermediate results.

def fib(n):
	if n <= 1 return 1
	return fib(n-2) + fib(n-1)

Consider using an array and using the former two elements to compute the current element.

def fib2(n):
	if n = 0 return 0
 
	dp = arr[0; n+1]
	dp[0] = 0
	dp[1] = 1
 
	for i in 2..(n+1):
		dp[i] = dp[i-1] + dp[i-2]
 
	return dp[n]

Essentially every DP solution involves:

  • memory structure
  • base case
  • filling memory structure using recurrence
    • in this example

Staircase problem

Suppose you have stair steps. You can leap one, two, or three steps at a time. What is the number of combinations that you could reach step ?

Solution

  • Create an array where represents the number of ways to reach step .
  • Base cases are: and .
  • To reach step , there was some last step.
    • It was either 1, 2, or 3 steps away.
    • Hence, the number of ways to go from 0 to is the sum of the number of ways to go from 0 to , 0 to , and 0 to .
    • This gives us a recurrence of

Pseudocode

def numways(n):
	dp = [0; n+1]
	dp[0] = 0
	dp[1] = 1
	dp[2] = 2
 
	for i in 3..(n+1):
		dp[i] = dp[i-1] + dp[i-2] + dp[i-3]
 
	return dp[n]

Add/multiply problem

Problem

Suppose you had two operations: add one, and multiply by two. How many operations does it take to get from 0 to some ?

Solution

  • Base cases:
    • because for 0, you need 0 operations.
    • because for 1, you add 1 to 0.
  • Now, we’ll multiply by 2 as much as possible, then add 1 if the element is odd and multiply by 2 if it is even. This produces this recurrence:

Pseudocode

def minop(k):
	dp = [0; k+1]
	dp[0] = 0
	dp[1] = 1
 
	for i in 2..(k+1):
		dp[i] = dp[i-1] + 1
		if i is even:
			dp[i] = min(dp[i], dp[i/2] + 1)
 
	return dp[k]

The house robber problem

House robber. Given an array of houses with values , you want to rob them, but you can’t rob two adjacent houses or alarms will go off. What is the maximum amount you can steal?

Solution

  • Define array with = max we can steal from houses . The base cases are that .
  • At the next house visited, we can choose to rob or not rob. We take the maximum of both scenarios. If we rob the house, then we could not have robbed the previous house. If we didn’t rob the house, then we could have robbed the previous house. This gives us the recurrence:

Pseudocode

def houserobber(H):
	dp = [0; n]
	dp[0] = H[0]
	dp[1] = max(H[0], H[1])
 
	for i in 2..n:
		dp[i] = max(dp[i-2] + H[i], dp[i-1])
 
	return dp[n-1]

Counting paths problem

Given , what is the number of paths through an grid from (1,1) to () if you can only go down and right?

Solution

  • Define array where is the number of paths from (1,1) to ().
  • Base cases are that . These are like the first row and column of the matrix.
  • Now the recurrence:
    • For non-base case cells, the number of ways to reach () is the sum of the number of ways to reach the cell above it and the cell to its left. This is because you can only come to () via or .
    • Hence, the relation is: .
    • will give the number of paths from (1,1) to .

Pseudocode

def count_paths(n, m):
	dp = [0; n+1][0; m+1]
 
	for i in 0..n:
		dp[i][0] = 1
	for j in 0..m:
		dp[0][j] = 1
 
	for i in 1..n:
		for j in 1..m:
			dp[i][j] = dp[i-1][j] + dp[i][j-1]
 
	return dp[n][m]

Counting paths, but now with bombs

Find the number of paths to reach cell (n, m). Additionally, the grid has bombs, denoted by . Find the number of paths from (0,0) to () such that no bombs are traversed.

Solution

  • Define array where is the number of paths from to .
  • Base cases:
  • This means that if a cell has a bomb, no paths can go through it. Otherwise, the number of paths to a cell is the sum of the paths to the cell above it and to its left. The solution will be .

Pseudocode

def count_with_bombs(n, m, bombs):
	dp = [0; n+1][0; m+1]
 
	if bombs[0][0]: return 0
 
	dp[0][0] = 1
 
	for i in 1..n:
		if not bombs[i][0]:
			dp[i][0] = dp[i-1][0]
 
	for j in 1..m:
		if not bombs[0][j]:
			dp[0][j] = dp[0][j-1]
 
	for i in 1..n:
		for j in 1..m:
			if not bombs[i][j]:
				dp[i][j] = dp[i-1][j] + dp[i][j-1]

The time complexity here is because we’re visiting each cell of the grid exactly once.