# Introduction to Dynamic Programming ## About me * Carlos Gonzalez Oliver * Emai: carlos@ozeki.io * Homepage: carlosoliver.co **My interests:** * Structural Bioinformatics * Graph algorithms * Representation learning _Some questions I ask:_ * _How can we efficiently search through graph databases?_ * _What kinds of patterns can we discover in datasets of graphs?_ * _Can we model biological systems using efficient data structures to learn about how they function?_ --- ## Dynamic Programming (DP) | | | | -------- | -------- | | ![](https://upload.wikimedia.org/wikipedia/en/7/7a/Richard_Ernest_Bellman.jpg) | <ul> <li>Richard Bellman, 1950s, working at RAND institute.</li> <li>Picked the name "Dynamic Programming" to please his boss. </li>| * Method for solving a large problem by breaking it down into smaller sub-problems. Applications * **Bioinformatics: protein and RNA folding, sequence alignment** * Speech recognition: Viterbi's algorithm * Time series modeling: Dynamic time warping * Search: string matching * **Scheduling: weighted interval** * Music: Beat tracking * Routing: shortest path problems --- ## Objectives After this lesson we should be able to: ::: info - [ ] Recognize the **ingredients** needed to solve a problem with DP. - [ ] Write down and execute some simple DP algorithms. - [ ] Be able to implement a DP algorithm for a real-world problem (homework). ::: --- ## Basic Intuition * What is: * $1 + 1 + 1 + 1 + 1 + 1 = ?$ * Now what is: * $1 + 1 + 1 + 1 + 1 + 1 + 1 = ?$ <pre> </pre> --- ## First ingredient: Optimal Substructures ::: info The optimal structure can be built by combining optimal solutions to smaller problems. ::: E.g. Fibonacci numbers * $Fib(0) = 0$ * $Fib(1) = 1$ * $Fib(n) = Fib(n-1) + Fib(n-2)$ Recursive algorithm: ``` ``` --- ## Second ingredient: Overlapping Substructures :::info The same subproblem's solution is used multiple times. ::: ```python= def fib(n): if n == 0: return 0 if n == 1: return 1 else: return fib(n-1) + fib(n-2) ``` Call tree `fib(6)`: ``` ``` Recursive solution is $\mathcal{O}(2^n)$. --- ## Can we do better? :hand: `M-fib(n)`: ``` ``` --- ## An example _without_ overlapping subproblems. **Binary Search**: Given a sorted array $A$ and a query element, find the index where the query occurs. ```python def binary_search(A, left, right, query): if low > high: return None mid = (left + right) // 2 if A[mid] == query: return i elif A[mid] > query: return binary_search(A, left, mid-1, query) else: return binary_search(A, mid+1, right, query) ``` Call tree `binary_search(A=[ 15, 22, 32, 36, 41, 63, 75], left=0, right=4, query=75)`: ``` ``` --- ## Case study: Weighted Interval Scheduling * **Input:** $n$ requests, labeled $\{1, .., n\}$. Each request has a start time $s_i$ and an end time $f_i$, and a __weight__ $v_i$. * **Output:** a _compatible_ subset $S$ of $\{1, .., n\}$ that maximizes $\sum_{i \in S} v_i$. :::info $S$ is compatible iff all pairs of intervals in $S$ are non-overlapping. ::: * Uses: resource allocation for computer systems, optimizing course selection. Example: ``` ``` --- ## False start: greedy approach * From "previous" lectures we know the greedy approach to the unweighted IS problem gives the optimal solution (i.e. pick item with earliest ending time) Counterexample: ``` ``` :::warning The weights force us to consider all possible subproblems (i.e. local choice is not enough). ::: --- ## A helper function Let us sort all intervals by increasing _finish_ time and let $p(j)$ return the latest interval $i$ that is still compatible with $j$. ![](https://raw.githubusercontent.com/cgoliver/cgoliver.github.io/master/assets/wis.png) --- ## First ingredient: Optimal Substructure? * Consider $\mathcal{O}_j$ to be the _optimal_ set of requests over all items $j$. * Also consider the _weight_ of the best solution up to $j$ as $\mathrm{OPT}(j)$. * There are two cases for the last request, $j$.: Case 1 ``` ``` Case 2 ``` ``` --- ## Optimal Substructure Let $\textrm{OPT}(j)$ be the total weight of the optimal over intevals up to $j$. We can now write an expression for computing $\textrm{OPT}(j)$: ``` ``` **Ingredients** - [x] Optimal Substructure - [ ] Overlapping Subproblems --- ## First algorithm Now we can write down a recursive algorithm that gives us the maximum weight over $1, ..., n$. `compute_OPT(j)`: ``` ``` --- ## Ingredient 2: Overlapping Substructures Let's build the execution tree for our recursive algorithm on this example: ![](https://raw.githubusercontent.com/cgoliver/cgoliver.github.io/master/assets/wis.png) `compute_OPT(6)`: ``` ``` Runtime: ``` ``` **Ingredients** - [x] Optimal Substructure - [x] Overlapping Subproblems --- ## Memoization * Key idea in DP: remember solutions to sub-problems you already computed. New algo: `M_compute_OPT(j)` ``` ``` Runtime: ``` ``` --- ## Are we done? * Recall $\textrm{OPT}(j)$ is just a number, we want the _set_ of intervals with the score $\textrm{OPT}(j)$... i.e. $\mathcal{O}_j$. * **Traceback** is a key idea in DP. We reconstruct the solution backwards from the $M$ array using the recurrence. * **Obervation:** We know that an interval $j$ belongs to $\mathcal{O}_j$ if: ``` ``` Now our DP execution has two steps: 1) Fill $M$ 2) Reconstruct solution from $M[n]$ --- ## Full Example ![](https://raw.githubusercontent.com/cgoliver/cgoliver.github.io/master/assets/wis.png) Fill $M$ and get $\mathcal{O}_n$: ``` ``` --- ## Recap * Dynamic Programming works well when we have a problem structure such that: * Combining sub-problems solves the whole problem * Sub-problems overlap * We can often reduce runtime complexity from exponential to polynomial or linear. * Steps to solving a problem with DP: * Define subproblems: * $\textrm{fib}(n-1)$ and $\textrm{fib}(n-2)$ are subproblems of $\textrm{fib(n)}$ * Write down the recurrence that relates subproblems. * $\textrm{fib}(n) = \textrm{fib}(n-1) + \textrm{fib}(n-2)$ * Recognize and solve the base cases. * $\textrm{fib}(0) = 1$, $\textrm{fib}(1) = 1$ * Implement a solving methodology. (e.g. memoization, tabulation is also an option) --- ## General interest: RNA folding * RNA molecules are essential to all living organisms. * Knowing the sequence is easy but not so informative, knowing the structure is hard but tells us a lot about the molecule's function. ![](https://raw.githubusercontent.com/cgoliver/cgoliver.github.io/master/assets/rna_img.png) --- ## A Sketch of Nussinov's Algorithm * First attempt at solving this problem | | | | -------- | -------- | | <img src="https://upload.wikimedia.org/wikipedia/commons/thumb/5/5a/Ruth_Nussinov.png/440px-Ruth_Nussinov.png" width="200px"> | Ruth Nussinov, designed the first RNA folding algo in 1977. | * In CS terms, an RNA is a string on a 4-letter alphabet. * An RNA __structure__ is a pair of indices over the string with certain constraints. ![](https://raw.githubusercontent.com/cgoliver/cgoliver.github.io/master/assets/rna_arches.png) --- ## RNA folding rules We use some fairly realistic constraints on admissible structures: :::info 1. Only `A-U`, `U-A` and `C-G`, `G-U` form pairs 2. Pairs shall not cross (nestedness). 3. Start and end of a pair should be separated by at least $\theta$ spaces (remove steric clashes). 4. The best structure is the one that forms the most pairs (stability). e.g each pair adds 1 to the score. ::: This lets us identify the problem structure needed for a DP solution: Consider an optimal set of pairs $\mathcal{O}_{ij}$ between two indices $(i, j)$, and the score of the olution $\mathrm{OPT}(i, j)$. We have two cases for index $j$: Case 1. $j$ is not in $\mathcal{O}_{ij}$: ``` ``` **Note we introduce a new variable** $\rightarrow$ 2 dimensional DP. Case 2. $j$ is in $\mathcal{O}_{ij}$: ``` ``` **Ingredients?** - [ ] Optimal substructures - [ ] Overlapping subproblems From this we can build our recurrence that fills the table up to $\mathrm{OPT}(1, L)$. ``` ``` **Bonus Questions:** What is the runtime for filling the table? :::success If you want an exercise sheet to learn how to implement this send me an email `carlos@ozeki.io`. ::: ---