Now that we're familiar with Recursion (L1) and Python (L2), we'll look at a very important problem solving technique, DP. Just like Recursion = {Fairy, Idea}, DP = {Recursion, Memory}.
DP is NOT breaking problems into smaller subproblems and solving them. That's Recursion. DP is using extra memory to speed up recursion. This distinction clear, today we'll do loads of examples in DP.
Pointers:
E1. Fibonacci.
E2. Binomial Coefficients.
E3. S is a list of integers, find the longest increasing subsequence of S.
E4. Given a set of integers S = {s1..sn}, find (if exists) a subset that adds exactly to T (given).
E5. Take X,Y,Z as strings (where len(Z) = len(X) + len(Y)) and tell whether Z is a shuffle of X+Y.
HOMEWORK:
E6. Cut a rod (length n) into integer parts to maximize the total cost. Cost of parts is P = {P1..Pn}.
E7. T = {T1..Tn} are nos on a circle (some may be negative). Find the maximum possible sum of any arc.
# S1
def fibonacci(n):
mem = []
mem.append(0) # f(0) = 0
mem.append(1) # f(1) = 1
for i in range(n):
mem.append(mem[-1] + mem[-2])
return mem[-1]
print(fibonacci(42))
# notice the time speedup!
# S2 (with Pizzeria proof) (and why they're called BCs)
def bin_coff(n, r):
C = [[0 for _ in range(r+1)] for _ in range(n+1)]
for i in range(n+1):
for j in range(min(i, r) + 1):
if j == 0 or j == i: # base case
C[i][j] = 1
else: # recursion
C[i][j] = C[i-1][j-1] + C[i-1][j]
return C[n][r]
print(bin_coff(5,2))
# S3
def longest_increasing_subsequence(S):
L = [1 for _ in range(len(S))] # length of LIS ending at index
P = [-1 for _ in range(len(S))] # index of previous number in LIS
for i in range(1, len(S)):
for j in range(i):
if (S[i] > S[j]) and (L[j]+1 > L[i]):
L[i] = L[j]+1
P[i] = j
def reconstruct(S, L, P):
LIS = []
t = max(range(len(L)), key = lambda z: L[z])
while P[t] != -1:
LIS.append(S[t])
t = P[t]
else:
LIS.append(S[t])
return LIS[::-1]
return reconstruct(S,L,P)
print(longest_increasing_subsequence([2,4,3,5,1,7,6,9,8]))
# S4 (Read up more about the Knapsack problem)
def knapsack(S, T):
n = len(S)
pr = [ [False for i in range(T+1)] for j in range(n+1)] # partial results
for i in range(n+1):
pr[i][0] = True # base case
for i in range(1,n+1):
for j in range(1,T+1):
pr[i][j] = pr[i-1][j] if j<S[i-1] else (pr[i-1][j] or pr[i-1][j-S[i-1]]) # recursive step
return pr[n][T]
print(knapsack([1,2,5,9,10], 23))
print(knapsack([1,2,5,9,10], 22))
# S5
def is_shuffle(X, Y, Z):
n = len(X)
m = len(Y)
assert(len(Z) == n+m) # precondition (more in L22)
ps = [[False for i in range(m+1)] for j in range(n+1)]
for i in range(n+1):
for j in range(m+1):
if i == 0:
ps[i][j] = (Z[:j] == Y[:j]) # base cases
elif j==0:
ps[i][j] = (Z[:i] == X[:i])
else:
ps[i][j] = ((ps[i-1][j] and Z[i+j-1]==X[i-1])) or ((ps[i][j-1] and Z[i+j-1]==Y[j-1]))
return ps[n][m]
print(is_shuffle('chocolate', 'chips', 'cchocohilaptes'))
print(is_shuffle('chocolate', 'chips', 'chocochilatspe'))
# S6
## HOMEWORK. Fill here and submit.
# S7
## HOMEWORK. Fill here and submit.
This lecture might have been a teensy bit hard. If that's the case, practice more, it'll get much easier with time.
On the other hand, if you're comfortable with this lecture, go on to read this.