跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

动态规划基础:树型 DFS、回溯与记忆化搜索

动态规划问题可通过回溯、记忆化搜索结合树型 DFS 求解。以爬楼梯、打家劫舍、完全平方数及零钱兑换为例,解析递归的自顶向下与自底向上写法,阐述记忆化数组原理及实现细节,对比不同递归策略在求解最优解时的差异与优化路径。

内存管理发布于 2026/3/15更新于 2026/10/24108 浏览
动态规划基础:树型 DFS、回溯与记忆化搜索

1. 基础与示范

回溯和记忆化搜索是动态规划的基础,本文只讲回溯 + 记忆化解决动态规划问题。要记住任何的回溯都应看作树,所以动态规划也可以当作树来求解。实际上动态规划的推导步骤就是:

  • 回溯 --> 回溯 + 记忆化 --> 递推公式。

递归有自顶向下和自底向上两种写法,但是自底向上更适合记忆化,因为:

  • 自顶向下过程中常用全局变量和参数传递信息,常常无返回值,记忆化数组的原理是当前节点到递归终止的最优解。当记忆到这个最优解时,当前路径的**'递'这一过程就结束了,如果还需要'归'**,那么递归函数就需要返回值,就成为有递有归或自底向上了。

记忆化的核心与原理就是:

  • 当前从某个子节点(一般是叶子结点)'归'到某个根节点这个路径的信息已经有了,其他子节点归到某个根节点时,可能也会用到同样的信息,于是直接拿来用跳过'归'的过程。

下面从递归 + 回溯到记忆化搜索进行基础例题讲解。

1.1 70.爬楼梯 自顶向下

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

文章配图

该题是非常基础的题,我们单纯将其当作树进行处理:

  • 每个节点下面有两个节点,分别是走一步和走两步
  • 递归终止条件是走到了 n
  • 没有显示恢复现场,是因为我们用自顶向下传递当前走到的步数,直接在参数中进行更改
  • **有递无归:**根节点是 0 表示从第 0 步开始,叶子结点是 n 表示走到了第 n 步
class Solution { public: int climbStairs(int n) { int ans = 0; auto dfs = [&](this auto&& dfs, int cur) -> void { if(cur == n){ ans += 1; return; } if(cur > n) return; dfs(cur + 1); dfs(cur + ); }; ();  ans; } };
2
dfs
0
return

1.2 70.爬楼梯 自底向上

当然也可以用自底向上:

  • 每个节点不停向上返回,返回的是走到当前这一步的方案总数
  • **归到根节点得出答案:**根节点是 n,叶子结点是 0
  • 走到当前这一步的方案总数是前一个台阶和前两个台阶的方案总数之和
  • 终止条件,也就是走到叶子节点,是到 0
  • 注意:自底向上不是指根节点和叶子节点表示的含义与自顶向下相反,而是指递归中求出答案的过程是在递的过程还是归的过程
  • 但是一般情况下,将目标作为根节点,0 作为叶子节点的写法确实都是自底向上
  • 无论自顶向下还是自底向上,这个结果在大部分测试用例中超时。
class Solution { public: int climbStairs(int n) { auto dfs = [&](this auto&& dfs, int cur) -> int { if(cur == 0){ return 1; } if(cur < 0) return 0; int sum1 = dfs(cur-1); int sum2 = dfs(cur-2); return sum1 + sum2; }; return dfs(n); } };

1.3 70.爬楼梯 自底向上 + 记忆化

为什么没有自顶向下 + 记忆化?可以有,但是逻辑会很复杂,因为:

  • 记忆化需要知道"从位置 cur 到终点 n 有多少种走法"
  • 如果是 int 返回类型,函数可以直接返回这个值:return dfs(cur+1) + dfs(cur+2)
  • 如果是 void 类型,函数无法告诉你这个值,只能通过副作用(修改 ans)间接计算
class Solution { public: int climbStairs(int n) { vector mem(n + 1, -1); auto dfs = [&](this auto&& dfs, int cur) -> int { if(cur == 0){ return 1; } if(cur < 0) return 0; if(mem[cur] != -1) return mem[cur]; int sum1 = dfs(cur-1); int sum2 = dfs(cur-2); mem[cur] = sum1 + sum2; return sum1 + sum2; }; return dfs(n); } };

2. 198.打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下,一夜之内能够偷窃到的最高金额。

文章配图

2.1 自顶向下

对于当前的节点(房屋),有两个子节点:

  • 选择当前房屋的钱,然后到下一个房屋,
  • 放弃当前房屋的钱,然后到下下一个房屋。

