算法-动态规划

173 阅读3分钟

一、必读文章——dp解题套路框架

动态规划问题的一般形式就是求最值。比如最长递增子序列、最小编辑距离等等。

  • 求解动态规划的核心问题是穷举,动态规划三要素:

    • 这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
    • 而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
    • 只有列出正确的「状态转移方程」,才能正确地穷举。
  • 状态转移方程框架:明确 base case -> 明确「状态」 -> 明确「选择」 -> 定义 dp 数组/函数的含义

1.1、fib数列

非dp问题,只具有重叠子问题特性

使用「备忘录」的递归

使用「备忘录」,把使用暴力递归解法时,复杂度为O(n^2)的递归调用树,降低为复杂度为O(n)的树。

实际上,这种解法和迭代的动态规划已经差不多了,只不过这种方法叫做「自顶向下」,动态规划叫做「自底向上」。

啥叫「自顶向下」?注意我们刚才画的递归树(或者说图),是从上向下延伸,都是从一个规模较大的原问题比如说 f(20),向下逐渐分解规模,直到 f(1)f(2) 这两个 base case,然后逐层返回答案,这就叫「自顶向下」。

啥叫「自底向上」?反过来,我们直接从最底下,最简单,问题规模最小的 f(1)f(2) 开始往上推,直到推到我们想要的答案 f(20),这就是动态规划的思路,这也是为什么动态规划一般都脱离了递归,而是由循环迭代完成计算

使用「DP table」的迭代

优化DP table

根据斐波那契数列的状态转移方程,当前状态只和之前的两个状态有关,其实并不需要那么长的一个 DP table 来存储所有的状态,只要想办法存储之前的两个状态就行了。所以,可以进一步优化,把空间复杂度降为 O(1)。

这个技巧就是所谓的*「状态压缩」*。

1.2、凑零钱问题

如何列出正确的状态转移方程

1、确定 base case,确定最基础的情形,一般是题目给出的 / 不用计算可以一眼看出来的。

2、确定「状态」,也就是原问题和子问题中会变化的变量

3、确定「选择」,也就是导致「状态」产生变化的行为。做了一个选择,会导致状态变化;不做选择,延续原来的状态。

4、明确 dp 数组 / 函数的定义

二、子序列类型问题

1、一维dp数组

dp[i] 一般表示以 nums[i] 这个数结尾的目标子序列的长度。

最长递增子序列

求解连续数组;一维dp数组

最大子数组

求解连续数组;一维dp数组;状态压缩

涉及两个字符串/数组时(比如最长公共子序列、编辑距离),dp 数组的含义如下:

在子数组arr1[0..i]和子数组arr2[0..j]中,我们要求的子序列(最长公共子序列)长度为dp[i][j]

只涉及一个字符串/数组时(比如最长回文子序列),dp 数组的含义如下:

在子数组array[i..j]中,我们要求的子序列(最长回文子序列)的长度为dp[i][j]

2、二维dp数组 - 单字符串

最长回文子序列

解题模板

求解离散序列;二维dp数组

3、二维dp数组 - 双字符串

最长公共子序列

对于两个字符串求子序列的问题,都是用两个指针ij分别在两个字符串上移动,大概率是动态规划思路