动态规划的一般形式就是求最值。
动态规划的核心设计思想是数学归纳法。
核心问题是穷举
重叠子问题、最优子结构、状态转移方程就是动态规划三要素
最难的就是写状态转移方程
状态转移方程思考框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
递归算法的时间复杂度计算:用子问题个数乘以解决一个子问题需要的时间。
为什么叫状态转移方程?
f(n) 的函数参数会不断变化,所以你把参数 n 想做一个状态,这个状态 n 是由状态 n - 1 和状态 n - 2 转移(相加)而来,这就叫状态转移,仅此而已。
斐波纳奇数列问题有助于我们理解重叠子问题。
要符合「最优子结构」,子问题间必须互相独立
如果题目出现
- 要求你给出达成某个目的的解法个数
- 不要求你给出每一种解法对应的具体路径
基于动态规划的思想来做题,我们首先要想到的思维工具就是“倒着分析问题”。“倒着分析问题”分两步走:
- 定位到问题的终点
- 站在终点这个视角,思考后退的可能性
动态规划,是一个自底向上的过程。它要求我们站在已知的角度,通过定位已知和未知之间的关系,一步一步向前推导,进而求解出未知的值。
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?示例 1:输入:n = 2输出:2解释:有两种方法可以爬到楼顶。1. 1 阶 + 1 阶2. 2 阶示例 2:输入:n = 3输出:3解释:有三种方法可以爬到楼顶。1. 1 阶 + 1 阶 + 1 阶2. 1 阶 + 2 阶3. 2 阶 + 1 阶提示:1 <= n <= 45来源:力扣(LeetCode)链接:https://leetcode.cn/problems/climbing-stairs著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
方法一:
var climbStairs = function(n) {
const f = []
f[1] = 1
f[2] = 2
for (let i = 3; i <= n; i++) {
f[i] = f[i - 1] + f[i - 2]
}
return f[n]
};
方法二:
var climbStairs = function (n) {
if (n < 3) return n
let dp = new Array(n)
dp[1] = 1
dp[2] = 2
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2]
}
return dp[n]
};
最少硬币数目
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
示例 4:
输入:coins = [1], amount = 1
输出:1
示例 5:
输入:coins = [1], amount = 2
输出:2
提示:
1 <= coins.length <= 12
1 <= coins[i] <= 231 - 1
0 <= amount <= 104
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/gaM7Ch
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
方法一:
var coinChange = function(coins, amount) {
const f = []
f[0] = 0
for (let i = 1; i <= amount; i++) {
f[i] = Infinity
for (let j = 0; j < coins.length; j++) {
if (i - coins[j] >= 0) {
f[i] = Math.min(f[i], (f[i - coins[j]]) + 1)
}
}
}
if (f[amount] === Infinity) {
return -1
}
return f[amount]
};
方法二:
var coinChange = function (arr, target) {
var dp = new Array(target + 1).fill(Infinity);
dp[0] = 0;
for (var i of arr) {
for (var j = i; j <= target; j++) {
dp[j] = Math.min(dp[j], dp[j - i] + 1);
}
}
return dp[target] == Infinity ? -1 : dp[target];
};
最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1
提示:
1 <= nums.length <= 2500
-104 <= nums[i] <= 104
进阶:
你能将算法的时间复杂度降低到 O(n log(n)) 吗?
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/longest-increasing-subsequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
function lengthOfLIS(nums) {
const dp = new Array(nums.length).fill(1);
for (let i = 0; i < nums.length; i++) {
for (let j = 0; j < i; j++) {
if (nums[i] > nums[j]) dp[i] = Math.max(dp[i], dp[j] + 1);
}
}
let res = Math.max(...dp)
return res;
}
俄罗斯套娃信封问题
给你一个二维整数数组 envelopes ,其中 envelopes[i] = [wi, hi] ,表示第 i 个信封的宽度和高度。
当另一个信封的宽度和高度都比这个信封大的时候,这个信封就可以放进另一个信封里,如同俄罗斯套娃一样。
请计算 最多能有多少个 信封能组成一组“俄罗斯套娃”信封(即可以把一个信封放到另一个信封里面)。
注意:不允许旋转信封。
示例 1:
输入:envelopes = [[5,4],[6,4],[6,7],[2,3]]
输出:3
解释:最多信封的个数为 3, 组合为: [2,3] => [5,4] => [6,7]。
示例 2:
输入:envelopes = [[1,1],[1,1],[1,1]]
输出:1
提示:
1 <= envelopes.length <= 105
envelopes[i].length == 2
1 <= wi, hi <= 105
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/russian-doll-envelopes
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
var maxEnvelopes = function(envelopes) {
const n = envelopes.length;
envelopes.sort((a, b) => {
return a[0] == b[0] ? b[1] - a[1] : a[0] - b[0];
})
const height = new Array(n);
for (let i = 0; i < n; i++) height[i] = envelopes[i][1];
return lengthOfLIS(height);
};
function lengthOfLIS(nums) {
const top = new Array(nums.length);
let piles = 0;
for (let i = 0; i < nums.length; i++) {
let poker = nums[i];
let left = 0,
right = piles;
while (left < right) {
let mid = Math.floor(left + (right - left) / 2);
if (top[mid] > poker) {
right = mid;
} else if (top[mid] < poker) {
left = mid + 1;
} else {
right = mid;
}
}
if (left == piles) piles++;
top[left] = poker;
}
return piles;
}