总结:

  • **根节点:**第一个房屋
  • **叶子结点:**最后一个房屋或倒数第二个房屋
  • **子节点:**下一个房屋或下下个房屋
class Solution { public: int rob(vector<int>& nums) { int n = nums.size(); int ans = INT_MIN; int path = 0; auto dfs = [&](this auto&& dfs, int cur) -> void { if(cur >= n){ ans = max(ans, path); return; } path += nums[cur]; dfs(cur + 2); path -= nums[cur]; dfs(cur + 1); }; dfs(0); return ans; } };

2.2 自底向上

  • **根节点:**最后一个房屋
  • **子节点:**前一个房屋或前前个房屋
  • **叶子结点:**第一个房屋或第二个房屋
class Solution { public: int rob(vector<int>& nums) { int n = nums.size(); auto dfs = [&](this auto&& dfs, int cur) -> int { if(cur < 0) return 0; int sum1 = dfs(cur - 1); int sum2 = dfs(cur - 2) + nums[cur]; int sum = max(sum1, sum2); return sum; }; return dfs(n - 1); } };

2.3 记忆化

  • **根节点:**最后一个房屋
  • **子节点:**前一个房屋或前前个房屋
  • **叶子结点:**第一个房屋或第二个房屋.
  • **记忆:**从叶子节点到当前节点的最大和(多个叶子结点都表示第一个或第二个房屋,叶子节点不同,但是不同叶子节点代表的元素相同,可以理解为指针不同但值相同,每次从叶子节点返回时,已经有其他叶子节点返回过了,这时就可以复用值)
class Solution { public: int rob(vector<int>& nums) { int n = nums.size(); vector<int> mem(n, -1); auto dfs = [&](this auto&& dfs, int cur) -> int { if(cur < 0) return 0; if(mem[cur] != -1) return mem[cur]; int sum1 = dfs(cur - 1); int sum2 = dfs(cur - 2) + nums[cur]; int sum = max(sum1, sum2); mem[cur] = sum; return sum; }; return dfs(n - 1); } };

3. 279.完全平方数

给你一个整数 n,返回 和为 n 的完全平方数的最少数量。

完全平方数是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

文章配图

3.1 自顶向下

  • **根节点:**1
  • **叶子结点:**路径和为 n 时的某个数
  • **子节点:**从 1 开始到 sqrt(n - path) 内所有整数
  • **终止条件:**当前路径总和 > 目标和
  • 执行动作:计算当前路径 + 子节点值的路径和
class Solution { public: int numSquares(int n) { int curCount = 0; int path = 0; int ans = INT_MAX; auto dfs = [&](this auto&& dfs, int cur) -> void { if(path == n){ ans = min(curCount, ans); return; } if(path > n) return; for(int i = 1; i <= sqrt(n - path); i++){ curCount++; path += i*i; dfs(i); curCount--; path -= i*i; } }; dfs(1); return ans == INT_MAX ? 0 : ans; } };

3.2 自底向上

  • **根节点:**表示和为 n
  • **叶子结点:**0
  • 子节点:从 1 开始到 sqrt(n - path) 内所有整数构建的和
  • 终止条件:'递'到叶子节点,也就是和为 0
class Solution { public: int numSquares(int n) { auto dfs = [&](this auto&& dfs, int sum) -> int { if(sum == 0) return 0; if(sum < 0) return -1; int minCount = INT_MAX; for(int i = 1; i <= sqrt(sum); i++){ int count = dfs(sum - i * i); int curCount = count + 1; if(curCount > 0){ minCount = min(minCount, curCount); } } return minCount == INT_MAX ? -1 : minCount; }; return dfs(n); } };

3.3 记忆化

  • **根节点:**n
  • **叶子结点:**0
  • 子节点:从 1 开始到 sqrt(n - path) 内所有整数构建的和
  • 终止条件:'递'到叶子节点,也就是和为 0
  • 记忆:从和为 0 的叶子结点到当前节点的所需完全平方数的最少数量(多个子节点值可能相同,如 3 下面有 1,4 下面有 1,2,从 0 到 1 这段路径就会被复用)
class Solution { public: int numSquares(int n) { vector<int> mem(n + 1, -1); auto dfs = [&](this auto&& dfs, int sum) -> int { if(sum == 0) return 0; if(sum < 0) return -1; if(mem[sum] != -1) return mem[sum]; int minCount = INT_MAX; for(int i = 1; i <= sqrt(sum); i++){ int count = dfs(sum - i * i); int curCount = count + 1; if(curCount > 0){ minCount = min(minCount, curCount); } } if(minCount == INT_MAX){ return -1; } mem[sum] = minCount; return minCount; }; return dfs(n); } };

