1.动态规划
总结: 解决动态规划问题最难的地方有两点:
如何定义 f(n)f(n)
如何通过 f(1)f(1), f(2)f(2), … f(n - 1)f(n−1) 推导出 f(n)f(n),即状态转移方程
1)线性动态规划
单串 dp[i] 线性动态规划最简单的一类问题,输入是一个串,状态一般定义为 dp[i] := 考虑[0..i]上,原问题的解,其中 i 位置的处理,根据不同的问题,主要有两种方式:
第一种是 i 位置必须取,此时状态可以进一步描述为 dp[i] := 考虑[0..i]上,且取 i,原问题的解;
第二种是 i 位置可以取可以不取
大部分的问题,对 i 位置的处理是第一种方式,例如力扣:
70 爬楼梯问题
801 使序列递增的最小交换次数
790 多米诺和托米诺平铺
746 使用最小花费爬楼梯
线性动态规划中单串 dp[i] 的问题,状态的推导方向以及推导公式如下
1. 依赖比 i 小的 O(1) 个子问题
dp[n] 只与常数个小规模子问题有关,状态的推导过程 dp[i] = f(dp[i - 1], dp[i - 2], ...)。时间复杂度 O(n)O(n),空间复杂度 O(n)O(n) 可以优化为 O(1)O(1),例如上面提到的 70, 801, 790, 746 都属于这类。
如图所示,虽然紫色部分的 dp[i-1], dp[i-2], ..., dp[0] 均已经计算过,但计算橙色的当前状态时,仅用到 dp[i-1],这属于比 i 小的 O(1)O(1) 个子问题。
2. 依赖比 i 小的 O(n) 个子问题
dp[n] 与此前的更小规模的所有子问题 dp[n - 1], dp[n - 2], ..., dp[1] 都可能有关系。
状态推导过程如下:
dp[i] = f(dp[i - 1], dp[i - 2], ..., dp[0])
依然如图所示,计算橙色的当前状态 dp[i] 时,紫色的此前计算过的状态 dp[i-1], ..., dp[0] 均有可能用到,在计算 dp[i] 时需要将它们遍历一遍完成计算。
其中 f 常见的有 max/min,可能还会对 i-1,i-2,...,0 有一些筛选条件,但推导 dp[n] 时依然是 O(n)O(n) 级的子问题数量。
以 min 函数为例,这种形式的问题的代码常见写法如下
for i = 1, ..., n
for j = 1, ..., i-1
dp[i] = min(dp[i], f(dp[j]))
2)前缀和
3)区间动态规划
4)背包动态规划
5)状态压缩动态规划
6)计数问题
7)矩阵快速幂
8)数位动态规划
2.递归
3.字符串
4.哈希表
5.二分查找
6.深度优先搜索
7.广度优先搜索
8.排序