{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Dynamic Programming (DP)\n", "\"Open\n", "\n", "- https://www.cs.cmu.edu/~avrim/451f09/lectures/lect1001.pdf\n", "- https://www.geeksforgeeks.org/overlapping-subproblems-property-in-dynamic-programming-dp-1/\n", "- powerful technique that allows one to solve many different types of problemns in time $O(n^2)$ or $O(n^3)$ for which a naive approach would take exponential time\n", "- two main properties of a problem that warrents DP solution:\n", " 1. Overlapping Subproblems\n", " 2. Optimal Substructures" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Overlapping Subproblems\n", "- problem combines solutions from many overlapping sub-problems\n", "- DP is not useful when there are no common (overlapping) subproblems\n", "- computed solutions to sub-problems are stored in a look-up table to avoid recomputation\n", "- slighlty different from **Divide and Conquer** technqiue\n", " - divide the problems into smaller non-overlapping subproblems and solve them independently\n", " - e.g.: merge sort and quick sort\n", "\n", "## Optimal Substructures\n", "- optimal solution of the given problem can be obtained by using optimal solutions of its subproblems" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2 Types of DP solutions\n", "\n", "## 1. Top-Down (Memoization)\n", "- based on the Latin word memorandum, meaning \"to be remembered\" \n", "- similar to word memorization, its a technique used in coding to improve program runtime by memorizing intermediate solutions\n", "- using dict type lookup data structure, one can memorize intermediate results of subproblems\n", "- tpically recursion use top-down approach\n", "\n", "### Process\n", "- start solving the given problem by breaking it down\n", "- first check to see if the given problem has been solved already\n", " - if so, return the saved answer\n", " - if not, solve it and save the answer\n", " \n", "## 2. Bottom-Up (Tabulation)\n", "- start solving from the trivial subproblem\n", "- store the results in a table/list/array\n", "- move up towards the given problem by using the results of subproblems\n", "- typically iterative solutions uses bottom-up approach" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### simple recursive fib function\n", "- recall, fibonacci definition is recursive and has many common/overlapping subproblems\n", "" ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fib at 30th position = 1346269\n", "fib function count = 2692537\n" ] } ], "source": [ "count = 0\n", "def fib(n):\n", " global count\n", " count += 1\n", " if n <= 1:\n", " return 1\n", " f = fib(n-1) + fib(n-2)\n", " return f\n", "\n", "n=30 #40, 50? takes a while\n", "print(\"fib at {}th position = {}\".format(n, fib(n)))\n", "print(\"fib function count = {}\".format(count))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### theoritical computational complexity\n", "- Time Complexity: $T(n)$ = time to calculate Fib(n-1) + Fib(n-2) + time to add them: O(1)\n", "- using Big-Oh (O) notation for upper-bound:\n", " - $T(n) = T(n-1) + T(n-2) + O(1)$\n", " - $T(n) = O(2^{n-1}) + O(2^{n-2}) + O(1)$\n", " - $T(n) = O(2^n)$\n", " \n", " **precisely**\n", " - $T(n) = O(1.6)^n$\n", " \n", " - 1.6... is called golden ratio - https://www.mathsisfun.com/numbers/golden-ratio.html\n", "- Space Complexity = $O(n)$ due to call stack" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.35596761399983734\n" ] } ], "source": [ "#print(globals())\n", "import timeit\n", "print(timeit.timeit('fib(30)', number=1, globals=globals()))\n", "# big difference between 30 and 40" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### memoized recursive fib function" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "count = 0\n", "def MemoizedFib(memo, n):\n", " global count\n", " count += 1\n", " if n <= 1:\n", " return 1\n", " if n in memo:\n", " return memo[n]\n", " memo[n] = MemoizedFib(memo, n-1) + MemoizedFib(memo, n-2)\n", " return memo[n]" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fib at 1000th position = 70330367711422815821835254877183549770181269836358732742604905087154537118196933579742249494562611733487750449241765991088186363265450223647106012053374121273867339111198139373125598767690091902245245323403501\n", "fib function called 2118 times.\n" ] } ], "source": [ "memo = {}\n", "n=1000 #try 40, 50, 60, 100, 500, 10000, ...\n", "print(\"fib at {}th position = {}\".format(n, MemoizedFib(memo, n)))\n", "print(\"fib function called {} times.\".format(count)) " ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.0009976609999284847\n" ] } ], "source": [ "import timeit\n", "memo = {}\n", "n=1000\n", "print(timeit.timeit('MemoizedFib(memo, n)', number=1, globals=globals()))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### computational complexity of memoized fib\n", "- Time Complexity - $O(n)$\n", "- Space Complexity - $O(n)$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### normally large integer answers are reported in mod $(10^9+7)$\n", "- mod of a farily large prime number\n", "- need to know some modular arithmetic: https://www.khanacademy.org/computing/computer-science/cryptography/modarithmetic/a/modular-addition-and-subtraction\n", "- $(A+B) \\% C = (A \\% C + B \\% C) \\% C$\n", "- $(A-B) \\% C = (A \\% C - B \\% C) \\% C$\n" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [], "source": [ "mod = 1000000007\n", "def MemoizedModFib(memo, n):\n", " if n <= 1:\n", " return 1\n", " if n in memo:\n", " return memo[n]\n", " memo[n] = (MemoizedFib(memo, n-1)%mod + MemoizedFib(memo, n-2)%mod)%mod\n", " return memo[n]" ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "fib at 1000th position = 107579939\n" ] } ], "source": [ "memo = {}\n", "n=1000 #try 40, 50, 60, 100, 500, 10000, ...\n", "print(\"fib at {}th position = {}\".format(n, MemoizedModFib(memo, n))) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### bottom-up (iterative) fibonacci solution\n", "- first calculate fib(0) then fib(1), then fib(2), fib(3), and so on" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "def iterativeFib(n):\n", " # fib array/list\n", " fib = [1]*(n+1) # initialize 0..n list with 1\n", " for i in range(2, n+1):\n", " fib[i] = fib[i-1] + fib[i-2]\n", " return fib[i]" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.0001866370002971962\n" ] } ], "source": [ "n=1000\n", "print(timeit.timeit('iterativeFib(n)', number=1, globals=globals()))\n", "# is faster than recursive counterpart" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Coin Change Problem\n", "- https://www.geeksforgeeks.org/understanding-the-coin-change-problem-with-dynamic-programming/\n", "- essential to understanding the paradigm of DP\n", "- a variation of problem definition:\n", " - Given a infinite number of coins of various denominations such as $1$ cent (penny), $5$ cents (nickel), and $10$ cents (dime), can you determine the total number of combinations (order doesn't matter) of the coins in the given list to make up some amount $N$?\n", "- Example 1: \n", " - Input: coins = $[1, 5, 10]$, $N = 8$\n", " - Output: 2\n", " - Combinations: \n", " 1. $1 + 1 + 1 + 1+1+1+1+1 = 8$\n", " - $1 + 1 + 1 + 5 = 8 $ \n", "- Example 2:\n", " - Input: coins = $[1, 5, 10], N = 10$\n", " - Output: 4\n", " - Combinations:\n", " 1. $1+1+1+1+1+1+1+1+1+1=10$\n", " - $ 1+1+1+1+1+5 = 10$\n", " - $ 5+5 = 10$\n", " - $10 = 10$\n", "- Implementation:\n", " - we use tabulation/list/array to store the number of ways for outcome $N = 0$ to $12$\n", " - values of list represent the number of ways; indices represent the outcome/sum $N$\n", " - so ways = [0, 0, 0, 0, 0, 0....] initialized with 12 0s\n", " - base case:\n", " - ways[0] = 1; there's 1 way to make sum N=0 using 0 coin\n", " - for each coin:\n", " - if the value of coin is less than the outcome/index $N$,\n", " - update the ways[n] = ways[n-coin] + ways[n]" ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [], "source": [ "def countWays(coins, N):\n", " # use ways table to store the results\n", " # ways[i] will store the number of solutions for value i\n", " ways = [0]*(N+1) # initialize all values 0-12 as 0\n", " # base case \n", " ways[0] = 1\n", " # pick all coins one by one\n", " # update the ways starting from the value of the picked coin\n", " print('values:', list(range(N+1)))\n", " for coin in coins:\n", " for i in range(coin, N+1):\n", " ways[i] += ways[i-coin]\n", " print('ways: ', ways, coin)\n", " return ways[N]\n", " " ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "values: [0, 1, 2, 3, 4, 5, 6, 7, 8]\n", "ways: [1, 1, 1, 1, 1, 1, 1, 1, 1] 1\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2] 5\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2] 10\n", "Number of Ways to get 8 = 2\n" ] } ], "source": [ "coins = [1, 5, 10]\n", "N = 8\n", "print('Number of Ways to get {} = {}'.format(N, countWays(coins, N)))" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]\n", "ways: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3] 5\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 4] 10\n", "Number of Ways to get 10 = 4\n" ] } ], "source": [ "N = 10\n", "print('Number of Ways to get {} = {}'.format(N, countWays(coins, N)))" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "values: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]\n", "ways: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] 1\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3] 5\n", "ways: [1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 4, 4, 4] 10\n", "Number of Ways to get 10 = 4\n" ] } ], "source": [ "N = 10\n", "print('Number of Ways to get {} = {}'.format(N, countWays(coins, 12)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## find minimum number of coins that make a given value/change\n", "- Problem: \n", " - Input: $coins = [5, 10, 25], N = 30$\n", " - Output: 2\n", " - Combinations: $25 + 5 = 30$" ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "import math\n", "\n", "# DP solution for min coin count to make the change N\n", "def minCoins(coins, N):\n", " # count list stores the minimum number of coins required for i value\n", " # all values 0-N are initialized to infinity\n", " count = [math.inf]*(N+1)\n", " # base case\n", " # no. of coin required to make 0 value is 0\n", " count[0] = 0\n", " # computer min coins for all values from 1 to N\n", " for i in range(1, N+1):\n", " for coin in coins:\n", " # for every coin smaller than value i\n", " if coin <= i:\n", " if count[i-coin]+1 < count[i]:\n", " count[i] = count[i-coin]+1\n", " return count[N]\n", " " ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "min coins required to give total of 6 change = 2\n" ] } ], "source": [ "coins = [1, 3, 4]\n", "N = 6\n", "print('min coins required to give total of {} change = {}'.format(N, minCoins(coins, N)))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Exercises\n", "1. Ocean's Anti-11 - https://open.kattis.com/problems/anti11\n", " - Hint: count all possible n length binary integers (without 11) for the first few (2,3,4) positive integers and you'll see a Fibonaccii like pattern that gives the total number of possible binaries without 11 in them\n", "- Write a program that finds factorials of a bunch of positive integer numbers. Would memoization improve time complexity of the program? " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.3" } }, "nbformat": 4, "nbformat_minor": 2 }