Dynamic Programming

Learning Goals

  1. Basic dynamic programming
  2. Memoization

Dynamic Programming is a method for
solving a problem by

  1. breaking it down into simpler subproblems,
  2. solving each subproblem just once,
  3. and storing their solutions using a memory-based data structure (array, map,etc).

Fibonnaci

Recursive Solution

The naive approach

def fib(n):
    if (n <= 0):
        return 0
    if (n == 1):
        return 1
    return fib(n-1) + fib(n-2)
def fib_with_count(n):
    count = [0]*(n+1)

    def fib(n):
        count[n] = count[n] + 1
        if (n <= 0):
            return 0
        if (n == 1):
            return 1
        return fib(n-1) + fib(n-2)
    
    res = fib(n)
    print("Each fib(n) was called ", 
        count, " times")
    print("Total fib calls was ", 
        sum(count))
    return res

Recursive Solution

With memoization

memo = { 0: 0, 1: 1 }
def fib_memo(n):
    if (n in memo):
        return memo[n]
    res = fib_memo(n-1) + fib_memo(n-2)
    memo[n] = res
    return res
def fib(n):
    if (n <= 0):
        return 0
    if (n == 1):
        return 1
    return fib(n-1) + fib(n-2)

Normal

Using memoization

Issues with recursive memoization

  • Stack overflow
  • Linear memory usage

..fib(n) only needs to know fib(n-1) and fib(n-2)

Dynamic Programming is a method for solving a problem by breaking it down into simpler subproblems,
solving each of those subproblems just once,
and storing their solutions using a memory-based data structure (array, map,etc).

Recursive fibonacci with memoization is dynamic programming, as it breaks down the problem into subproblems and solves each of them only once.

The Knights Dialer

Based on an excellent article at

https://hackernoon.com/google-interview-questions-deconstructed-the-knights-dialer-f780d516f029

How many disctinct numbers can be typed, starting at number 'i' after 'n' jumps?

Possible jumps

NEIGHBORS_MAP = {
    1: (6, 8),
    2: (7, 9),
    3: (4, 8),
    4: (3, 9, 0),
    5: tuple(),  # 5 has no neighbors
    6: (1, 7, 0),
    7: (2, 6),
    8: (1, 3),
    9: (2, 4),
    0: (4, 6),
}

4 jumps starting at 6

1

3

6

15

Naive recursion would be exponential time

Dynamic Programming is a method for
solving a problem by

  1. breaking it down into simpler subproblems,
  2. solving each subproblem just once,
  3. and storing their solutions using a memory-based data structure (array, map,etc).

solving each subproblem just once,

neighors = { ... } # map of neighbors                               
                                                                
def count_sequences(start_position, num_hops):                  
    if num_hops == 0:                                           
        return 1                                                
                                                                
    num_sequences = 0                                           
    for position in neighbors(start_position):                  
        num_sequences += count_sequences(position, num_hops - 1)
    return num_sequences                                        
                                                                
if __name__ == '__main__':                                      
    print(count_sequences(6, 4))    

Runtime with memoization

  • Subproblemer are solved once
  • We do n jumps
    ​each having x subproblems
  • Runtime 9*n, linear!

Are we done yet?

No.


def count_sequences(start_position, num_hops):                
    prior_case = [1] * 10                                     
    current_case = [0] * 10                                   
    current_num_hops = 1                                      
                                                              
    while current_num_hops <= num_hops:                       
        current_case = [0] * 10                               
        current_num_hops += 1                                 
                                                              
        for position in range(0, 10):                         
            for neighbor in neighbors(position):              
                current_case[position] += prior_case[neighbor]
        prior_case = current_case                             
                                                              
    return current_case[start_position]  

Still linear memory usage

Can we go faster?

Yes, but its very hard

Examples

Comparing data

seq_data = get_data_sequentially(..)
threaded_data = get_data_threaded(..)

# Data must have same length
self.assertTrue(len(seq_data) == len(threaded_data))

# Each data point must match,
#   but data is in random order, 
#   and sorting it is too slow.
# TODO Can we compare the data in linear time?

A project I'm working on has to fetch a lot of data.

Fetching the data took 1127 seconds,
which is about 19 minutes.

To speed it up we added threads to do a lot of it in paralell - how do you quickly test if this is correct?

seen_data = {} # memoization: O(1) insert and lookup

# Memo the seq data
for data in seq_data:
    # False until seen by both lists
    seen_data[data.id] = False

# Verify threaded data was also found seq.
for data in threaded_data:
    if not data.id in seen_data:
        assertTrue(False, 
            "Data not found {}".format(data.id))
    else:
        seen_data[data.id] = True

# Verify threading missed no data
for (k,v) in seen_data:
    assertTrue(v, 
      "{} not found".format(k))

Other relevant examples

  • Calculating map clustering
  • Searches, quick string comparisons

Time
For
Practice

https://open.kattis.com/contests/ptasxp

Dynamic Programming

By Patrick Lid Monslaup

Dynamic Programming

What is dynamic programming, and how can it be leveraged to increase code quality and speed?

  • 427