Lecture 19: Dynamic Programming
CSE 373: Data Structures and Algorithms
1
Administrivia
Dynamic Programming
3
What is Dynamic Programming
An algorithmic technique of optimizing a given algorithm by:
4
Dynamic Programming Techniques
A little confusing? Don’t worry, you are not alone!
5
Question
Given a number n, print n-th Fibonacci Number.
How can we solve this problem in the most optimized way?
6
Clarify
7
Example
Edge Case 1:
n = 0; Fib = None
Output = 0
Edge Case 2:
n = 1; Fib = 1
Output = 1
Middle Case 1:
n = 2; Fib = 1, 1
Output = 1
Middle Case 2:
n = 9; Fib = 1, 1, 2, 3, 5, 8,
13, 21, 34
Output = 34
8
Approach
Brute Force:
Has O(2^n) runtime with O(n) space complexity…
There is a faster way using Dynamic Programming…
9
Recursive Solution
public static int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n-1) + fib(n-2);
}
10
Memoized Dynamic Programming Solution
public int fib(int n, int[] memo) {
if (memo[n] != null) {
return memo[n];
} else if (n <= 1) {
return n;
} else {
int result = fib(n-1, memo)
+ fib(n-2, memo);
memo[n] = result;
return result;
}
}
11
2n+1 recursive calls gives us O(n) instead of O(2n)
Fibonacci Memoized Solution Expression
Memo[i] = { 0 , if i = 0 }
{ 1 , if i = 1 }
{ Memo[1] + Memo[2] , otherwise }
Fib(i) = { Memo[0] , if i = 0 }
{ Memo[1] , if i =1 }
{ Memo[i-1] + Memo[i-2] , otherwise }
This might be on the Final Exam….
12
Bottom Up Dynamic Programming Solution
public int fib(int n) {
int f[] = new int[n+1];
f[0] = 0;
f[1] = 1;
f[2] = 1;
for (int i = 3; i <= n; i++) {
f[i] = f[i-1] + f[i-2];
}
return f[n];
}
13
Optimize
14
Implement
15
Implement - Java Code
static int fib(int n) {
int a = 0, b = 1, c;
if (n == 0)
return a;
for (int i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
16
Test
Test with Middle Case 2:
n = 9
Fib = 1, 1, 2, 3, 5, 8, 13, 21, 34
Resulting variable values after for loop ends:
a = 21, b = 34, c = 34
Return b = 34
17
Question - Practice on your own! :))
You are climbing a staircase. It takes n steps to reach the top.
Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?
How can we solve this problem in the most optimized way?
18
Clarify
19
Example
Edge Case 1:
n = 1; 1. Take one step forward.
Output = 1
Edge Case 2:
n = 2; 1) 1 + 1 step 2) 2 steps
Output = 2
Middle Case:
n = 3;
1) 1 + 1 + 1 steps
2) 1 + 2 steps
3) 2 + 1 steps
Output = 3
20
Approach
Brute Force:
Has O(2n) runtime with O(n) space complexity…unless…dynamic programming…
21
Optimize
22
Implement
23
Implement - Java Code
public int climbStairs(int n) {
int secondLast = 1, last = 1, current = 1;
if (n == 1)
return last;
for (int i = 2; i <= n; i++) {
current = secondLast + last;
secondLast = last;
last = current;
}
return last;
}
24
Fun Fact
The previous solution runs faster than 100% of leetcode solutions for the problem…which is the first time I have ever gotten a 100%...if you haven’t been convinced of the power of DP yet…you should be now…
Test
Test with Middle Case:
n = 3
3 different ways to get to step 3
Resulting variable values after for loop ends:
secondLast = 2, last = 3, current = 3
Return last = 3
26
More Examples of Dynamic Programming Problems
Helpful walk through videos:
27
P4 Dynamic Programming Portion
Watch Intro Video Here
TAs not allowed to help you code Extra Credit Dynamic Programming Part of P4
28
Embedded Video From Previous Slide
29
Questions?
30
31
32
33
2. Identify sub problems: Paths in the DAG represent valid stacks
34
3. Find how subproblems build to larger solution
35
4. Generalize relationship between subproblems and final solution
36
5. Implement solving subproblems in correct order
37
38
Divide and Conquer
Quick Sort (v1)
0 |
8 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 2 | 91 | 22 | 55 | 1 | 7 | 6 |
Divide
0 | 1 | 2 | 3 |
2 | 1 | 7 | 6 |
0 | 1 | 2 |
91 | 22 | 55 |
0 | 1 | 2 | 3 |
1 | 2 | 6 | 7 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 2 | 6 | 7 | 8 | 22 | 55 | 91 |
Combine
0 |
1 |
0 |
2 |
0 |
6 |
0 |
7 |
0 |
8 |
0 |
22 |
0 | 1 | 2 | 3 |
1 | 6 | 7 | 55 |
0 |
55 |
0 |
91 |
…
…
Conquer
Choose a “pivot” element, partition array relative to it!
Again, no extra conquer step needed!
Simply concatenate the now-sorted arrays!
PIVOT
0 |
8 |
Quick Sort (v1): Divide Step
0 |
8 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 2 | 91 | 22 | 55 | 1 | 7 | 6 |
Divide
0 | 1 | 2 | 3 |
2 | 1 | 7 | 6 |
0 | 1 | 2 |
91 | 22 | 55 |
Recursive Case:
PIVOT
Base Case:
0 | 1 |
7 | 6 |
0 |
1 |
0 |
2 |
PIVOT
PIVOT
0 | 1 |
22 | 55 |
0 |
91 |
PIVOT
PIVOT
0 |
6 |
0 |
7 |
0 |
22 |
0 |
55 |
Quick Sort (v1): Combine Step
0 |
8 |
Combine
Simply concatenate the arrays that were created earlier! Partition step already left them in order ☺
0 |
1 |
0 |
2 |
0 |
91 |
0 |
6 |
0 |
7 |
0 |
22 |
0 |
55 |
0 | 1 |
6 | 7 |
0 | 1 |
22 | 55 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
1 | 2 | 6 | 7 | 8 | 22 | 55 | 91 |
0 | 1 | 2 | 3 |
1 | 2 | 6 | 7 |
0 | 1 | 2 |
22 | 55 | 91 |
Quick Sort (v1)
quickSort(list) {
if (list.length == 1):
return list
else:
pivot = choosePivot(list)
smallerHalf = quickSort(getSmaller(pivot, list))
largerHalf = quickSort(getBigger(pivot, list))
return smallerHalf + pivot + largerHalf
}
Worst case runtime?
Best case runtime?
In-practice runtime?
Stable?
In-place?
No
Can be done!
0 | 1 | 2 | 3 |
2 | 1 | 7 | 6 |
0 | 1 |
7 | 6 |
0 |
1 |
0 |
2 |
PIVOT
PIVOT
0 |
6 |
0 |
7 |
0 | 1 | 2 | 3 |
1 | 2 | 6 | 7 |
0 | 1 |
6 | 7 |
Worst case: Pivot only chops off one value
Best case: Pivot divides each array in half
Can we do better?
Strategies for Choosing a Pivot
Most commonly used
Quick Sort (v2: In-Place)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
8 | 1 | 4 | 9 | 0 | 3 | 5 | 2 | 7 | 6 |
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
6 | 1 | 4 | 9 | 0 | 3 | 5 | 2 | 7 | 8 |
Low
X < 6
High
X >= 6
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
6 | 1 | 4 | 2 | 0 | 3 | 5 | 9 | 7 | 8 |
Low
X < 6
High
X >= 6
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
5 | 1 | 4 | 2 | 0 | 3 | 6 | 9 | 7 | 8 |
PIVOT?
PIVOT?
PIVOT?
PIVOT!
Select a pivot
Move pivot out of the way
Bring low and high pointers together, swapping elements if needed
Meeting point is where pivot belongs; swap in. Now recurse on smaller portions of same array!
Divide
Quick Sort (v2: In-Place)
quickSort(list) {
if (list.length == 1):
return list
else:
pivot = choosePivot(list)
smallerPart, largerPart = partition(pivot, list)
smallerPart = quickSort(smallerPart)
largerPart = quickSort(largerPart)
return smallerPart + pivot + largerPart
}
Worst case runtime?
Best case runtime?
In-practice runtime?
Stable?
In-place?
No
Yes
0 | 1 | 2 | 3 | 4 | 5 |
0 | 3 | 6 | 9 | 7 | 8 |
choosePivot:
- Use one of the pivot selection strategies
partition:
Can we do better?
We’d really like to avoid hitting the worst case.
Key to getting a good running time, is always cutting the array (about) in half.
How do we choose a good pivot?
CSE 373 19 SU - ROBBIE WEBER
48
Pivots
CSE 373 19 SU - ROBBIE WEBER
49
Median of three is a common choice in practice
Sorting: Summary
| Best-Case | Worst-Case | Space | Stable |
Selection Sort | Θ(n2) | Θ(n2) | Θ(1) | No |
Insertion Sort | Θ(n) | Θ(n2) | Θ(1) | Yes |
Heap Sort | Θ(nlogn) | Θ(nlogn) | Θ(n) | No |
In-Place Heap Sort | Θ(nlogn) | Θ(nlogn) | Θ(1) | No |
Merge Sort | Θ(nlogn) | Θ(nlogn) | Θ(nlogn) Θ(n)* optimized | Yes |
Quick Sort | Θ(nlogn) | Θ(n2) | Θ(n) | No |
In-place Quick Sort | Θ(nlogn) | Θ(n2) | Θ(1) | No |
What does Java do?
Key Takeaway: No single sorting algorithm is “the best”!
* They actually use Tim Sort, which is very similar to Merge Sort in theory, but has some minor details different
Insertion Sort
STRATEGY 1:
ITERATIVE IMPROVEMENT
STRATEGY 2:
IMPOSE STRUCTURE
STRATEGY 3:
DIVIDE AND CONQUER
Selection Sort
Heap Sort
Merge Sort
Quick Sort
WORST
BEST
STABLE
IN-PLACE
IN-PLACE
IN-PLACE
IN-PLACE
STABLE
WORST
BEST
WORST
BEST
WORST
BEST
WORST
BEST
Minimizes array writes, otherwise never preferred.
Simple, stable, low-overhead, great if already sorted.
Always good runtimes
Stable, very reliable! In-place variant is slower.
Fastest in practice (constant factors), bad worst case.
SPACE
SPACE
SPACE
SPACE
SPACE
Insertion Sort
STRATEGY 1:
ITERATIVE IMPROVEMENT
STRATEGY 2:
IMPOSE STRUCTURE
STRATEGY 3:
DIVIDE AND CONQUER
Selection Sort
Heap Sort
Merge Sort
Quick Sort
WORST
BEST
STABLE
IN-PLACE
IN-PLACE
IN-PLACE
IN-PLACE
STABLE
WORST
BEST
WORST
BEST
WORST
BEST
WORST
BEST
Minimizes array writes, otherwise never preferred.
Simple, stable, low-overhead, great if already sorted.
Always good runtimes
Stable, very reliable! In-place variant is slower.
Fastest in practice (constant factors), bad worst case.
SPACE
SPACE
SPACE
SPACE
SPACE
Can we do better than n log n?
But Don’t Take it From Me…
DANCE EDITION
Here are some excellent visualizations for the sorting algorithms we’ve talked about!
Comparing Sorting Algorithms
Comparing Sorting Algorithms