=========================================================================== CSC 373H Lecture Summary for Week 4 Winter 2006 =========================================================================== Rock Climber's Problem - Suppose a rock climber wants to get to the top of a rock by the safest path possible. - At each step, climber reaches for handholds above. Can only reach handholds that are nearby: above, above to left, and above to right. - Associated with each handhold is a "danger" rating; the "danger" of a path is the sum of danger ratings of all handholds used. - Example: express as a table of danger ratings: 2 8 9 5 8 5 4 6 2 3 5 7 6 6 1 3 2 5 4 8 - Obvious greedy doesn't yield solution: gives path (bottom to top) 2, 5, 4, 2 for total of 13. - Optimal path is 4, 1, 2, 5 for total of 12. - Idea: once we know the best path to every handhold on a layer, we can easily compute the ratings of the best path to each handhold on the next layer. . The best path to a handhold on the top layer is a solution to the entire problem. - Observe that we only need one path to a particular handhold. We don't need to know this path to move to the next layer. - Recursive solution: to find the best way to get to a handhold j in row i, check the danger of getting to handholds (i-1, j-1), (i-1, j) and (i-1, j+1) and take the best. - Works, but each level of recursion makes three calls, for Theta(3^n) calls total. Still too much! - Memoization: Notice that we compute the best path to a handhold (call it A(i,j)) many times. Instead of recomputing, let's compute it once and remember it! - Create an array where A(i,j) is the value of the best path to handhold (i,j) - Use our recurrence to compute each A(i,j) - as needed, or - in bottom-up fashion (usually much easier) - Formally define recurrence: Say d(i,j) is danger of handhold (i,j) Then A(i,j) = . d(i,j), if i=0 (handhold in bottom row), or . d(i,j) + min { A(i-1,j-1), A(i-1,j), A(i-1,j+1) } otherwise - Use this to compute table in bottom-up fashion (start with bottom row, and compute next rows using these values) oo 13 19 16 12 15 oo A = oo 12 11 14 7 8 oo oo 7 9 8 10 5 oo oo 3 2 5 4 8 oo (oo = infinity, an easy way to not worry about boundary cases) - Next: reconstuct the path that yielded the lowest danger value. Dynamic Programming Paradigm: - For optimization problems that satisfy the following properties: . "simple subproblems": subproblems can be characterized precisely using a constant number of parameters (usually numerical indices); . "subproblem optimality": an optimal solution to the problem can be obtained from optimal solutions to subproblems; . "subproblem overlap": smaller subproblems are repeated many times as part of larger problems. - Step 0: Describe recursive structure of problem: how problem can be decomposed into simple subproblems and how global optimal solution relates to optimal solutions to these subproblems. - Step 1: Define an array indexed by the parameters that define subproblems, to store the optimal value for each subproblem (make sure one of the "sub"problems actually equals the whole problem). - Step 2: Based on the recursive structure of the problem, describe a recurrence relation satisfied by the array values from step 1 (including degenerate or base cases). - Step 3: Write iterative algorithm to compute values in the array, in a bottom-up fashion, following recurrence from step 2. - Step 4: Use computed array values to figure out actual solution that achieves best value (generally, describe how to modify algorithm from step 3 to be able to find answer). Matrix Chain Multiplication (cont'd). From last time, our array recurrence was N[i,i] = 0, and, for i < j, N[i,j] = min{ d_i d_k d_{j+1} + N[i,k-1] + N[k,j] : i < k <= j } - Basic recursive solution: BestCost(d, i, j): if i = j: return 0 else: // i < j best := oo for k := i+1 to j: try := d[i]*d[k]*d[j+1] + BestCost(d, i, k-1) + BestCost(d, k, j) if try < best: best := try return best Running time? Combinatorial explosion! Just like simple Fibonacci, many subproblems are recomputed over and over giving exponential running time. (For example, trace calls for BestCost(d, 0, 4).) - Bottom-up algorithm: . Instead of recomputing values many times, compute smaller values first and store them in an array to be looked up (so we never need to make recursive calls). In class, I suggested to compute values one diagonal at a time, but other ways possible as long as all entries N[i,k-1] and N[k,j] are present by the time N[i,j] is computed. For example, MatrixChain(d, n): for i := n-1 downto 0: N[i,i] := 0 for j := i+1 to n-1: N[i,j] := oo for k := i+1 to j: try := d[i]*d[k]*d[j+1] + N[i,k-1] + N[k,j] if try < N[i,j]: N[i,j] := try return N[0,n-1] . Trace on example input: 2, 3, 5, 1, 8 snapshots for each value of i (each row filled left-to-right): i = 3: i = 2: 0 1 2 3 0 1 2 3 0 0 1 1 2 2 0 40 3 0 3 0 i = 1: i = 0: 0 1 2 3 0 1 2 3 0 0 0 30 21 37 1 0 15 39 1 0 15 39 2 0 40 2 0 40 3 0 3 0 - Running time: O(n^3) (nested loops iterating O(n) times). - Algorithm computes minimum cost but does not give order of multiplications to achieve this cost. - Possibility: working from N[0,n-1], recompute all possibilities considered in order to find breakpoint k that yielded best value; then recursively do the same for each subproblem. This requires additional Theta(n^3) time. - Trick: use a second array B[i,j] to store best value of k used to compute N[i,j]. At the end, B[0,n-1] = index of last multiplication to perform, and we can recursively print each subproduct. MatrixChain(d, n): for i := n-1 downto 0: N[i,i] := 0; B[i,i] := i for j := i+1 to n-1: N[i,j] := oo; B[i,j] := -1 for k := i+1 to j: try := d[i]*d[k]*d[j+1] + N[i,k-1] + N[k,j] if try < N[i,j]: N[i,j] := try; B[i,j] := k parenthesize(B, 0, n-1) This yields the following values in array B for example above: i = 3: i = 2: 0 1 2 3 0 1 2 3 0 0 1 1 2 2 2 3 3 3 3 3 i = 1: i = 0: 0 1 2 3 0 1 2 3 0 0 0 1 1 3 1 1 2 3 1 1 2 3 2 2 3 2 2 3 3 3 3 3 Subroutine to print the final answer recursively. // print best way to compute A_i...A_j parenthesize(B, i, j): if i = j: print "A_i" else: print "(" parenthesize(B, i, B[i,j]-1) print ") (" parenthesize(B, B[i,j], j) print ")" For the example above, the result would be: ((A_0) ((A_1) (A_2))) (A_3) Interval scheduling with profits: [6.1] - Just like activity (interval) scheduling but each activity (interval) has a "profit" and we want schedule with maximum profit. More precisely: Input: Activities (s_1,f_1,w_1), ..., (s_n,f_n,w_n) where s_i = start time, f_i = finish time, w_i = profit. Output: Subset of activities S subset {1,2,...,n} such that no activities in S overlap and profit(S) is maximal. - Greedy doesn't work, no matter how we sort. - Step 0: Subproblems consist of subsets of activities to choose from, but no easy way to characterize these using constant number of parameters. Trick: Sort activities by finish time, as before (f_1 <= ... <= f_n). Consider optimal schedule S, and last activity scheduled in S, say k. Then k must be larger than index of all other jobs scheduled (because of sorting order) and rest of schedule must consist of optimal way to schedule the other activities.