简介
动态规划是大厂面试常考的算法题类型,这类问题类似于数学归纳法,又像求数列的通项公式,即假设[0, n-1]的情况已知,如何去求dp[n]。解决问题的关键自然是找到这个通项公式,也就是状态转移方程。而找到状态转移方程的三个个难点又在于,一是如何定义状态,即DP Table每个位置的含义;二是如何定义选择,即每个状态可以通过其他状态的哪些变化而来;三是,在能够解决问题的情况下,如何根据状态转移方程来缩小空间复杂度。我们在每一个例子中来理解这三个问题。
经典的斐波那契数列问题
求斐波那契数列的第n个数的值(leetcode#509)
var fib = function(n) {
// 生成dp table
const dp = new Array(n+1).fill(0)
dp[0] = 0
dp[1] = 1
for(let i = 2; i <= n; i++){
// 状态转移方程
dp[i] = dp[i-1]+dp[i-2]
}
return dp[n]
};
其实非常的简单明了,dp[i]代表斐波那契数列的第i个数,即状态;选择在这里只有一种,就是对前一个数和前前一个数求和;状态转移方程为。观察发现其实dp[i]只和前两个数的状态有关,并没有必要开辟一个O(n)的空间,所以我们可以进一步状态压缩,每次只记录前两个数字。
var fib = function(n) {
if(n === 0) return 0
let prev = 0, cur = 1
for(let i = 2; i <= n; i++){
[cur,prev] = [prev + cur,cur]
}
return cur
};
换零钱问题
给定一个目标金额amount,以及可用的各种零钱额度coins,求凑够这个金额所使用的最少金币数(leetcode#300)
var coinChange = function(coins, amount) {
const dp = new Array(amount+1).fill(Infinity)
dp[0] = 0
for(let i = 1; i <= amount; i++){
for(let coin of coins) {
if(i-coin < 0) continue
dp[i] = Math.min(dp[i], dp[i-coin]+1)
}
}
return dp[amount] === Infinity ? -1:dp[amount]
}
状态dp[i]代表凑够i金额所使用的最少金币数,选择当然是所有的coins,状态转移方程考虑每一个金额都可以由dp[i-k](k of coins)加上一枚k额度的金币获得,所以。
最长递增子序列问题(LIS)
求一个数组的最长递增子序列的长度(leetcode#300),子序列的含义是A subsequence is a sequence that can be derived from an array by deleting some or no elements without changing the order of the remaining elements. For example, [3,6,2,7] is a subsequence of the array [0,3,1,6,2,2,7].
// dp[i]代表以第i个元素结尾的最长递增子序列长度
var lengthOfLIS = function(nums) {
let dp = new Array(nums.length).fill(1)
for(let i = 0; i < nums.length; i++){
for(let j = 0; j < i; j++){
if(nums[j] < nums[i]){
dp[i] = Math.max(dp[i], dp[j]+1)
}
}
}
return Math.max.apply(null, dp)
};
状态dp[i]代表以第i个元素结尾的最长递增子序列长度,选择是找当前位置之前,比当前位置数字小的(因为是递增的嘛),状态转移方程是从找到的比当前位置小的位置的dp[j]的最大值加1,即。需要注意的是最终的结果是对各个位置结尾的LIS再取一次最大值。