递推算法
数学归纳法
- 验证k0成立
- 证明如果ki成立,那么ki+1也成立
- 由第一步和第二步,证明由k0->ki也成立
如何求解递推问题
- 确定递推状态(重中之重)
- 一个函数符号f(x),外加这个函数符号的含义描述
- 一般函数所对应的值,就是要求解的值
- 确定递推公式(ki -> ki+1)
- 分析边界条件(k0)
- 程序实现(循环||递归)
LeetCode肝题
-
- 爬楼梯
- 递推状态f(n)指的是爬到n层楼梯所用的方法总数
- 能跨到n层楼梯的方法有两个,一种是从n-1层楼梯跨一步,另一种是从n-2层楼梯跨两步,所以到n层楼梯的方法总数等于到n-1层楼梯的方法总数+到n-2层楼梯的方法总数
- 由此推导出递推公式f(n) = f(n-1) + f(n-2)
- 分析边界值到第1层的方式有一种,到第2层的方式有两种f(1) = 1,f(2)=2
var climbStairs = function(n) {
let fn = Array(n + 1)
fn[1] = 1
fn[2] = 2
for (let i = 3; i <= n; i++) fn[i] = fn[i - 1] + fn[i - 2]
return fn[n]
};
-
- 使用最小花费爬楼梯
- 动态规划问题比递推问题多了一个决策过程
- 递推状态dp(n)指的是爬到n层楼梯所用的最小体力花费
- 能跨到n层楼梯的方法有两个,也是n-1层和n-2层,此时要判断从0层到n-1层和从0层到n-2层那种方法花费的体力更小,这也就是决策过程
- 由此推导出递推公式dp(n) = Math.min(dp(n-1) + dp(n-2)) + cost[n]
- 分析边界值,到第0层需要花费cost[0],到第1层需要花费cost[1]
var minCostClimbingStairs = function(cost) {
let n = cost.length, dp = new Array(n + 1)
cost.push(0)
dp[0] = cost[0]
dp[1] = cost[1]
for(let i = 2; i <= n; i++) dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]
return dp[n]
};
- 剑指 Offer II 091. 粉刷房子
- 递推状态dp(n,i)指的粉刷第n个房子并且用第i种颜色的最小花费
- 总共三种颜色,所以第n个房子用第i种颜色的最小花费为粉刷第n-1个房子另外两种颜色的花费+粉刷第n个房子三种颜色花费的最小值
- 由此推导出递推公式dp(n,i) = Math.min(Math.min(dp(n-1,j), dp(n-1,k)) + costs[n][i])
- 分析边界值,粉刷第0个房子用第0种颜色需要花费costs[0][0],第1种颜色需要花费costs[0][1],第2种颜色需要花费costs[0][2]
- 本题dp[n]只依赖dp[n-1]和dp[n-2]的结果,所以可以使用滚动数组优化空间结构
var minCost = function(costs) {
let n = costs.length, dp = [[costs[0][0], costs[0][1], costs[0][2]], []], ans = 0
for(let i = 1; i < n; i++) {
let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
dp[ind][0] = Math.min(dp[pre_ind][1], dp[pre_ind][2]) + costs[i][0]
dp[ind][1] = Math.min(dp[pre_ind][0], dp[pre_ind][2]) + costs[i][1]
dp[ind][2] = Math.min(dp[pre_ind][0], dp[pre_ind][1]) + costs[i][2]
}
let ind = (n - 1) % 2
ans = Math.min(dp[ind][0], dp[ind][1], dp[ind][2])
return ans
};
-
- 三角形最小路径和
- 使用从下往上查找的方法
- 递推状态dp(n,i)指从第n层到第1层所途径的路径最小值
- 除了第n层,其余每层的第i个位置依赖于n+1层i的值和i+1的值最小值 + triangle[n][i]
- 由此推导出递推公式dp(n, i) = Math.min(dp(n+1, i),dp(n+1, i+1)) + triangle[n, i]
- 边界值就是triangle[n-1]
- 本题dp[n]只依赖dp[n+1]的结果,所以可以使用滚动数组优化空间结构
let minimumTotal = function(triangle) {
let dp = new Array(2), n = triangle.length
dp[0] = []
dp[1] = []
dp[(n - 1) % 2] = triangle[n - 1].slice()
for(let i = n - 2; i >= 0; i--) {
let ind = i % 2, next_ind = ind === 0 ? 1 : 0
for (let j = 0; j <= i; j++) {
dp[ind][j] = Math.min(dp[next_ind][j+1], dp[next_ind][j]) + triangle[i][j]
}
}
return dp[0][0]
};
-
- 杨辉三角 II
- 使用从下往上查找的方法
- 递推状态f(n,i)指第0层第i位的值
- 第1层=1,其余每层,除了第一位和最后一位等于1,i位等于上一层i位 + i-1 位
- 推导出递推公式f(n, i) = f(n - 1, i - 1) + f(n - 1, i)
- 边界值就是triangle[n-1]
var getRow = function(n) {
let f = [new Array(n + 1).fill(1), new Array(n + 1).fill(1)]
for(let i = 1; i <= n; i++) {
let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
for(let j = 1; j < i; j++) {
f[ind][j] = f[pre_ind][j - 1] + f[pre_ind][j]
}
}
return f[n % 2]
};
-
- 最大子序和
- 递推状态dp(n)指第n位之前较大的连续和
- dp[0] = nums[0],后面的位置等于前一位的值加上nums里当前位置的值,如果前一位的值为负,说明此次相加对结果不产生增益,所以对前一位和0取最大值
- 推导出递推公式dp(n) = Math.max(dp[n-1], 0) + nums[n]
- 边界值就是dp[0] = nums[0]
- 最后在dp里取最大值
var maxSubArray = function(nums) {
let dp = nums.slice()
for (let i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i - 1], 0) + nums[i]
}
return Math.max(...dp)
};
空间优化,dp[n]只依赖于dp[n-1]的值
let maxSubArray = function(nums) {
let n = nums.length, ans = nums[0], pre = nums[0]
for(let i = 1; i < n; i++) {
pre = Math.max(nums[i], pre + nums[i])
ans = Math.max(pre, ans)
}
return ans
};
-
- 乘积最大子数组
记录两个数,当前值为负先交换最大最小值,然后把乘积和当前值的最小值放入min,为正把乘积和当前值最大值放入max,过程取max的最大值
var maxProduct = function(nums) {
let ans = -Infinity, max = 1, min = 1
for(let item of nums) {
if (item < 0) [max, min] = [min, max]
max = Math.max(item, item * max)
min = Math.min(item, item * min)
ans = Math.max(ans, max)
}
return ans
};
-
- 买卖股票的最佳时机 II
var maxProfit = function(prices) {
let ans = 0
for (let i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) ans += prices[i] - prices[i - 1]
}
return ans
};
-
- 打家劫舍
- 递推状态dp(n,i)指打劫到第n家所获取到的最大收获,i值0是不打劫1是打劫
- 打劫到第n家的最大收获取决于(第n家打劫且第n-1家不打劫的收获)和(第n家不打劫且(第n-1家打劫和不打劫的最大值)的收获)的最大值
- 推导出递推公式dp(n,i) = Math.max(dp[n-1][0] + nums[n], Math.max(dp[n-1][0], dp[n-1][1]))
- 边界值就是dp(0,0) = 0,dp(0,1) = nums[0]
var rob = function(nums) {
let n = nums.length, dp = [[], []]
dp[0][0] = 0
dp[0][1] = nums[0]
for(let i = 1; i < n; i++) {
let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
dp[ind][0] = Math.max(dp[pre_ind][1], dp[pre_ind][0])
dp[ind][1] = dp[pre_ind][0] + nums[i]
}
let ind = (n + 1) % 2
return Math.max(dp[ind][0], dp[ind][1])
};
-
- 零钱兑换
- 递推状态dp(n)指拼凑n面额最少使用的硬币数量
- 拼凑n面额最少的硬币数量取决于当前使用的面额coins[j],如果dp[n - coins[j]]合法,那么dp[n]等于所有(dp[n - coins[j]]+1)的最小值
- 推导出递推公式dp(n) = Math.min(dp[coins[j]] + 1)
- 边界值为凑齐0面额需要使用0个硬币,dp[0]=0
var coinChange = function(coins, amount) {
let dp = new Array(amount + 1).fill(-1)
dp[0] = 0
for (let i = 1; i <= amount; i++) {
for (let j = 0; j < coins.length; j++) {
if (i < coins[j]) continue
if (dp[i - coins[j]] == -1) continue
if (dp[i] == -1 || dp[i] > dp[i - coins[j]] + 1) dp[i] = dp[i - coins[j]] + 1
}
}
return dp[amount]
};
-
- 最长递增子序列
- 递推状态dp(n)指以n位置作为结尾最长递增子序列的长度
- dp[n]的值就取决于在nums里,n位之前所有小于nums[n]的位置,这些位置在dp数组中最小的值再加上1
- 推导出递推公式dp(n) = Math.max(dp[j] + 1),其中j < n, nums[j] < nums[n]
- dp所有位置初始化为1,因为递增子序列至少有一个是自己
var lengthOfLIS = function(nums) {
let n = nums.length, dp = new Array(n).fill(1), ans = 1
for(let i = 1; i < n; i++) {
for(j = 0; j <= i; j++) {
if (nums[i] <= nums[j]) continue
dp[i] = Math.max(dp[i], dp[j] + 1)
}
ans = Math.max(ans, dp[i])
}
return ans
};