%%
# 纲要
> 主干纲要、Hint/线索/路标
# Q&A
#### 已明确
![[00-数据结构与算法笔记/专题总结/动态规划与记忆化搜索#^jk4krx]]
![[00-数据结构与算法笔记/专题总结/动态规划与记忆化搜索#^u6cya8]]
![[00-数据结构与算法笔记/专题总结/动态规划与记忆化搜索#^vi6u2v]]
#### 待明确
> 当下仍存有的疑惑
**❓<font color="#c0504d"> 有什么问题?</font>**
# Buffer
## 闪念
> sudden idea
## 候选资料
> Read it later
%%
# 动态规划与记忆化搜索的区别
> ❓<font color="#c00000">动态规划与记忆化搜索的区别是什么?</font> ^jk4krx
二者都是在 **将 "原问题" 拆分为 "子问题"** 求解的场景下,处理 "**==重叠子问题==**" 的方法,
都是通过 "**存储已求得的子问题结果**" 来**避免重复计算**。
不同之处在于:
- 「记忆化搜索」是在 "**==分治==**" 的求解思想上,通过 "**备忘**" 而对 "**==递归==**" 实现进行优化;
- **自顶向下**:从**顶层问题**开始,**通过 "==分治==" 的思想==递归==求解子问题**,并在**每次递归调用时检查和存储子问题的中间结果**
- 「动态规划」是通过 "**==迭代/递推==**"的方式求解问题
- **自底向上**:从**最小的子问题**开始,**通过==迭代==逐步解决更大的问题**,直到最终解决整个问题
在《算法导论》中提到,"**记忆化搜索**" 本质上即是 "动态规划" 思想的另一种实现方式——**带备忘录的自顶向下递归法**。[^1]
两种方法具有相同的**渐进时间复杂度**,不过在某些情况下,**自顶向下的方式并不需要真正递归地搜索所有子问题(不满足条件时直接跳出),因此可能时间开销更小**。而 dp 的方式,是一定从**最小子问题**开始递推的。
![[00-数据结构与算法笔记/专题总结/分治与递归#^zoo8sz]]
> [!example] 记忆化搜索 + 剪枝可能效率优于 dp
>
> 参见 [[99-题解/leetcode/lc-2501 数组中最长的方波|lc-2501 数组中最长的方波]] 一题,如果用记忆化搜索,由于是**自顶向下递归**,因此不需要先对数据排序,从哪个入口往下求解都行。但如果是用 dp 的话,就必须先排序,然后**从小到大递推求**。
>
> ![[_attachment/00-数据结构与算法笔记/专题总结/动态规划与记忆化搜索.assets/IMG-动态规划与记忆化搜索-6ED456D31D3424F3BA42C4A0082F6185.png|603]]
>
> [!tip] 对于求**方案数**,**到达目的地的不同路径数**,这类问题 "**记忆化搜索**" 或者 "DP" 都可以
>
> - **分治 + 记忆化搜索**:
> - `memo[i][j]` 代表从`(i,j)` 到达目的地的不同路径数量,则其等于**经 `(i,j)` 各个邻居到达目的地**的**路径数总和**。
> - **动态规划**:
> - `dp[i][j]` 代表从起点到达位置 `(i,j)` 的不同路径数量,则其等于**经 `(i,j)` 各个邻居到达`(i,j)`** 的**路径数总和**。
>
<br><br>
# 动态规划(自底向上)
## 动态规划的适应场景
> ❓ <font color="#c00000">如何判断一个问题是否能应用动态规划求解?</font> ^u6cya8
动态规划适用于求解 "**具有==最优子结构==性质**" 和 "**具有==重叠子问题==**" 的 **==最优化==问题**。
1. **==最优子结构性质==**:"**原问题的最优解**" 可以通过其 "**子问题的最优解**" 来构建得到。
2. **==重叠子问题==**:**不同的子问题之间具有公共的子子问题**
"**最优化问题**" 即是指问题**可以有很多可行解**,每个解对应一个值,希望求得的是**具有==最优值==(最大值或最小值)的一个解**(可能有**多个解**能得到**相同的最优值**,只需求得其中一个)
#### 最优子结构
「**最优子结构**」是一种**性质**:
如果**一个问题的最优解可以分解为若干个==子问题的最优解==而构建得到**,则认为**该问题具有最优子结构**,<br>因而可通过 **==递归地==解决子问题来构建整体问题的最优解**。
> [!faq] ❓ <font color="#c00000">"最优子结构" 与 "状态转移方程" 有什么关联?</font>
>
> - 「**最优子结构**」是**关于问题本身的一种性质**,描述了 "**问题的最优解==可以==由子问题的最优解构成**"。
> - 「**状态转移方程**」是**动态规划的具体实现**,描述了 "**==如何==通过子问题的解迭代地推导出更大问题的解**"。
>
> 问题具有 "**最优子结构**" 是能够**应用动态规划求解**、描述出 "**==状态转移方程==**" 的前提。
> ^vi6u2v
> [!example] 示例:求解 A -> B 之间的最短路径
>
> 假设 **A 到 B 一定会经过中间点 C**,则 **A 到 B 的最短路径长度 = A 到 C 的最短路径长度 + C 到 B 的最短路径长度**,
> 由此说明 "**求A-> B 之间的最短路径**" 这一问题**具有最优子结构**。
#### 重叠子问题
**不同的子问题在求解过程中会重复出现**,动态规划即是通过**保存这些子问题的解来避免重复计算**。
<br><br>
## 动态规划方法的核心
应用动态规划方法,需要定义/明确 **两个核心要素**:
- 「**==状态==**」:表示**问题在某一阶段的==解==**,即**一个子问题的解**
- 「**==状态转移方程==**」—— 描述 **==如何==通过已求得的子问题解来==构建==当前问题的解**;
此外,还需要确定 "**==边界条件==**" ——即**最小子问题的解**,是应用动态规划进行 **"递推" 所需的起始项**。
<br>
## 应用动态规划求解问题的步骤
> ❓ <font color="#c00000">如何分析、应用动态规划来具体求解一个最优化问题?</font> ^4v103k
- (1)**判断该问题是否可应用动态规划求解**:
1. ~~是否是求解 "**最优解**" 的问题~~
2. 问题是否具有 "**最优子结构性质**" ——决定了**是否可以应用动态规划**求解
3. 是否存在 "**重叠子问题**" ——决定了需要 "**备忘/存储**" 多少**子问题状态**
- (2)**应用动态规划求解问题**:
1. 定义「**==状态==**」—— 即 `dp[i]` 的定义;
- 确定 **填表方向** (如何填 `dp` 表),分析**是否需要额外边界来 "统一/简化" 状态转移逻辑**,从而确定对状态 `dp[i]` 或 `dp[i][j]` 的定义。
2. 定义「**==状态转移方程==**」——即 `dp[i] -> dp[i+1]` 的递推关系式;
3. 确定「**==边界条件==**」—— 最小子问题的解(`dp[0]` 的值),**递推所需的起始项**;
4. 实现 "**自底向上**" 的递推求解过程
#### 判断 dp 填表的方向
> ❓<font color="#c00000">如何判断应用 dp 时填表的方向?</font>
根据**状态转移方程**来确定:例如:
- `dp[i]` 是**由其前面的项 `dp[i-...]`** 还是**由其后面的项 `dp[i+...]`** 求得?
- `dp[i][j]` 是由其**上方、下方、左侧、还是右侧**的项求得?
递推过程需要**先求得前一项才能推出后一项**,由此可以确定**填表方向**。
<br><br>
%%
## 动态规划的优化
1. 考虑是否需要开额外边界,从而简化对边界条件的处理逻辑,简化代码;
2. 考虑是否可进行**状态压缩**:应用几个**变量**或者**滚动数组**记录前项。
### (1)开额外边界
开辟额外边界的目的:将**求解 「初始项」所需的初始化逻辑** 纳入「 **统一的状态转移方程**」中。
如果状态转移方程中涉及 `dp[i-k]` 或者 `dp[i+k]`,则**对于边界情况 `dp[i == 0]` 或 `dp[i == n-1]` 不能应用状态转移方程求解(下标越界)**,需要特别处理。开额外边界即是为了防止越界,而统一对边界条件的处理逻辑,简化代码。
#### 判断是否需要额外边界
> ❓<font color="#c00000">如何判断是否需要为 dp 数组或 dp 表开辟额外的边界?</font>
如果应用「**状态转移方程**」 求解 「**初始项 `dp[0]` 或 `dp[1]`**」 时 **==超出边界==**(例如需要 `dp[-1]`),则可以 **"==开辟额外边界==" 并 ==重新定义 `dp[i]` 的含义**==。
> [!example] 背包问题
>
> - 定义 `dp[i][j]` 表示从**前 `i` 项物品**(下标`[0,i-1]`)中选取物品**放入容量为 `j` 的背包**所能获得的最大价值。
> - `dp` 数组大小为 `(N+1, W+1)`;
> - 在**物品维度多开一位**,从而用 `dp[0][.]` 表示不取任何物品的情况,这样对于第一项物品 `nums[0]`,不取的情况可以由 "`dp[0][0]`" 转移得到。
> - `dp[0][.]` 表示 "**==不取任何物品==**" 的情况
> - `dp[.][0]` 表示 "**==背包容量为 `0`==**" 的情况。
> [!example] 打家劫舍问题:
>
> - 定义 `dp[i]` 表示偷取 `[0, i]` 区间的房子所能获得的最大金额。
> - 状态转移方程:`dp[i] = max(dp[i-1], dp[i-2] + nums[i])`
>
> 当`i == 0` 和 `i == 1` 时存在越界,因此初始项 `dp[0], dp[1]` 需要单独处理。
> 为此,**==可在 `dp` 数组前部多开两位==**,映射 `dp'[i+2] => dp[i]`,统一实现为:
>
> - 定义`dp[i+2]` 表示偷取 `[0, i]` 区间的房子所能获得的最大金额
> - `dp` 数组长度为 `n+2`
> - 状态转移方程:`dp[i+2] = max(dp[i+1], dp[i] + nums[i])`
> - 初始化 `dp[0] = dp[1] = 0`,无特别含义;
如何定义 dp:
```cpp
// dp[i]表示长为i的s[0, i-1]......, dp[0]表示空,长度为0
vector<int> dp(n + 1, init_val);
// dp[i]表示以s[i]结尾的......,
vector<int> dp(n, init_val);
```
---
似乎**并没有完全省略初始化**,例如 [[99-题解/leetcode/lc-494 目标和|lc-494 目标和]] 中,如果不额外开一位,需要单调地初始化第一行第一列。
### (2)状态压缩
> [!tip]
> 应用状态压缩时(利用**变量**或**滚动数组**),初始**变量值**或**滚动数组值**可以自然地设置为 "**==额外边界==**",
> 从而**省略对初始项的初始化逻辑**,统一纳入 "**状态转移**" 的代码逻辑中求得。
<br><br>
## 动态规划的应用 trick
##### (1)为 dp 数组申请额外空间,处理边界条件
- **多申请一行或一列的空间, 用来处理边界条件**
=> 可省略对`dp[][]` 首行首列的单独初始化, 简化代码。
=> 需要注意循环时的 idx 需要适应地修改, 多出了一行一列。
=》多申请一行一列后,再将二维 dp 压缩成一行后,需要注意每次按行遍历时,首项的初始化;
- 多申请额外空间,再通过 "**下标映射**" 来实现对边界条件的处理;
> [!example] 示例: [[99-题解/leetcode/lc-198 打家劫舍|lc-198 打家劫舍]]
>
> 递推关系式为 `dp[i] = max(dp[i-2] + arr[i], dp[i-1])`,正常来说需要**从 `i>=2` 开始递推**,原因在于:
>
> (1)上述递推式**只在 `i>=2` 成立**;
> (2)需保证否则 `i-2` **下标不越界**;
>
> 在处理时,可**为 dp 数组多开两位** 并进行 **==下标映射==**:**将 `i-2` 映射为=> `i` 从而保证不越界,故取`bias = 2`**,
> 由此得到映射后递推式 `dp[i+bias] = max(dp[i-2+bias] + arr[i], dp[i-1]+bias)`,
> 即`dp[i+2] = max(dp[i] + arr[i], dp[i+1])` =>
>
> 由此,记 **==`dp[i+2]` 表示偷窃 `[0, i]` 所能获得的最大利润==**,而 **`dp[0]` 与 `dp[1]` 则为边界情况,可根据场景均初始化为 0**。
>
> 最后结果返回 `dp[n+1]` 即表示偷窃 `[0, n]` 所能获得的最大利润。
---
%%
%%
1. 一维 `dp[]` 的状态转移,可考虑用一个或多个变量来实现。
例如 f (n) 只与 f (n-1) 和 f (n-2) 有关, 则可以只用三个变量实现。
二维 `dp[][]` 的状态转移,可考虑只用一行数组来实现(滚动数组)。
=> 多行压缩成一行后,每一行第一列可以需要根据条件单独处理;
%%
<br><br><br>
# 动态规划问题分类
## 背包问题
> 背包类问题的核心:"**选**" 或者 "**不选**" 的思想。
![[_attachment/00-数据结构与算法笔记/专题总结/动态规划与记忆化搜索.assets/IMG-动态规划与记忆化搜索-54F6F721E991EE3D53F89FFE491B265A.png|621]]
![[Excalidraw-Solution/动态规划与记忆化搜索.excalidraw.svg|419]]
%%[[Excalidraw-Solution/动态规划与记忆化搜索.excalidraw.md|🖋 Edit in Excalidraw]]%%
### 0-1 背包问题
给定 `N` 个物品(每种物品 **==只有一个==**),每个物品 `i` 具有两个属性:**重量 `weight[i]`** 和 **价值`value[i]`**。
求一个容量为 `W` 的**背包所能装下的最大物品价值**。
### 完全背包问题
给定 `N` 种物品(**每种物品有==无限个==**),每个物品 `i` 具有两个属性:**重量 `weight[i]`** 和 **价值`value[i]`**。
求一个容量为 `W` 的**背包所能装下的最大物品价值**。
### 多重背包问题
给定 `N` 种物品(**每种==物品的数量由 `cnt[i]` 给定==**),每个物品 `i` 具有两个属性:**重量 `weight[i]`** 和 **价值`value[i]`**。求一个容量为 `W` 的**背包所能装下的最大物品价值**。
### 分组背包问题
**物品分为 `N` 组**,**==每一组中有若干种不同物品==**,要求**只能从每一组中选择一个物品**。
每个物品 `i` 具有两个属性:**重量 `weight[i]`** 和 **价值`value[i]`**。
求一个容量为 `W` 的**背包所能装下的最大物品价值**。
%%
## 线性 DP
## 区间 DP
## 树形 DP
%%
<br><br><br>
# 参考资料
# Footnotes
[^1]: 《算法导论》P207