动态规划的一般形式就是求最值。

动态规划的核心设计思想是数学归纳法。

核心问题是穷举

重叠子问题、最优子结构、状态转移方程就是动态规划三要素

最难的就是写状态转移方程
状态转移方程思考框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。

递归算法的时间复杂度计算:用子问题个数乘以解决一个子问题需要的时间。

为什么叫状态转移方程?
f(n) 的函数参数会不断变化,所以你把参数 n 想做一个状态,这个状态 n 是由状态 n - 1 和状态 n - 2 转移(相加)而来,这就叫状态转移,仅此而已。

斐波纳奇数列问题有助于我们理解重叠子问题。

要符合「最优子结构」,子问题间必须互相独立

如果题目出现

  • 要求你给出达成某个目的的解法个数
  • 不要求你给出每一种解法对应的具体路径

基于动态规划的思想来做题,我们首先要想到的思维工具就是“倒着分析问题”。“倒着分析问题”分两步走:

  1. 定位到问题的终点
  2. 站在终点这个视角,思考后退的可能性

动态规划,是一个自底向上的过程。它要求我们站在已知的角度,通过定位已知和未知之间的关系,一步一步向前推导,进而求解出未知的值。

爬楼梯

  1. 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
  2. 每次你可以爬 1 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
  3. 示例 1
  4. 输入:n = 2
  5. 输出:2
  6. 解释:有两种方法可以爬到楼顶。
  7. 1. 1 + 1
  8. 2. 2
  9. 示例 2
  10. 输入:n = 3
  11. 输出:3
  12. 解释:有三种方法可以爬到楼顶。
  13. 1. 1 + 1 + 1
  14. 2. 1 + 2
  15. 3. 2 + 1
  16. 提示:
  17. 1 <= n <= 45
  18. 来源:力扣(LeetCode
  19. 链接:https://leetcode.cn/problems/climbing-stairs
  20. 著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

方法一:

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;
}