- Published on
Dynamic Programming
- Authors
- Name
- Loi Tran
- @PrimeTimeTrann
is a technique in computer programming that helps to efficiently solve a class of problems that have overlapping sub problems and optimal substructure property.
Fibonacci
Write a function fib(n)
that takes in a number as an argument.
The function should return the n-th number of the fibonacci sequence.
const fib = (n) => {
if (n <= 2) return 1
return fib(n - 1) + fib(n - 2)
}
// t = O(2^n)
// s = O(n)
console.log(fib(6)) // 8
console.log(fib(7)) // 13
console.log(fib(8)) // 21
console.log(fib(50)) // 12586269025
// Memoized
const fib = (n, memo = {}) => {
if (n in memo) return memo[n]
if (n <= 2) return 1
memo[n] = fib(n - 1, memo) + fib(n - 2, memo)
return memo[n]
}
// t = O(n)
// s = O(n)
console.log(fib(6)) // 8
console.log(fib(7)) // 13
console.log(fib(8)) // 21
console.log(fib(50)) // 12586369025
Grid Traveler
Write a function gridTraveler(m, n)
that returns the number of ways to travel from top left to bottom right corner of grid.
const gridTraveler = (m, n) => {
if (m === 1 && n === 1) return 1
if (m === 0 || n === 0) return 0
return gridTraveler(m - 1, n) + gridTraveler(m, n - 1)
}
// t = O(2^m+n)
// s = O(n + m)
console.log(gridTraveler(1, 1)) // 1
console.log(gridTraveler(2, 3)) // 3
console.log(gridTraveler(3, 2)) // 3
console.log(gridTraveler(3, 3)) // 6
console.log(gridTraveler(8, 8)) // 3432
console.log(gridTraveler(18, 18)) // 2333606220
// Memoized
const gridTraveler = (m, n, memo = {}) => {
const key = m + ',' + n
if (key in memo) return memo[key]
if (m === 1 && n === 1) return 1
if (m === 0 || n === 0) return 0
memo[key] = gridTraveler(m - 1, n, memo) + gridTraveler(m, n - 1, memo)
return memo[key]
}
// t = O(m * n)
// s = O(n + m)
console.log(gridTraveler(1, 1)) // 1
console.log(gridTraveler(2, 3)) // 3
console.log(gridTraveler(3, 2)) // 3
console.log(gridTraveler(3, 3)) // 6
console.log(gridTraveler(8, 8)) // 3432
console.log(gridTraveler(18, 18)) // 2333606220
Memoization Recipe
- Make it work
- visualize the problem as a tree
- tmplement the tree using recursion
- test it
- Make it efficient
- Add a memo object
- Add a base case to return memo values
- Store return values into memo
Can Sum
Write a function canSum(targetSum, nums)
that takes in a target sum and an array of numbers as arguments.
The function should return a boolean indicating whether or not it is possible to generate the targetSum using numbers from the array.
You may use an element of the array as many times as needed.
You may assume that all inputs are non negative.
const canSum = (targetSum, nums) => {
if (targetSum === 0) return true
if (targetSum < 0) return false
for (let n of nums) {
let remainder = targetSum - n
if (canSum(remainder, nums) === true) {
return true
}
}
return false
}
// t = O(n^m)
// s = O(m)
console.log(canSum(7, [2, 3])) // true
console.log(canSum(8, [2, 3, 5])) // true
console.log(canSum(7, [5, 3, 4, 7])) // true
console.log(canSum(7, [5, 3, 4, 7])) // true
console.log(canSum(7, [2, 4])) // false
console.log(canSum(300, [7, 14])) // false
// Memoized
const canSum = (targetSum, nums, memo = {}) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return true
if (targetSum < 0) return false
for (let n of nums) {
const remainder = targetSum - n
if (canSum(remainder, nums, memo) === true) {
memo[targetSum] = true
return true
}
}
memo[targetSum] = false
return false
}
console.log(canSum(7, [2, 3])) // true
console.log(canSum(8, [2, 3, 5])) // true
console.log(canSum(7, [5, 3, 4, 7])) // true
console.log(canSum(7, [5, 3, 4, 7])) // true
console.log(canSum(7, [2, 4])) // false
console.log(canSum(300, [7, 14])) // false
Time = O(m * n)
Space = O(m)
How Sum
Write a function howSum(targetSum, nums)
that takes in a targetSum and an array of numbers as arguments.
The function should return an array containing any combination of elements that add up to exactly the targetSum. If there is no combination that adds up to the targetSum, then return null.
If there are multiple combinations possible you may return any single one.
const howSum = (targetSum, nums) => {
if (targetSum === 0) return []
if (targetSum < 0) return null
for (let n of nums) {
const remainder = targetSum - n
const remainderResult = howSum(remainder, nums)
if (remainderResult !== null) {
return [...remainderResult, n]
}
}
return null
}
console.log(howSum(7, [2, 3])) // [3, 2, 2]
console.log(howSum(7, [5, 3, 4, 7])) // [4, 3]
console.log(howSum(7, [2, 4])) // null
console.log(howSum(8, [2, 3, 5])) // [2, 2, 2, 2]
console.log(howSum(300, [7, 14])) // []
m = targetSum
n = array length
Time = O(n ^ m * m)
Space = O(m)
// Memoized
const howSum = (targetSum, nums, memo = {}) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return []
if (targetSum < 0) return null
for (let n of nums) {
const remainder = targetSum - n
const remainderResult = howSum(remainder, nums, memo)
if (remainderResult !== null) {
memo[targetSum] = [...remainderResult, n]
return memo[targetSum]
}
}
memo[targetSum] = null
return null
}
console.log(howSum(7, [2, 3])) // [3, 2, 2]
console.log(howSum(7, [5, 3, 4, 7])) // [4, 3]
console.log(howSum(7, [2, 4])) // null
console.log(howSum(8, [2, 3, 5])) // [2, 2, 2, 2]
console.log(howSum(300, [7, 14])) // []
Time = O(n * m^2)
Space = O(m^2)
Best Sum
Write a function bestSum(targetSum, nums)
that takes in a targetSum and an array of numbers as arguments
The function should return an array containing the shortest combination of numbers that add up to exactly the targetSum.
If there is a tie for the shortest combination, you may return any one of the shortest.
const bestSum = (targetSum, nums) => {
if (targetSum === 0) return []
if (targetSum < 0) return null
let shortestCombination = null
for (let n of nums) {
const remainder = targetSum - n
const remainderCombination = bestSum(remainder, nums)
if (remainderCombination !== null) {
const comb = [...remainderCombination, n]
if (shortestCombination === null || comb.length < shortestCombination.length) {
shortestCombination = comb
}
}
}
return shortestCombination
}
console.log(bestSum(7, [5, 3, 4, 7])) // [7]
console.log(bestSum(8, [2, 3, 5])) // [3, 5]
console.log(bestSum(8, [1, 4, 5])) // [4, 4]
console.log(bestSum(100, [1, 2, 5, 25])) // [25, 25, 25, 25]
Time = O(m * n^m)
Space = O(m^2)
// Memoized
const bestSum = (targetSum, nums, memo = {}) => {
if (targetSum in memo) return memo[targetSum]
if (targetSum === 0) return []
if (targetSum < 0) return null
let smallest = null
for (let n of nums) {
const remainder = targetSum - n
const remainderCombination = bestSum(remainder, nums, memo)
if (remainderCombination !== null) {
const comb = [...remainderCombination, n]
if (smallest === null || comb.length < smallest.length) {
smallest = comb
}
}
}
memo[targetSum] = smallest
return smallest
}
console.log(bestSum(7, [5, 3, 4, 7])) // [7]
console.log(bestSum(8, [2, 3, 5])) // [3, 5]
console.log(bestSum(8, [1, 4, 5])) // [4, 4]
console.log(bestSum(100, [1, 2, 5, 25])) // [25, 25, 25, 25]
Time = O(n * m ^ 2)
Space = O(m^2)
Summary
canSum -> "Can you do it? yes/no" -> Decision Problem
howSum -> "How can you do it?" -> Combinatoric Problem
bestSum -> "What is the 'best' way to do it?" -> Optimization Problem
Can Construct
Write a function canConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return a boolean indicating whether or not the target
can be constructed by concatenating elements of the wordBank
array.
You may reuse elements of wordBank
as many times as needed.
const canConstruct = (target, wordBank) => {
if (target === '') return true
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
if (canConstruct(suffix, wordBank) === true) {
return true
}
}
}
return false
}
console.log(canConstruct('abcdef', ['ab', 'cd', 'ef', 'abc', 'def', 'abcd', 'ef']))
console.log(canConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar']))
console.log(canConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't']))
console.log(canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee']))
m = target length n = word bank length
Time = O(n^m * m)
Space = O(m^2)
// Memoized
const canConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return true
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const suffix = target.slice(word.length)
if (canConstruct(suffix, wordBank, memo) === true) {
memo[target] = true
return true
}
}
}
memo[target] = false
return false
}
console.log(canConstruct('abcdef', ['ab', 'cd', 'ef', 'abc', 'def', 'abcd', 'ef']))
console.log(canConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar']))
console.log(canConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't']))
console.log(canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee']))
m = target length n = word bank length
Time = O(n * m^2)
Space = O(m^2)
Count Construct
Write a function countConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return the number of ways that the target
can be constructed by concatenating elements of the wordBank
array.
You may reuse elements of the word bank as many times as you want.
const countConstruct = (target, wordBank) => {
if (target === '') return 1
let totalCount = 0
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const numWaysForRest = countConstruct(target.slice(word.length), wordBank)
totalCount += numWaysForRest
}
}
return totalCount
}
console.log(countConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl']))
console.log(countConstruct('abcdef', ['ab', 'cd', 'ef', 'abc', 'def', 'abcd', 'ef']))
console.log(countConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar']))
console.log(countConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't']))
console.log(countConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee']))
m = target length n = word bank length
Time = O(n * m^2)
Space = O(m^2)
// Memoized
const countConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return 1
let totalCount = 0
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const numWaysForRest = countConstruct(target.slice(word.length), wordBank, memo)
totalCount += numWaysForRest
}
}
memo[target] = totalCount
return totalCount
}
console.log(countConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl']))
console.log(countConstruct('abcdef', ['ab', 'cd', 'ef', 'abc', 'def', 'abcd', 'ef']))
console.log(countConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar']))
console.log(countConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't']))
console.log(countConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee']))
m = target length n = word bank length
Time = O(n * m^2)
Space = O(m^2)
All Construct
Write a function allConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return a 2D array containing all of the ways that the target
can be constructed by concatenating elements of the wordBank
array.
const allConstruct = (target, wordBank) => {
if (target === '') return [[]]
const result = []
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const suf = target.slice(word.length)
const suffixWays = allConstruct(suf, wordBank)
const targetWays = suffixWays.map((way) => [word, ...way])
result.push(...targetWays)
}
}
return result
}
console.log(allConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl']))
// [
// ['purp', 'le'],
// ['p', 'ur', 'p', 'le'],
// ]
console.log(allConstruct('hello', ['dog', 'cat', 'mouse']))
// []
console.log(allConstruct('', ['dog', 'cat', 'mouse']))
// [[]]
m = target length n = word bank length
Time = O(n * m^2)
Space = O(m^2)
// Memoized
const allConstruct = (target, wordBank, memo = {}) => {
if (target in memo) return memo[target]
if (target === '') return [[]]
const result = []
for (let word of wordBank) {
if (target.indexOf(word) === 0) {
const suf = target.slice(word.length)
const suffixWays = allConstruct(suf, wordBank, memo)
const targetWays = suffixWays.map((way) => [word, ...way])
result.push(...targetWays)
}
}
memo[target] = result
return result
}
console.log(allConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl']))
// [
// ['purp', 'le'],
// ['p', 'ur', 'p', 'le'],
// ]
console.log(allConstruct('hello', ['dog', 'cat', 'mouse']))
// []
console.log(allConstruct('', ['dog', 'cat', 'mouse']))
// [[]]
f## Tabulation
Tabaulation is another technique used to solve dynamic programming problems.
Visualize as a table Size the based on inputs Initialize the table with default values Seed the trivial answer into the table Iterate through the table Fill further positions based on the current problem
Fib Tabulation
Write a function fib(n)
that takes in a number as an argument. The function should return the n-th number of the Fibonacci sequence.
The 0th number of the sequence is 0. The 1st number of the sequence is 1.
To generate the next number of the sequence, we sum the previous two.
const fib = (n) => {
const table = Array(n + 1).fill(0)
table[1] = 1
for (let i = 0; i <= n; i++) {
table[i + 1] += table[i]
table[i + 2] += table[i]
}
return table[n]
}
console.log(fib(6))
Time = O(n)
Space = O(n)
Grid Traveler
Write a function gridTraveler(m, n)
that calculates how many ways from top left to bottom right of a grid which is m x n spaces large.
// m rows, n columns
const gridTraveler = (m, n) => {
const table = Array(m + 1)
.fill()
.map(() => Array(n + 1).fill(0))
table[1][1] = 1
for (let i = 0; i <= m; i++) {
for (let j = 0; j <= n; j++) {
const current = table[i][j]
if (j + 1 <= n) table[i][j + 1] += current
if (i + 1 <= m) table[i + 1][j] += current
}
}
return table[m][n]
}
console.log(gridTraveler(1, 1)) // 1
console.log(gridTraveler(2, 3)) // 3
console.log(gridTraveler(3, 2)) // 3
console.log(gridTraveler(3, 3)) // 6
console.log(gridTraveler(18, 18)) // 2333606220
Time = O(mn)
Space = O(mn)
Can Sum
Write a function canSum(targetSum, numbers)
that takes in a target sum and an array of numbers as arguments.
The function should return a boolean indicating whether or not it is possible to generate the targetSum using numbers from the array.
You may use an element of the array as many times as needed.
You may assume that all inputs are nonnegative.
const canSum = (targetSum, numbers) => {
const table = Array(targetSum + 1).fill(false)
table[0] = true
for (let i = 0; i <= targetSum; i++) {
if (table[i]) {
for (const n of numbers) {
table[i + n] = true
}
}
}
return table[targetSum]
}
// m = targetSum
// n = numbers.length
// t = o(mn)
// s = (m)
console.log(canSum(7, [2, 3])) // true
console.log(canSum(7, [5, 3, 4])) // true
console.log(canSum(7, [2, 4])) // false
console.log(canSum(8, [2, 3, 5])) // true
console.log(canSum(300, [7, 14])) // false
How Sum
Write a function howSum(targetSum, nums)
that takes in a targetSum and an array of numbers as arguments.
The function should return an array containing any combination of elements that add up to exactly the targetSum.
If there is no combination that adds up to the targetSum, then return null.
If there are multiple combinations possible you may return any single one.
const howSum = (targetSum, nums) => {
const table = Array(targetSum + 1).fill(null)
table[0] = []
for (let i = 0; i < targetSum; i++) {
if (table[i] !== null) {
for (const n of nums) {
table[i + n] = [...table[i], n]
}
}
}
return table[targetSum]
}
// t = o(nm ^ 2)
// s = o(m ^ 2)
console.log(howSum(7, [2, 3])) // [3, 2, 2]
console.log(howSum(7, [5, 3, 4, 7])) // [4, 3]
console.log(howSum(7, [2, 4])) // null
console.log(howSum(8, [2, 3, 5])) // [2, 2, 2, 2]
console.log(howSum(300, [7, 14])) // null
Best Sum
Write a function bestSum(targetSum, nums)
that takes in a targetSum and an array of numbers as arguments
The function should return an array containing the shortest combination of numbers that add up to exactly the targetSum.
If there is a tie for the shortest combination, you may return any one of the shortest.
const bestSum = (targetSum, nums) => {
const table = Array(targetSum + 1).fill(null)
table[0] = []
for (let i = 0; i < targetSum; i++) {
if (table[i] != null) {
for (let n of nums) {
const newComb = [...table[i], n]
if (!table[i + n] || table[i + n].length > newComb.length) {
table[i + n] = newComb
}
}
}
}
return table[targetSum]
}
console.log(bestSum(7, [5, 3, 4, 7])) // [7]
console.log(bestSum(8, [2, 3, 5])) // [3, 5]
console.log(bestSum(8, [1, 4, 5])) // [4, 4]
console.log(bestSum(100, [1, 2, 5, 25])) // [25, 25, 25, 25]
// t = o(nm ^ 2)
// s = o(m ^ 2)
Time = O(m * n^m)
Space = O(m^2)
Can Construct
Write a function canConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return a boolean indicating whether or not the target
can be constructed by concatenating elements of the wordBank
array.
You may reuse elements of wordBank
as many times as needed.
const canConstruct = (target, wordBank) => {
const table = Array(target.length + 1).fill(false)
table[0] = true
for (let i = 0; i < target.length; i++) {
if (table[i]) {
for (let word of wordBank) {
if (target.slice(i, i + word.length) === word) {
table[i + word.length] = true
}
}
}
}
return table[target.length]
}
console.log(canConstruct('abcdef', ['ab', 'cd', 'ef', 'abc', 'def', 'abcd', 'ef'])) // true
console.log(canConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar'])) // false
console.log(canConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't', 'o'])) // true
console.log(canConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee'])) // false
// t = o(nm ^ 2)
// s = o(m)
Count Consruct
Write a function countConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return the number of ways that the target
can be constructed by concatenating elements of the wordBank
array.
You may reuse elements of the word bank as many times as you want.
const countConstruct = (target, wordBank) => {
const table = Array(target.length + 1).fill(0)
table[0] = 1
for (let i = 0; i < target.length; i++) {
for (const word of wordBank) {
if (target.slice(i, i + word.length) === word) {
table[i + word.length] += table[i]
}
}
}
return table[target.length]
}
console.log(countConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl'])) // 2
console.log(countConstruct('abcdef', ['ab', 'abc', 'cd', 'def', 'abcd'])) // 1
console.log(countConstruct('skateboard', ['bo', 'rd', 'ate', 't', 'ska', 'sk', 'boar'])) // 0
console.log(countConstruct('potato', ['p', 'ot', 'eo', 'g', 'a', 't']))
console.log(countConstruct('eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeef', ['e', 'ee', 'eee', 'eeee', 'eeeee', 'eeeeee']))
m = target length n = word bank length
Time = O(n * m^2)
Space = O(m)
All Construct
Write a function allConstruct(target, wordBank)
that accepts a target string and an array of strings.
The function should return a 2D array containing all of the ways that the target
can be constructed by concatenating elements of the wordBank
array.
const allConstruct = (target, wordBank) => {
const table = Array(target.length + 1)
.fill()
.map(() => [])
table[0] = [[]]
for (let i = 0; i < target.length; i++) {
for (const word of wordBank) {
if (target.slice(i, i + word.length) === word) {
const newComb = table[i].map((arr) => [...arr, word])
table[i + word.length].push(...newComb)
}
}
}
return table[target.length]
}
console.log(allConstruct('fish', ['dog', 'cat', 'mouse'])) // [[]]
console.log(allConstruct('bird', ['bi', 'rd', 'do', 'g'])) // [ [ 'bi', 'rd' ] ]
console.log(allConstruct('purple', ['purp', 'p', 'ur', 'le', 'purpl']))
// [
// ['purp', 'le'],
// ['p', 'ur', 'p', 'le'],
// ]
console.log(allConstruct('ape', ['a', 'p', 'e', 'ap', 'pe']))
// [ [ 'purp', 'le' ], [ 'p', 'ur', 'p', 'le' ] ]
// []
// [ [] ]
// [ [ 'a', 'pe' ], [ 'ap', 'e' ], [ 'a', 'p', 'e' ] ]
m = target length n = word bank length
Time = O(n^m)
Space = O(n^m)
Conclusion
Dynamic Programming
- notice any overlapping sub problems
- decide what is the trivially smallest input
- think recursively to use memoization
- think iteratively to use Tabulation
- draw a strategy first!