Loading docs/dp/memo.md +111 −63 Original line number Diff line number Diff line # 聊聊动态规划与记忆化搜索 by $\color{Gray}InterestingLSY$ (菜到发灰) by $\color{Gray}{InterestingLSY}$ (菜到发灰) > 想体验把暴搜改改就是正解的快感吗? 想体验状压 dp 看似状态多到爆炸实际一跑却嗷嗷快 (实际有效的状态数很少) 的荣耀吗? 记忆化搜索, 符合您的需求! 只要 998 , 记忆化搜索带回家! 记忆化搜索, 记忆化搜索, 再说一遍, 记忆化搜索! --- ## 1. 记忆化搜索是啥 Loading Loading @@ -32,7 +36,7 @@ int main(){ ``` 这就是个十分智障的大暴搜是吧 ...... emmmmmm....... $\color{Red}30$ 分 emmmmmm....... $\color{Red}{30}$ 分 然后我心血来潮, 想不借助任何 "外部变量"(就是 dfs 函数外且 ** 值随 dfs 运行而改变的变量 **), 比如 ans Loading Loading @@ -67,7 +71,7 @@ int main(){ } ``` ~~emmmmmm....... 还是 $\color{Red}30$ 分~~ ~~emmmmmm....... 还是 $\color{Red}{30}$ 分~~ 但这个时候, 我们的程序已经不依赖任何外部变量了. Loading Loading @@ -122,19 +126,35 @@ int main(){ - 不依赖任何 ** 外部变量 ** - 答案以返回值的形式存在, 而不能以参数的形式存在(就是不能将 dfs 定义成 $dfs(pos ,tleft , nowans )$, 这里面的 nowans 不符合要求. - 答案以返回值的形式存在, 而不能以参数的形式存在(就是不能将 dfs 定义成 $dfs(pos ,tleft , nowans )$, 这里面的 nowans 不符合要求). - 对于相同一组参数, dfs 返回值总是相同的 --- ## 2. 记忆化搜索与动态规划的关系: ~~ 基本是朋 (ji) 友关系~~ 有人会问: 记忆化搜索难道不是搜索? 是搜索. 但个人认为她更像 dp : 不信你看 $mem$ 的意义: > 在时间 $tleft$ 内采集 ** 后 ** $pos$ 个草药, 能获得的最大收益 这不就是 dp 的状态? 时间复杂度 / 空间复杂度与 ** 不加优化的 dp** 完全相同 由上面的代码中可以看出: 不管定义咋扯, 反正我觉得 > $mem[pos][tleft] = max(mem[pos+1][tleft-tcost[pos]]+mget[pos]\ ,\ mem[pos+1][tleft])$ > 记忆化搜索就是动态规划,**(印象中)任何一个 dp 方程都能转为记忆化搜索 ** 这不就是 dp 的状态转移? 个人认为: > 记忆化搜索约等于动态规划,**(印象中)任何一个 dp 方程都能转为记忆化搜索 ** 大部分记忆化搜索的状态 / 转移方程与 dp 都一样, 时间复杂度 / 空间复杂度与 ** 不加优化的 ** dp 完全相同 比如: Loading @@ -155,6 +175,18 @@ int main(){ } ``` --- ## 3. 如何写记忆化搜索 ### 方法 I: 1. 把这道题的 dp 状态和方程写出来 2. 根据他们写出 dfs 函数 3. 添加记忆化数组 举例: $dp[i] = max\{dp[j]+1\}\quad 1 \leq j < i \text{且}a[j]<a[i]$ (最长上升子序列) 转为 Loading @@ -174,40 +206,53 @@ int main(){ cout << dfs(n) << endl; } ``` ### 方法 II: **当然, 以我的经验更多情况下记忆化搜索是写完暴力 dfs(本来想骗分)后突然发现能改成记忆化搜索** ~~ 然后 AC 了一道全场没几个人会的超难 dp~~ 1. 写出这道题的暴搜程序(最好是 [dfs](/search/dfs) ) 2. 将这个 dfs 改成 "无需外部变量" 的 dfs 3. 添加记忆化数组 感受以下那种发现自己写的暴力改改就是正解的快感吧! 举例: 本文最开始介绍 "什么是记忆化搜索" 时举的 "采药" 那题的例子 啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈! --- ## 3. 记忆化搜索的优缺点 咳咳, 说正事 ## 4. 记忆化搜索的优缺点 优点: - 记忆化搜索可以避免搜到无用状态, 特别是在有状态压缩时 - 边界情况非常好处理, 且能有效防止数组访问越界 举例: 给你一个有向图(注意不是完全图), 经过每条边都有花费, 求从点 1 出发, 经过每个点 ** 恰好一次 ** 后的最小花费(最后不用回到起点), 保证路径存在. dp 状态很显然: 设 $dp[pos][mask]$ 表示身处在 $pos$ 处, 走过 $mask$ ( mask 为一个二进制数) 中的顶点后的最小花费 常规 $dp$ 的状态为 $O(n\cdot 2^n)$ , 转移复杂度 (所有的加在一起) 为 $O(m)$ - ~~ 写起来简单易懂~~ 至少我镇么认为 qwq 但是! 如果我们用记忆化搜索, 就可以避免到很多无用的状态, 比如 $pos$ 为起点却已经经过了 $>1$ 个点的情况. - 不需要注意转移顺序(这里的 "转移顺序" 指正常 dp 中 for 循环的嵌套顺序以及循环变量是递增还是递减) 举例: 用常规 dp 写 "合并石子" 需要先枚举区间长度然后枚举起点, 但记忆化搜索直接枚举断点 (就是枚举当前区间由哪两个区间合并而成) 然后递归下去就行 - 边界情况非常好处理, 且能有效防止数组访问越界 - 有些 dp ( 如区间 dp )用记忆化搜索写很简单但正常 dp 很难 - 记忆化搜索天生携带搜索天赋, 可以使用技能 "剪枝"! 缺点: - 致命伤: 不能滚动数组!(哪位 dalao 会记搜 + 滚动的请在评论区留名) - 致命伤: 不能滚动数组! - 有些优化比较难加 - 由于递归, 有时效率较低但不至于 TLE - 由于递归, 有时效率较低但不至于 TLE (状压 dp 除外) - 代码有点长~~ 其实也不算太长~~ --- ## 4. 记忆化搜索的注意事项: ## 5. 记忆化搜索的注意事项 - 千万别忘了加记忆化! (别笑, 认真的 Loading @@ -215,6 +260,9 @@ int main(){ - 数组不要开小了(逃 ## 如有疑问或质疑, 请留下评论或私信我 --- ** 如有疑问或质疑, 请留下评论或私信我 ** ** questions are welcome ** Loading
docs/dp/memo.md +111 −63 Original line number Diff line number Diff line # 聊聊动态规划与记忆化搜索 by $\color{Gray}InterestingLSY$ (菜到发灰) by $\color{Gray}{InterestingLSY}$ (菜到发灰) > 想体验把暴搜改改就是正解的快感吗? 想体验状压 dp 看似状态多到爆炸实际一跑却嗷嗷快 (实际有效的状态数很少) 的荣耀吗? 记忆化搜索, 符合您的需求! 只要 998 , 记忆化搜索带回家! 记忆化搜索, 记忆化搜索, 再说一遍, 记忆化搜索! --- ## 1. 记忆化搜索是啥 Loading Loading @@ -32,7 +36,7 @@ int main(){ ``` 这就是个十分智障的大暴搜是吧 ...... emmmmmm....... $\color{Red}30$ 分 emmmmmm....... $\color{Red}{30}$ 分 然后我心血来潮, 想不借助任何 "外部变量"(就是 dfs 函数外且 ** 值随 dfs 运行而改变的变量 **), 比如 ans Loading Loading @@ -67,7 +71,7 @@ int main(){ } ``` ~~emmmmmm....... 还是 $\color{Red}30$ 分~~ ~~emmmmmm....... 还是 $\color{Red}{30}$ 分~~ 但这个时候, 我们的程序已经不依赖任何外部变量了. Loading Loading @@ -122,19 +126,35 @@ int main(){ - 不依赖任何 ** 外部变量 ** - 答案以返回值的形式存在, 而不能以参数的形式存在(就是不能将 dfs 定义成 $dfs(pos ,tleft , nowans )$, 这里面的 nowans 不符合要求. - 答案以返回值的形式存在, 而不能以参数的形式存在(就是不能将 dfs 定义成 $dfs(pos ,tleft , nowans )$, 这里面的 nowans 不符合要求). - 对于相同一组参数, dfs 返回值总是相同的 --- ## 2. 记忆化搜索与动态规划的关系: ~~ 基本是朋 (ji) 友关系~~ 有人会问: 记忆化搜索难道不是搜索? 是搜索. 但个人认为她更像 dp : 不信你看 $mem$ 的意义: > 在时间 $tleft$ 内采集 ** 后 ** $pos$ 个草药, 能获得的最大收益 这不就是 dp 的状态? 时间复杂度 / 空间复杂度与 ** 不加优化的 dp** 完全相同 由上面的代码中可以看出: 不管定义咋扯, 反正我觉得 > $mem[pos][tleft] = max(mem[pos+1][tleft-tcost[pos]]+mget[pos]\ ,\ mem[pos+1][tleft])$ > 记忆化搜索就是动态规划,**(印象中)任何一个 dp 方程都能转为记忆化搜索 ** 这不就是 dp 的状态转移? 个人认为: > 记忆化搜索约等于动态规划,**(印象中)任何一个 dp 方程都能转为记忆化搜索 ** 大部分记忆化搜索的状态 / 转移方程与 dp 都一样, 时间复杂度 / 空间复杂度与 ** 不加优化的 ** dp 完全相同 比如: Loading @@ -155,6 +175,18 @@ int main(){ } ``` --- ## 3. 如何写记忆化搜索 ### 方法 I: 1. 把这道题的 dp 状态和方程写出来 2. 根据他们写出 dfs 函数 3. 添加记忆化数组 举例: $dp[i] = max\{dp[j]+1\}\quad 1 \leq j < i \text{且}a[j]<a[i]$ (最长上升子序列) 转为 Loading @@ -174,40 +206,53 @@ int main(){ cout << dfs(n) << endl; } ``` ### 方法 II: **当然, 以我的经验更多情况下记忆化搜索是写完暴力 dfs(本来想骗分)后突然发现能改成记忆化搜索** ~~ 然后 AC 了一道全场没几个人会的超难 dp~~ 1. 写出这道题的暴搜程序(最好是 [dfs](/search/dfs) ) 2. 将这个 dfs 改成 "无需外部变量" 的 dfs 3. 添加记忆化数组 感受以下那种发现自己写的暴力改改就是正解的快感吧! 举例: 本文最开始介绍 "什么是记忆化搜索" 时举的 "采药" 那题的例子 啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈! --- ## 3. 记忆化搜索的优缺点 咳咳, 说正事 ## 4. 记忆化搜索的优缺点 优点: - 记忆化搜索可以避免搜到无用状态, 特别是在有状态压缩时 - 边界情况非常好处理, 且能有效防止数组访问越界 举例: 给你一个有向图(注意不是完全图), 经过每条边都有花费, 求从点 1 出发, 经过每个点 ** 恰好一次 ** 后的最小花费(最后不用回到起点), 保证路径存在. dp 状态很显然: 设 $dp[pos][mask]$ 表示身处在 $pos$ 处, 走过 $mask$ ( mask 为一个二进制数) 中的顶点后的最小花费 常规 $dp$ 的状态为 $O(n\cdot 2^n)$ , 转移复杂度 (所有的加在一起) 为 $O(m)$ - ~~ 写起来简单易懂~~ 至少我镇么认为 qwq 但是! 如果我们用记忆化搜索, 就可以避免到很多无用的状态, 比如 $pos$ 为起点却已经经过了 $>1$ 个点的情况. - 不需要注意转移顺序(这里的 "转移顺序" 指正常 dp 中 for 循环的嵌套顺序以及循环变量是递增还是递减) 举例: 用常规 dp 写 "合并石子" 需要先枚举区间长度然后枚举起点, 但记忆化搜索直接枚举断点 (就是枚举当前区间由哪两个区间合并而成) 然后递归下去就行 - 边界情况非常好处理, 且能有效防止数组访问越界 - 有些 dp ( 如区间 dp )用记忆化搜索写很简单但正常 dp 很难 - 记忆化搜索天生携带搜索天赋, 可以使用技能 "剪枝"! 缺点: - 致命伤: 不能滚动数组!(哪位 dalao 会记搜 + 滚动的请在评论区留名) - 致命伤: 不能滚动数组! - 有些优化比较难加 - 由于递归, 有时效率较低但不至于 TLE - 由于递归, 有时效率较低但不至于 TLE (状压 dp 除外) - 代码有点长~~ 其实也不算太长~~ --- ## 4. 记忆化搜索的注意事项: ## 5. 记忆化搜索的注意事项 - 千万别忘了加记忆化! (别笑, 认真的 Loading @@ -215,6 +260,9 @@ int main(){ - 数组不要开小了(逃 ## 如有疑问或质疑, 请留下评论或私信我 --- ** 如有疑问或质疑, 请留下评论或私信我 ** ** questions are welcome **