EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017

American University of Beirut · Data Structures and Algorithms · 55 pages · Dr. Louay Bazzi

All EECE 330 files
EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 1

Stuck on a question? A tutor can walk you through it.

EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 2 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 3 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 4 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 5 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 6 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 7 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 8 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 9 EECE 330 Other Extra Lecture About Dynamic Programming, Spring 2017, page 10

Showing the first 10 of 55 pages. Download the PDF to see all of it.

More EECE 330 files

Show the text of this lab
Dynamic Programming Examples not included in slides: take notes Dynamic Programming (DP) • Not a specific algorithm, but a technique, like divide-and-conquer. • Used for: – decision problems (YES/NO answer) – optimization problems: Find a solution with the optimal value. Minimization or maximization – Counting problems • Term “dynamic” for historical reasons Recall the divide-and-conquer design technique • Divide and conquer paradigm involves 3 steps: – Divide problem into a number of sub-problems – Conquer the problem by solving them recursively – Combine the solutions of the sub-problems into the original problem solution • Examples: Binary Search, Merge Sort, Tower of Hanoi, and other recursive functions we have seen. Dynamic Programming versus divide-and-conquer • Similarity: both recursive • Main difference: we have many repeated sub-problems • Direct recursion leads to an exponential time algorithm Dynamic Programming versus divide-and-conquer (continued) • We can overcome this problem using a table (1- dimentional or 2-dimentional array) indexed by the sub- problems • Store, don’t re-compute: – When solving a sub-problem: – Lookup in table. – If answer is there, use it. – Else, compute answer, then store it. • This called the Memoization approach • Another approach is the Bottom up approach: avoid recursion completely by filling table bottom up Main step of Dynamic programming • Characterize the structure of the solution. • Recursively define the value of an solution • Direct recursive solutions is not efficient at all due repeated sub-problems • Use a table indexed by the sub-problems, and proceed by – Memoization, or – the bottom up approach • Construct the solution from computed information. Plan • We will illustrate the above steps on two problems: – Subset Sum problem – Longest Common Subsequence problem Subset Sum problem • Given a set of n positive integers A[0],A[1], …, A[n-1], and a non-negative target integer tar, check whether or not the set contains a subset whose sum is equal to the target • The elements of the desired subset need not be consecutive in the list • The subset can consist of an arbitrary number of elements: 0(if tar=0), 1, 2, 3, 4, … Examples • Set: 9 2 1 5 10 20 29 tar= 27 Answer: YES (2+5+20=27) • Set: 9 2 1 5 10 20 29 tar= 59 Answer: YES (9+1+20+29=59) • Set: 9 2 1 5 10 20 29 tar= 4 Answer: NO (no subset has sum = 4) • Set: 9 2 1 5 10 20 29 tar= 0 Answer: YES (the empty subset has sum = 0) Problem Difficulty • We cannot use nested loops as in the 2- SUM [PSS3] because: – we don’t know beforehand the subset size, and hence the number of need nested loops – and even if we knew the subset size, e.g. size =10, it is not efficient at all to use 10 nested loops as it takes in the order of n10 steps Recursive structure • Original problem: “Does the set A[0],A[1], …, A[n-1] contain a subset whose sum = tar?” • Look at the last element A[n-1] • Either we take it or we don’t take it Recursive structure (Continued) • If we take it, we get the sub-problem: “Does the set A[0],A[1], …, A[n-2] contain a subset whose sum = tar-A[n-1]?” (sub-prob size = n-1) • If we don’t, we get the sub-problem: “Does the set A[0],A[1], …, A[n-2] contain a subset whose sum = tar” (sup-prob size = n-1) • We try both cases recursively • Base case: Recursive structure (Continued) • If we take it, we get the sub-problem: “Does the set A[0],A[1], …, A[n-2] contain a subset whose sum = tar-A[n-1]?” (size = n-1) • If we don’t, we get the sub-problem: “Does the set A[0],A[1], …, A[n-2] contain a subset whose sum = tar” (size = n-1) • We try both cases recursively • Base case: size= 1 return TRUE iff tar== 0 or A[0] • Also return FALSE if tar<0 Recursive solution bool subsetSum(int A[], int i, int tar) // A[0…i] // intitial call cout<<subsetSum(A,n-1,tar); { if(i==0) return (tar==0)||(tar==A[0]); if(tar<0) return false; bool found1= subsetSum(A, i-1,tar-A[i]); bool found2= subsetSum(A, i-1,tar); return (found1 || found2); } Recursive solutions is not efficient at all due repeated sub-problems • Draw recursion on n=5 • Many repeated sub-problems • Exponentially many recursive calls • Takes in the order of 2n steps How many possible sub-problems do we have? How many possible sub-problems do we have? • They are: “Does the size set A[0],A[1], …, A[i] contain a subset whose sum = j?” for i=0….n-1 and for j=0….tar • That is, number of sub-probs = n*(tar+1) • It is quadratic not exponential ! Memoization approach • Use a 2-dimentional array T[0... n-1][0…tar] indexed by the n*(tar+1) sub-problems intialize it to -1’s (i.e., answer not computed yet) • Store, don’t re-compute: – When solving a sub-problem: – Lookup in table. – If answer is there, use it. – Else, compute answer, then store it. Draw table for: 9 2 1 5 (n = 4) and tar=3 Memoized SubsetSum bool memoizedSubSum(int A[], int i, int tar, int **T) // intitial call cout<< memoizedSubSum(A,n-1,tar,T); //table T[0... n-1][0…tar] pre-allocated and initialized to -1’s { if(i==0) return (tar==0)||(tar ==A[0]); //size 1 set if(tar<0) return false; if(tar-A[i]>=0 && T[i-1][tar-A[i]] == -1 ) T[i-1][tar-A[i]] =memoizedSubSum(A, i-1,tar-A[i], T); if( T[i-1][tar] == -1 ) T[i-1][tar] = memoizedSubSum(A, i-1,tar, T); return (tar-A[i]>=0) &&T[i-1][tar-A[i]] || T[i-1][tar] ; } Alternative bottom up approach We can avoid recursion completely by filling table bottom up: bool bottomUpSubsetSum(int A[], int n, int tar) // non-recursive function { bool **T = new bool*[n];// allocate memory for table bool T[0...n-1][0…tar] for(int i=0;i<n;i++) T[i] = new bool[tar+1]; T[0][0] = true; // Initialize first column for(int j=1;j<=tar;j++) T[0][j] = (j ==A[0]); // Initialize first column for(i=1;i<n;i++) // fill table for(int j = 0;j<=tar;j++) // bottom-up if( j-A[i] >=0) T[i][j] = T[i-1][j] || T[i-1][j-A[i]]; else T[i][j] = T[i-1][j]; bool answer = T[n-1][tar]; for(i=0;i< n;i++) delete [] T[i]; delete [] T; return answer; } Draw table for: 9 2 1 5 (n = 4) and tar=3 Finding the subset • If the answer is YES, we want to display a subset of the set whose sum is equal to the target • How? Finding the subset (continued) • Illustrate on previous example Finding the subset by backtracking If answer==true, backtrack using table (before deleting it): int i=n-1; int j=tar; while(j != 0) { if( i>0 && j-A[i]>=0 && T[i-1][j-A[i]] || i==0 && j==A[0] ) { cout<<A[i]<<" "; // take A[i] j=j-A[i]; // backtrack } i--; } Total time • In the order of n*tar to build table • Backtracking takes time linear in n • Thus total time: in the order of n*tar • Much faster than the direct recursive solution which takes in the order of 2n steps Next • Longest Common Subsequence problem • It is an optimization problem (unlike subset sum, which is a decision problem) Longest Common Subsequence (LCS) problem • A subsequence of a string S[1…N] is any sequence S[i1], S[i2], …, S[it], where 1≤i1< i2<…< it ≤N • Example: “Art” is a subsequence of “Algorithms” • Thus, unlike substrings, subsequences need not consist of consecutive characters • LCS problem: Given 2 strings X[1….m] and Y[0…n-1] find the length of the longest subsequence common to both • Note: we are starting from 1 and not from 0 for technical convenience Longest Common Subsequence (LCS) problem (Continued) • Example: X: A B C B D A B Y: B D C A B A Longest Common Subsequence (LCS) problem (Continued) • Example: X: A B C B D A B Y: B D C A B A BCBA is LCS of X and Y BDA is CS but not a LCS (BCBA is longer) BCAB is another LCS Applications • Comparing files, e.g.,. C++ file in EECE 230 to detect cheating • Comparing DNA’s Problem Difficulty • Brute force algorithm: Try every subsequence of X, check if it is subsequence of Y, and take the longest • There are 2m Subsequences of X • Exponential time, takes forever • Greedy approach: take a character of X and try to match to a character of Y, and so on .. • Does not give the longest common subsequence. Try it. Structure of the optimal solution • Look at the last chars of X and Y: X[m] and Y[n] and compare them Structure of the optimal solution (Continued) • Look at the last chars of X and Y: X[m] and Y[n] and compare them • Case 1: If X[m]=Y[n], then the LCS(X,Y) must end with X[m] Why? Otherwise, it can be made longer by adding X[m], which contradicts the LCS optimality Structure of the optimal solution (Continued) • Case 2: If X[m]≠Y[n] Let U be a LCS(X[1…m-1],Y[1…n]) V be a LCS(X[1…m],Y[1…n-1]) W longest among U and V Then W is a LCS(X,Y) Why? Structure of the optimal solution (Continued) Proof: Take any LCS Z of (X,Y) If Z does not end with X[m], then Z is a CS(X[1…m- 1],Y[1…n]). It must be also a LCS of (X[1…m-1],Y[1…n]) since otherwise Z can be made a longer CS of (X,Y), contradicting the maximality of Z. Thus length(Z)=length(U) If Z does not end with Y[n], then by a similar argument length(Z)=length(V) Thus length(W) ≥ length(Z) by the definition of Z But length(Z) ≥ length(W) since Z is a LCS(X,Y) Hence length(W) = length(Z), i.e., W is a LCS(X,Y) Recursive structure of the optimal solution Recursive structure of the optimal solution (Continued) Define C(i,j) = length of the a LCS(X[1…i], Y[1…j]) for i=1,….,m and j=1,….,n. Extended to i=0 and j=0, by leting C(i,j)=0 if i=0 or j=0 (one of the strings is empty) Original problem: C(m,n) Recursive structure of the optimal solution (Continued) Recurrence: If i,j≥1 and X[i]=Y[j] C(i,j) = C(i-1,j-1)+1 If i,j≥1 and X[i]≠Y[j] C(i,j) = max(C(i-1,j),C(i,j-1)) If i=0 or j=0 (base case) C(i,j) = 0 Recursive solution int LCS( char* X, int i, char* Y, int j) //X[1..i] and Y[1…i] start from index 1 not 0 for convenience // intitial call cout<<LCS(X,m,Y,n); { if(i==0 || j==0) return 0; if(X[i]==Y[j]) return LCS(X,i-1,Y,j-1)+1; else { int a = LCS(X,i-1,Y,j); int b = LCS(X,i,Y,j-1); if(a>b) return a; else return b; } } Recursive solutions is not efficient at all due repeated sub-problems • Draw recursion on m=4 and n=3 • Many repeated sub-problems • Exponentially many recursive calls • In the worst case, takes at least 2min(m,n) steps How many possible sub-problems do we have? How many possible sub-problems do we have? • They are: C(i,j) = length of the a LCS(X[1…i], Y[1…j]) for i=0,….,m and j=0,….,n. • That is, number of sub-probs = (m+1)*(n+1) • It is quadratic not exponential ! Memoization approach • Use a 2-dimentional array C[0... m][0…n] indexed by the (m+1)*(n+1) sub-problems intialize it to -1’s (i.e., answer not computed yet) • Store, don’t re-compute: – When solving a sub-problem: – Lookup in table. – If answer is there, use it. – Else, compute answer, then store it. Draw table for X = “ABCB” and Y = “BDC” Memoized LCS int memoizedLCS( char* X, int i, char* Y, int j, int **T) // intitial call cout<< memoizedLCS(X,m,Y,n,T); //table T[0... n][0…m] pre-allocated and initialized to -1’s { if(i==0 || j==0) return 0; if( X[i]==Y[j]) { if(T[i-1][j-1]== -1 ) T[i-1][j-1] =memoizedLCS(X,i-1,Y,j-1,T); return T[i-1][j-1]+1; } else { if(T[i-1][j] == -1) T[i-1][j] =memoizedLCS(X,i-1,Y,j,T); if(T[i][j-1] == -1) T[i][j-1] =memoizedLCS(X,i,Y,j-1,T); if(T[i][j-1] > T[i-1][j]) return T[i][j-1]; else return T[i-1][j]; } } Alternative bottom up approach We can avoid recursion completely by filling table bottom up: int bottomUpLCS(char* X, int m, char* Y, int n) // non-recursive function { //strings X[1..m] and Y[1…n] start from index 1 not 0 for convenience int **T = new int*[m+1];// allocate memory for table int T[0...m][0…n] for(int i=0;i<=m;i++) T[i] = new int[n+1]; for(i=0;i<=m;i++) T[i][0] = 0; // Initialize first row for(int j=1;j<=n;j++) T[0][j] = 0; // Initialize first column for(i=1;i<=n;i++) // fill table for(int j = 1;j<=n;j++) // bottom-up if(X[i]==Y[j]) T[i][j] = T[i-1][j-1]+1; else { if (T[i-1][j]>T[i][j-1]) T[i][j] = T[i-1][j]; else T[i][j] = T[i][j-1]; } int length = T[m][n]; for(i=0;i<=m;i++) delete [] T[i]; delete [] T; return lenght;} Finding the LCS • The above function finds the length of the LCS, we want to find a LCS of X and Y • How? Finding the LCS (Continued) • Use an auxiliary table Aux[1…m][1…n] to store the arrows: up-left (‘\\’), up (‘|’), or left (‘-’), which indicate where the min come from each cell in the table C • Illustrate on previous example • The backtracking using Aux table • We can fill table Aux while filling C by modifying the function bottomUpLCS Modified BottonUpLCS int bottomUpFindLCS(char* X, int m, char* Y, int n, char* char* char* char* LLLL) { // LCS will be stored in L. Assume enough memory is allocated // LCS will be stored in L. Assume enough memory is allocated // LCS will be stored in L. Assume enough memory is allocated // LCS will be stored in L. Assume enough memory is allocated int **T = new int*[m+1];// allocate memory for table int T[0...m][0…n] for(int i=0;i<=m;i++) T[i] = new int[n+1]; char**Aux = new char* [m+1] ; char**Aux = new char* [m+1] ; char**Aux = new char* [m+1] ; char**Aux = new char* [m+1] ; // allocate memory for auxiliary table // allocate memory for auxiliary table // allocate memory for auxiliary table // allocate memory for auxiliary table for (i=1;i<=m; i++) for (i=1;i<=m; i++) for (i=1;i<=m; i++) for (i=1;i<=m; i++) Aux[i Aux[i Aux[i Aux[i] = new char [n+1]; ] = new char [n+1]; ] = new char [n+1]; ] = new char [n+1]; //Aux[1...m][1 //Aux[1...m][1 //Aux[1...m][1 //Aux[1...m][1… …… …n] n] n] n] for(i=0;i<=m;i++) T[i][0] = 0; // Initialize first row for(int j=1;j<=n;j++) T[0][j] = 0; // Initialize first column for(i=1;i<=m;i++) // fill table for(int j = 1;j<=n;j++) // bottom-up if(X[i]==Y[j]) { T[i][j] = T[i-1][j-1]+1; Aux[i][j Aux[i][j Aux[i][j Aux[i][j] = ] = ] = ] = ‘‘‘‘\\\\\\\\’’’’;;;; } // up left // up left // up left // up left else { if (T[i-1][j]>T[i][j-1]) {T[i][j] = T[i-1][j]; Aux[i][j Aux[i][j Aux[i][j Aux[i][j] = ] = ] = ] = ‘‘‘‘----’’’’;;;; } // left // left // left // left else { T[i][j] = T[i][j-1]; Aux[i][j Aux[i][j Aux[i][j Aux[i][j] = ] = ] = ] = ‘‘‘‘||||’’’’;;;; } // up // up // up // up } int length= T[m][n]; Finding a LCS by backtracking //Then we backtrack using the arrows stored in table Aux: //Then we backtrack using the arrows stored in table Aux: //Then we backtrack using the arrows stored in table Aux: //Then we backtrack using the arrows stored in table Aux: i=m; j = n; i=m; j = n; i=m; j = n; i=m; j = n; int int int int k = length k = length k = length k = length----1;1;1;1; while while while while (i>0 (i>0 (i>0 (i>0 & & j>0) { j>0) { j>0) { j>0) { ifififif( ( ( ( Aux[i][j Aux[i][j Aux[i][j Aux[i][j] == ] == ] == ] == ‘‘‘‘\\\\\\\\’’’’) { ) { ) { ) { L[k L[k L[k L[k]= ]= ]= ]=X[i X[i X[i X[i];];];]; iiii--------; j ; j ; j ; j--------; k ; k ; k ; k--------;;;; }}}} else if else if else if else if( ( ( ( Aux[i][j Aux[i][j Aux[i][j Aux[i][j] == ] == ] == ] == ‘‘‘‘||||’’’’) i ) i ) i ) i--------;;;; else else else else jjjj--------;;;; // if // if // if // if Aux[i][j Aux[i][j Aux[i][j Aux[i][j] == ] == ] == ] == ‘‘‘‘----’’’’ }}}} L[length L[length L[length L[length]= ]= ]= ]=‘‘‘‘\\\\0000’’’’; // ; // ; // ; // Null terminate L Null terminate L Null terminate L Null terminate L [delete tables T and Aux] [delete tables T and Aux] [delete tables T and Aux] [delete tables T and Aux] return length; } Total time • In the order of m*n to fill tables T and Aux • Backtracking takes time linear in (m+n) • Thus total time: in the order of m*n • Much faster than the direct recursive solution which takes in the order of 2min(m,n) steps Backtracking in DP • Backtracking is not needed if the desired answer is YES/NO or the size of the object we are looking for (e.g., the length of the LCS), • It is needed if we want to produce the object, e.g., the subset in the subset sum problem if the answer is YES, or a LCS in the LCS problem. Summary • Dynamic Programming (DP) is a design technique like divide-and-conquer • Typically used for optimization problems and also for decision problems and counting problems (counting examples in the Programming Assignment) • Main difference compared to divide-and- conquer: we have many repeated sub-problems • We start by characterizing the structure of the solution and we recursively define the value of an solution Summary (Continued) • If the direct recursive solutions is not efficient (e.g., exponential) due to repeated sub- problems and if the number of sub-problems is small (e.g., quadratic, cubic, or even linear), we use a table indexed by the sub-problems, and proceed by Memoization, or the bottom up approach • To find the object we are looking for, we use backtracking • To backtrack, we may have to use an auxiliary table which we fill while filling the main DP table.