Dynamic Programming I
Dynamic Programming (commonly referred to as DP) is an algorithmic technique for solving a problem by recursively breaking it down into simpler subproblems and using the fact that the optimal solution to the overall problem depends upon the optimal solution to it’s individual subproblems. Here is an interesting Quora question How should I explain dynamic programming to a 4-year-old?.
509. Fibonacci Number
a. Recursive solution
1 | func fib(n int) int { |
b. Dynamic Programming
State transition function: dp[i] = dp[i - 1] + dp[i - 2]
1 | func fib(n int) int { |
70. Climbing Stairs
State transition function: dp[i] = dp[i - 1] + dp[i - 2]
1 | func climbStairs(n int) int { |
If given we can climb the stairs from 1 ~ m steps each time, how to solve this problem? It becomes a full knapsack problem now. And the state transition function is dp[i] += dp[i - j]
, here 2 is the special case.
1 | func climbStairs(n int) int { |
746. Min Cost Climbing Stairs
Denote dp[i] to the cost we want to step away from stair ith, so the state transition function: min(dp[i - 1], dp[i - 2]) + cost[i]
.
1 | func minCostClimbingStairs(cost []int) int { |
Another way to think about it. If we denote dp[i] as the cost to reach to ith stair, the state transition function is dp[n] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])
1 | func minCostClimbingStairs(cost []int) int { |
62. Unique Paths
It’s easy to get the state transition function dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
. Note for the special case first line and first row, the value is 1.
1 | func uniquePaths(m int, n int) int { |
Based on the state transition function, dp[i][j] is defined only by two values, so we can optimize the space complexity from O(m * n) to O(n) by using a new state transition function dp[j] = dp[j] + dp[j - 1]
1 | func uniquePaths(m int, n int) int { |
63. Unique Paths II
This is a similar problem to #62. The only difference is that we need to reset the path sum to 0 if there’s an obstacle at the coordinate.
1 | func uniquePathsWithObstacles(obstacleGrid [][]int) int { |
343. Integer Break
The key point of solving this problem is to get the state transition function. There are two cases:
- we can split the number i into two i - j and j
- we can split the number into more than two, this case we can reuse the cached value from dp array. dp[i - j] * j
So the state transition function dp[i] = max(dp[i], dp[i - j] * j, (i - j) * j)
1 | func integerBreak(n int) int { |
96. Unique Binary Search Trees
Based on the BST specs, we can get the state transition function dp[i] = dp[j] * dp[i - j - 1]
, here dp[i] denotes when i is set to the root node, we have j nodes on left child and i - j - 1 on right child. Note here the base case is 1. If there’s 0 nodes on left tree, it means we can construct the left tree in one uniq way.
1 | func numTrees(n int) int { |