4. 322.零钱兑换

给你一个整数数组 coins,表示不同面额的硬币;以及一个整数 amount,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

文章配图

4.1 自顶向下

  • **根节点:**第一个硬币
  • **叶子节点:**不定,什么时候 和 > amount,谁就是叶子节点
  • **子节点:**所有硬币

或者:

  • **根节点:**和为 0
  • **叶子节点:**和为 amount
  • **子节点:**各个硬币构建的和

意思相同,可以当作一个结构体中多个不同字段

class Solution { public: int coinChange(vector<int>& coins, int amount) { int n = coins.size(); int ans = INT_MAX; long long path = 0; int curNum = 0; auto dfs = [&](this auto&& dfs, int cur) -> void { if(path == amount){ ans = min(ans, curNum); return; } if(path > amount) return; for(int i = 0; i < n; i++){ path += coins[i]; curNum++; dfs(i); path -= coins[i]; curNum--; } }; dfs(0); if(ans == INT_MAX) return -1; return ans; } };

4.2 自底向上

  • 根节点:amount
  • 叶子节点:0
  • 子节点:所有硬币构建的和
class Solution { public: int coinChange(vector<int>& coins, int amount) { int n = coins.size(); auto dfs = [&](this auto&& dfs, int curSum, int cur) -> int { if(curSum == 0) return 0; if(curSum < 0) return -1; int minConis = INT_MAX; for(int i = 0; i < n; i++){ int res = dfs(curSum - coins[i], i); if(res != -1) minConis = min(minConis ,res + 1); } return minConis == INT_MAX ? -1 : minConis; }; return dfs(amount, 0); } };

4.3 记忆化搜索

  • 根节点:amount
  • 叶子节点:0
  • 子节点:所有硬币构建的和
  • 记忆:构建到当前节点代表的和时需要的最少硬币数量
class Solution { public: int coinChange(vector<int>& coins, int amount) { int n = coins.size(); vector<int> mem(amount + 1, -2); auto dfs = [&](this auto&& dfs, int curSum, int cur) -> int { if(curSum == 0) return 0; if(curSum < 0) return -1; if(mem[curSum] != -2) return mem[curSum]; int minConis = INT_MAX; for(int i = 0; i < n; i++){ int res = dfs(curSum - coins[i], i); if(res != -1) minConis = min(minConis ,res + 1); } mem[curSum] = minConis == INT_MAX ? -1 : minConis; return minConis == INT_MAX ? -1 : minConis; }; return dfs(amount, 0); } };

目录

  1. 1. 基础与示范
  2. 1.1 70.爬楼梯 自顶向下
  3. 1.2 70.爬楼梯 自底向上
  4. 1.3 70.爬楼梯 自底向上 + 记忆化
  5. 2. 198.打家劫舍
  6. 2.1 自顶向下
  7. 2.2 自底向上
  8. 2.3 记忆化
  9. 3. 279.完全平方数
  10. 3.1 自顶向下
  11. 3.2 自底向上
  12. 3.3 记忆化
  13. 4. 322.零钱兑换
  14. 4.1 自顶向下
  15. 4.2 自底向上
  16. 4.3 记忆化搜索

更多推荐文章

查看全部
  • 大模型时代下的私有数据安全与利用
  • Python 网络爬虫基础与通用模板实现
  • 新版 QQ NT 桌面版内存优化实践
  • TikTok 用户涌入背景下,小红书是否应布局大模型技术
  • 无人机视觉目标检测数据集 VisDrone 介绍与数据格式转换
  • 基于 Java Swing 的个人所得税计算模拟器实现
  • 零基础入门网络安全的学习路线与实战指南
  • 数据结构实战:双向链表原理与 C 语言实现
  • AI产品经理核心能力与行业落地全景指南
  • AI 产品经理如何构建金融场景大模型产品架构
  • Linux 高频面试题与详细解析
  • 为什么选择 Python:核心特性与优势解析
  • GitHub Copilot 配合 Figma MCP 还原设计稿生成前端代码
  • Vue3 前端专属配置(VSCode settings.json + .prettierrc)
  • Android 开发者如何突破困境:深耕 Framework 实现职业进阶
  • ComfyUI 按需付费部署与成本优化方案
  • Claude Code vs GitHub Copilot CLI 深度评测与选型指南
  • Linux 部署 wvp-GB28181-pro 及接入上级国标平台方案
  • 发那科机器人与西门子 PLC 通讯方案:网关与 Modbus TCP 配置及代码
  • Prompt 驱动结构化抽取:从文本高效提取表格

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online