Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Dynamic Programming

Dynamic Programming

or, Why Aaron Patterson should give me $50 : What I learned from the Project Euler coding challenge (and so can you!)

Seattle, WA
Seattle.rb

Hsing-Hui Hsu

October 08, 2014
Tweet

More Decks by Hsing-Hui Hsu

Other Decks in Programming

Transcript

  1. Dynamic Programming or, why Aaron Patterson should give me $50

    So I’m going to talk about Dynamic Programming, which is something I learned while trying to do the September coding challenge. I’m pretty sure I didn’t win, but in trying to solve the problems on Project Euler I ended up yak-shaving myself into giving this talk. So.
  2. Your Mission: Project Euler So, I had a long a

    complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  3. Your Mission: Project Euler • Math! So, I had a

    long a complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  4. Your Mission: Project Euler • Math! • Coding! So, I

    had a long a complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  5. Your Mission: Project Euler • Math! • Coding! • Appearing

    badass! So, I had a long a complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  6. Your Mission: Project Euler • Math! • Coding! • Appearing

    badass! So, I had a long a complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  7. Your Mission: Project Euler • Math! • Coding! • Appearing

    badass! • So, I had a long a complicated history with Project Euler. I first encountered maybe 4 years ago, and hated it, probably bc at the time, while I really liked math, I didn’t actually know how to program, and due to reasons I first attempted them in Java. Fast forward to last year, and I came back to PE as a way to help me learn Ruby, and it was a completely different. I loved it!* Because math! And coding is fun! It impressed all my friends! I did about a dozen or so, and then a bunch more during the May Seattle.rb coding challenge.
  8. But I was quickly hitting a wall. PE problems get

    really hard really fast, and I found myself getting pretty stuck on a certain few.
  9. Problem 18: Maximum Sum Path So this was one of

    the first problems that I ended up getting stuck on for a very long time. It wasn’t obvious to me how to efficiently traverse the pyramid to find the optimal sum—every solution I came up with was either brute forcey, or wrong, or both.
  10. Problem 31 How many ways can you make change out

    of 2£ using 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p)? This was another one I got stuck on for the longest time. It’s a fairly classic problem, and from hearing a talk given at Seattle.rb, I knew there was a recursive way of solving it, but recursion was breaking my head and I just couldn’t do it.
  11. But upon closer inspection…. But after poking around the internet

    for help, it turned out that all these problems I was getting super stuck on had something in common.
  12. So, what exactly is DP? • A method for solving

    complex problems by breaking them down into simpler subproblems. • Useful for problems that compute the same subproblems over and over.
 So the thing I was finding in common was that all the problems could be broken up in to smaller sub-problems and these smaller subproblems are in turn divided in to still-smaller ones. It quickly became clear that there were some over-lappping subproblems, (big hint for DP) and because I was ending up calculating all those subproblems each time over and over, my program was slooowwww.
  13. Two types of DP Top-Down: Store previously computed results, a.k.a.

    use Memoization.
 Bottom-Up: Get the optimal solution for a subproblem starting from the end, and working back up.
 Top-Down : Start solving the given problem by breaking it down. If you see that the problem has been solved already, then just return the saved answer. If it has not been solved, solve it and save the answer. This is usually easy to think of and very intuitive. This is referred to as Memoization. Bottom-Up : Analyze the problem and see the order in which the sub-problems are solved and start solving from the trivial subproblem, up towards the given problem. In this process, it is guaranteed that the subproblems are solved before solving the problem. This is referred to as Dynamic Programming.

  14. Fibonacci Sequence But if you’re like me, I learn better

    by example. So let’s take a familiar problem, such as implementing a way of finding the nth Fibonacci number. (We’re going to use the top-down approach here.) Fibonacci sequence is a sequence of numbers* where each number is the sum of the two previous fibonacci numbers. Here’s a basic iterative implementation of it, which works, but is kind of boring.
  15. Fibonacci Sequence 1, 1, 2, 3, 5, 8, 13, 21,

    34, 55… But if you’re like me, I learn better by example. So let’s take a familiar problem, such as implementing a way of finding the nth Fibonacci number. (We’re going to use the top-down approach here.) Fibonacci sequence is a sequence of numbers* where each number is the sum of the two previous fibonacci numbers. Here’s a basic iterative implementation of it, which works, but is kind of boring.
  16. Fibonacci Sequence 1, 1, 2, 3, 5, 8, 13, 21,

    34, 55… But if you’re like me, I learn better by example. So let’s take a familiar problem, such as implementing a way of finding the nth Fibonacci number. (We’re going to use the top-down approach here.) Fibonacci sequence is a sequence of numbers* where each number is the sum of the two previous fibonacci numbers. Here’s a basic iterative implementation of it, which works, but is kind of boring.
  17. Fibonacci Sequence Recursive implementation This kind of problem is fortunately

    great for getting beginners to think about recursion. So here’s another implementation of the same problem of finding the nth fibonacci number, except using recursion. When I first understood how recursion worked, I was like…
  18. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  19. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end #=> 0.000017 But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  20. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end #=> 0.000017 #=> 0.000947 But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  21. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end #=> 0.000017 #=> 0.000947 #=> 0.114328 But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  22. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end #=> 0.000017 #=> 0.000947 #=> 0.114328 #=> 14.128237 But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  23. require ‘benchmark’ Benchmark.bm do |x|
 x.report { recursive_fib 10 }


    x.report { recursive_fib 20 }
 x.report { recursive_fib 30 }
 x.report { recursive_fib 40 } x.report { recursive_fib 45 }
 end #=> 0.000017 #=> 0.000947 #=> 0.114328 #=> 14.128237 #=> 159.97751 But wait a minute. Turns out recursion isn’t all that great in this scenario. Using benchmark to time how long it took to compute the 10th, 20th 30th and so on fib number,(click) it takes about17 microseconds for 10, less than a millisecond for 20. But then at 30 you get more than a 10th of a second, and at 40 it takes 14 seconds, and even 45 takes over 2.5 minutes.
  24. Taking a look at even how the 5th Fibonacci number

    is computed using this recursive method, you can see that calling fib on 5 calls fib of 4 and 3, which in turn calls the function on 3 and 2 and so on. You can see that there’s a lot of repeated calls.
  25. O(n!) In terms of big-O notation for those who are

    familiar or care about such things, that is a time complexity of O(n!). For those who aren’t familiar, that basically translates* to “Oh, no.”
  26. O(n!) == O(%#@‼) In terms of big-O notation for those

    who are familiar or care about such things, that is a time complexity of O(n!). For those who aren’t familiar, that basically translates* to “Oh, no.”
  27. Fibonacci: DP solution So as we saw in the recursive

    implementation, there were many repeated calls, why not store those computations? In this solution, there is an array, fibs, initialized at the start of the function. Then, finding the nth fibonacci number is just a matter of looking at the nth index in that array. I.e. this is the top down approach, where each computation is memoized in the fibs array.
  28. 0 1 2 3 4 5 6 7 8 0

    1 1 2 3 5 8 13 21 So basically, if the nth value already exists in the array, you just look it up, and worst case scenario, it only takes you n calls to find that number. In other words, you can get the nth fibonacci number in* linear time.
  29. O(n) == 0 1 2 3 4 5 6 7

    8 0 1 1 2 3 5 8 13 21 So basically, if the nth value already exists in the array, you just look it up, and worst case scenario, it only takes you n calls to find that number. In other words, you can get the nth fibonacci number in* linear time.
  30. The key difference here is that while n-2 and n-1

    are still being used as arguments, in one it is used as an index to access a previously computed and stored result, whereas in recursion you have to actually recompute everything all the way down.
  31. Maximum Sum Given an array of numbers, how would you

    compute which subset of numbers has the maximum sum if no two elements in the subset can be adjacent in the original array? Ex: [6, 4, 5, 7, 1, 12, 3, 2] => 27 So let’s move on to a more interesting example. How would you find the maximum sum returned by a subset of that array, if none of the elements in the subset can be adjacent? So for example, in the case of the array [6, 4, 5, 7, 1, 12, 3, 2], the possible subsets would be [6, 5, 1], [6, 7, 12], [4, 7, 12], [4, 7, 3] , [4, 1, 3] and so on. The sub-array whose elements reduces to the maximum possible sum is [6,7,12, 2], with a sum of 27. So, does anyone have any ideas as to how you’d approach this problem?
  32. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 [draw binary tree here] Even if you constructed all subsets of the array efficiently, you end up doing a depth first search of every possible legal combination of elements. (Explain terminal cases, how value for each index gets passed back up to the parent node)
  33. Here is an example of an implementation that solves this

    using brute force. As we saw in the diagram of what is happening when the Fibonacci sequence function is implemented recursively, this also requires a depth-first traversal of the tree from the previous slide.
  34. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  35. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  36. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  37. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  38. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  39. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  40. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  41. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  42. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  43. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  44. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  45. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  46. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  47. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  48. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 + So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  49. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  50. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  51. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  52. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  53. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  54. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  55. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 25 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  56. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 25 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  57. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 25 25 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  58. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 25 25 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  59. index 0 1 2 3 4 5 6 7 value

    6 4 5 7 1 12 3 2 index 0 1 2 3 4 5 6 7 max sum 6 6 4 4 6 5 11 6 13 13 25 25 27 So, in the bottom-up dynamic programming solution, you again have an array that stores the maximum sums up to that point in your iteration. You assume that anything that came before is the optimal, in this case maximum, solution. So, starting with the first element, you assume that it is included in the subset that contains the maximum sum. Since that subset at the first iteration only contains a 6, 6 is the maximum sum. Now we move to the second element. Since a subset that contains this element cannot also contain the first element (or else there would be adjacent elements in the subset), we compare which of 4 and 6 is larger and we get 6*. Moving to the next element, we know that the maximum sum at this point in the iteration has to either contain this element plus any subset that might include it. That is*, compare the sum of 5 and the previous max sum (6) and see if it’s bigger than the max sum of any subset that does not contain 5, which would be represented in the max_sum array at the previous index*. (etc)
  60. require ‘benchmark’ test1 = (1..30).to_a.shuffle test2 = (1..60).to_a.shuffle Benchmark.bm do

    |x| x.report { brute_force test1 } #=> 0.004670
 x.report { brute_force test2 } #=> 17.847418 x.report { max_subset test1 } #=> 0.000016 x.report { max_subset test2 } #=> 0.000013 end To prove that this DP approach is in fact more efficient, we can time it. For test1, we have DP clocking in at ~4 milliseconds versus ~16 microseconds for the brute force method, e.g. 287.5 times faster, and for test2, we get ~17 seconds versus ~13 microseconds, e.g. over 1 million times faster.
  61. Back to our regular scheduled programming… Problem 31: How many

    ways can you make change out of 2£ using 1p, 2p, 5p, 10p, 20p, 50p, £1 (100p) and £2 (200p)? Now let’s look at the Project Euler problem mentioned at the beginning of this talk.
  62. Here is a recursive implementation. I won’t go into too

    much detail, but if you’re interested in how it works, please watch Aja Hammerly’s excellent talk, “A World Without Assignment.” (http://confreaks.tv/videos/mwrc2014-a-world-without-assignment)
  63. Storing previously calculated results 0 1 2 3 4 5

    6 7 8 9 10 0 1 5 10 coins amount in cents So again, rather than re-calculating results we will need for future calculations, we store them all. Here, we use a two-dimensional chart to store these results rather than a single chart (as with the maximum sum of non-adjacent elements problem), which I will explain in a minute.
  64. So let’s first consider the terminal cases of this problem.

    These are the two terminal cases of the coin change problem. When you have no coins, or your total amount you’re trying to make change for is negative, then there are no ways to make change. If your amount is exactly 0 and you have some amount of coins, there is exactly one way to make change.
  65. Storing previously calculated results 0 1 2 3 4 5

    6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 5 1 10 1 amount in cents coins So we fill in the first row and column of our chart accordingly.
  66. Now let’s consider the meat of the logic. We know

    that with dynamic programming, we want to build upon previously calculated results. In this case, the two kinds of previously calculated results will be how many ways we can make change for the given amount with all the coins except the coin with the largest value (`ways_without_max`), and how many ways we can make change for that amount minus the value of your largest denomination using all of the coins (`ways_with_max`).
  67. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  68. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  69. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  70. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  71. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  72. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  73. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  74. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  75. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 +1 =1 0+ 1 = 1 5 1 10 1 1 1 1 1 1 1 1 1 coins amount in cents So for example, suppose we only have pennies. Then if our amount is 1, we know from our algorithm that the number of ways to make change is to add how many ways you can make change with everything except pennies (in this case, with no coins, which we know is zero from the top row of the same column, indicated by the blue 0), and how many ways we can make change with the amount minus the value of a penny using only pennies i.e. how can we make change for $0.00 with pennies? Again, from the previously stored result in the first column second row, we know that is 1 (the red 1). And so forth.
  76. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 5 1 1+ 0 =1 1 1 1 1+1 = 2 1 +1 = 2 10 1 coins amount in cents In other words, each cell in the chart is the sum of the value inherited from row above, plus the value at amount minus max for that current row, which will always have been previously calculated.
  77. count_change(amount, coins[0…-1]) + count_change(amount - max, coins) 0 1 2

    3 4 5 6 7 8 9 10 0 0 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 5 1 1 1 1 1 2 2 2 2 2 3 10 1 1 1 1 1 2 2 2 2 2 4 coins amount in cents Filling in the chart, we get these values.
  78. So putting together this talk ended up being kind of

    a yak shave. Turns out talks are a lot of work, and I actually didn’t get to many PE problems, which meant I didn’t win the $50 Aaron Patterson promised to the winner of this month’s Seattle.rb coding challenge. However, I did learn a ton, and since the coding challenge was just Seattle.rb’s way of getting more people to actively participate, I feel like giving this talk should totally count, don’t you?
  79. So putting together this talk ended up being kind of

    a yak shave. Turns out talks are a lot of work, and I actually didn’t get to many PE problems, which meant I didn’t win the $50 Aaron Patterson promised to the winner of this month’s Seattle.rb coding challenge. However, I did learn a ton, and since the coding challenge was just Seattle.rb’s way of getting more people to actively participate, I feel like giving this talk should totally count, don’t you?
  80. So what’s DP good for? • Solving problems quickly that

    might otherwise take a lot of time using more traditional approaches such as recursion • Examples: Coin Change, Knapsack problem, substring/DNA matching, minimum/maximum path
  81. No really, what’s DP good for? • Passing technical interviews

    at companies who arguably don’t know how to interview properly • I don’t know
  82. • http://www.codechef.com/wiki/tutorial-dynamic- programming • h t t p : /

    / w w w . a l g o r i t h m i s t . c o m / i n d e x . p h p / Dynamic_Programming • http://www.geeksforgeeks.org/dynamic-programming- set-10-0-1-knapsack-problem/ • http://functionspace.org/articles/32/Fibonacci-series-and- Dynamic-programming • http://confreaks.tv/videos/mwrc2014-a-world-without- assignment (Walk-through of coin-change problem implemented recursively)
  83. Questions? Comments? Can we be friends? 
 …Should @tenderlove give

    me $50? Hsing-Hui Hsu @SoManyHs Project Euler friend key: 270590_f23cf789be3f1c9c72b26 6ffe4c2541f