当前位置: 首页 > backend >正文

算法思维进阶 力扣 375.猜数字大小 II 暴力递归 记忆化搜索 DFS C++详细算法解析 每日一题

目录

  • 零、题目描述
  • 一、为什么这道题值得深入理解?
  • 二、题目拆解:用“决策树”理解核心矛盾
  • 三、思路演进:从暴力递归到记忆化搜索
  • 四、代码详解:逐行理解记忆化搜索的实现
  • 五、示例推演:用n=3理解记忆化过程
  • 六、复杂度分析与优势
    • 时间复杂度
    • 空间复杂度
    • 与暴力递归的对比
  • 七、坑点总结:实现时的注意事项
  • 八、举一反三:为下一篇做铺垫
  • 九、总结

零、题目描述

题目链接:力扣 375. 猜数字大小 II

在这里插入图片描述

如果感觉没明白题目的意思,我们可以换个角度,把这个问题理解成“提前准备一笔钱,确保无论对方选1到n中的哪个数字,你都能通过最优的猜测顺序把它猜出来,这笔钱最少需要多少”。

为什么要强调“无论对方选哪个数字”?因为你没法预知对方的选择,所以必须考虑最极端的情况——比如你猜了A,对方说“小了”;你再猜B,对方说“大了”……这个过程中花的钱可能有多有少,但你必须保证:哪怕遇到花钱最多的那条猜测路径,你手里的钱也足够用。而我们要找的,就是能覆盖所有可能情况的“最小启动资金”。

用两个例子直观感受:

举个直观的例子:

  • 当 n=2 时,花的最少的钱是 1。

    • 如果你先猜 1:猜对了不花钱;猜错了(实际是 2),花 1 元就能再猜到 2。最坏花费 1 元。
    • 如果你先猜 2:猜错了(实际是 1),要花 2 元再猜 1。最坏花费 2 元。
    • 所以最小确保金额是 1。
  • 当 n=3 时,答案是 2。

    • 先猜 2:猜错了(无论是 1 还是 3),只需再猜一次即可,总花费 2 元。
    • 先猜 1 或 3:最坏情况要花 4 元(比如猜 1 错了,实际是 3,总花费 1+3=4)。
    • 所以最优选择是先猜 2,最小确保金额为 2。

简单说就是:你要站在“最倒霉”的角度,选一条“花钱最少”的万能策略——我们不应管对方选的是什么数字,你都能靠这笔钱稳稳赢下游戏,而这笔钱还不能多花一分。

代码框架:

class Solution {
public:int getMoneyAmount(int n) {}
};

一、为什么这道题值得深入理解?

猜数字大小 II 是区间类记忆化搜索的经典代表,其价值体现在三个方面:

  • “最坏情况最优解”的思维训练:题目要求“确保能猜到的最小金额”,本质是在“最坏情况”(即每次都遇到最不利的数字)下寻找“最小成本”,这种“最大化最小值”的决策逻辑是很多博弈问题、风险控制问题的核心;
  • 区间子问题的拆解艺术:问题天然可拆解为“左区间”和“右区间”的子问题,理解这种“区间分割”的递归逻辑,能直接迁移到矩阵链乘法、石子游戏等区间DP问题;
  • 记忆化搜索的典型应用:暴力递归的时间复杂度高达O(n!),而通过记忆化搜索可将复杂度降至O(n³),这种“用空间换时间”的优化思路在此题中体现得淋漓尽致,能帮你掌握“识别重复子问题”的关键能力。

下一篇我们将讲解的「329. 矩阵中最长递增路径」同样依赖记忆化搜索解决“重复子问题”,提前吃透这道题的思路,能为后续学习打下坚实基础。

二、题目拆解:用“决策树”理解核心矛盾

要解决这道题,首先需明确“确保能猜到的最小金额”的含义。我们可以用决策树来可视化过程:

  • 每个节点代表一次猜测(值为所猜数字);
  • 左子树表示“实际数字比猜测值小”,右子树表示“实际数字比猜测值大”;
  • 从根节点到叶子节点(正确数字)的路径之和,就是此次猜测的总花费;
  • 我们需要找到一棵“根节点的最大路径和最小”的决策树——即“最坏情况下花费最小”的策略。

以 n=3 为例,决策树如下:

方案1:根节点猜2├─ 小于于目标数,左子树(那么答案是1):无需再猜,路径和=2└─ 大于目标数,右子树(那么答案是3):无需再猜,路径和=2最坏情况花费=2(取最大值)方案2:根节点猜1├─ 小于于目标数,左子树:无(1是最小)└─ 大于目标数,右子树(实际是2或3)├─需猜2(就可以知道答案了,是3),花费和是=1+2=3└─需猜3(就可以知道答案了,是2),花费和是=1+3=4最坏情况花费=4(取最大值)方案3:根节点猜3├─ 小于目标数,左子树(实际是2或1)|						  ├─需猜2(就可以知道答案了,是1),花费和是=3+2=5|				          └─需猜1(就可以知道答案了,是2),花费和是=3+1=4└─ 大于目标数,右子树:无(3是最大)最坏情况花费=5(取最大值)在根节点提供的情况中取花费最小的情况,就是根节点为2花费2为最终结果

核心矛盾:暴力枚举所有可能的猜测顺序会导致指数级复杂度(每个区间有O(n)种猜测选择,子区间又有O(n)种,递归深度O(n)),必须通过“存储子区间的计算结果”来避免重复计算。

三、思路演进:从暴力递归到记忆化搜索

1. 暴力递归:枚举所有可能的猜测点
核心思路:对于区间[left, right],尝试每一个可能的猜测点i(left ≤ i ≤ right),计算“猜i的花费 + 左右子区间的最坏情况花费”,取所有i中的最小值。

  • 子问题定义:dfs(left, right) 表示“猜中[left, right]之间的数字,确保能猜到的最小金额”;
  • 递归逻辑:dfs(left, right) = min( i + max(dfs(left, i-1), dfs(i+1, right)) ) (i从left到right遍历);
    • i 是当前猜的数字(需支付i元);
    • max(dfs(left, i-1), dfs(i+1, right)) 是“最坏情况”(左右子区间中花费更大的那个);
  • 边界条件:当left ≥ right时(区间内只有0或1个数字),无需猜测,花费为0。

暴力递归代码(超时但思路关键):

class Solution {
public:int getMoneyAmount(int n) {return dfs(1, n);}// 计算区间[left, right]的最小确保金额int dfs(int left, int right) {// 区间为空或只有一个数字,无需猜测if (left >= right) return 0;int minCost = INT_MAX;// 尝试每个可能的猜测点ifor (int i = left; i <= right; i++) {// 猜i的花费 = i + 左右子区间的最坏情况(取较大者)int cost = i + max(dfs(left, i-1), dfs(i+1, right));// 取所有猜测点中的最小花费minCost = min(minCost, cost);}return minCost;}
};

为什么超时?
以n=4为例,dfs(1,4) 会计算 dfs(1,0)dfs(2,4)dfs(1,1)dfs(3,4) 等子问题,而 dfs(2,4) 又会计算 dfs(2,1)dfs(3,4)——其中 dfs(3,4) 被重复计算。随着n增大,重复子问题呈指数级增长,时间复杂度高达O(n!),n=20时就已无法承受。

2. 记忆化搜索:用备忘录存储重复子问题
优化核心:区间[left, right]的最小花费是固定的,无论它是从哪个父区间拆分而来,结果都相同。因此,我们可以用一个二维数组 memo[left][right] 存储计算过的结果,避免重复递归。

步骤升级

  1. 定义备忘录 memo[left][right],初始值为0(表示未计算);
  2. 计算 dfs(left, right) 前,先检查 memo[left][right]
    • 若不为0,直接返回(已计算);
    • 若为0,计算后存入 memo[left][right] 再返回。

记忆化搜索代码

class Solution {
public:int memo[201][201];  // 备忘录:存储区间[left][right]的最小确保金额int getMoneyAmount(int n) {// 初始化备忘录(全局变量默认0,无需额外初始化)return dfs(1, n);}int dfs(int left, int right) {// 边界条件:区间为空或只有一个数字,花费0if (left >= right) return 0;// 若已计算,直接返回备忘录中的结果if (memo[left][right] != 0) return memo[left][right];int minCost = INT_MAX;// 尝试所有可能的猜测点ifor (int i = left; i <= right; i++) {// 计算左右子区间的花费int leftCost = dfs(left, i-1);int rightCost = dfs(i+1, right);// 当前猜测i的总花费 = i + 左右子区间的最坏情况(取较大者)int currentCost = i + max(leftCost, rightCost);// 取所有猜测点中的最小花费minCost = min(minCost, currentCost);}// 存入备忘录memo[left][right] = minCost;return minCost;}
};

四、代码详解:逐行理解记忆化搜索的实现

1. 备忘录设计

int memo[201][201];
  • 尺寸选择:题目中n最大为200,区间left和right的范围是1~200,因此201×201的数组足够覆盖所有可能的区间;
  • 初始值:全局数组默认初始化为0,而有效结果(除边界条件)均为正数,因此0可安全表示“未计算”。

2. 递归函数 dfs(left, right)

int dfs(int left, int right) {if (left >= right) return 0;  // 边界:无需猜测if (memo[left][right] != 0) return memo[left][right];  // 读取备忘录
  • 边界条件:当left ≥ right时,区间内最多只有一个数字,无需猜测即可确定,花费0;
  • 备忘录检查:若已计算过该区间,直接返回结果,避免重复递归。

3. 核心计算逻辑

int minCost = INT_MAX;
for (int i = left; i <= right; i++) {int leftCost = dfs(left, i-1);  // 左子区间花费int rightCost = dfs(i+1, right);  // 右子区间花费int currentCost = i + max(leftCost, rightCost);  // 最坏情况花费minCost = min(minCost, currentCost);  // 取最小的最坏情况
}
  • 遍历所有可能的猜测点i:每个i将区间拆分为[left, i-1]和[i+1, right];
  • 计算最坏情况:因为我们需要“确保能猜到”,必须考虑左右子区间中花费更大的那个(即最坏情况),再加上当前猜测i的花费;
  • 取最小值:在所有可能的i中,选择“最坏情况花费最小”的那个,即为当前区间的最优解。

4. 存入备忘录并返回

memo[left][right] = minCost;
return minCost;
  • 将计算结果存入备忘录,供后续调用直接使用,完成“空间换时间”的优化。

五、示例推演:用n=3理解记忆化过程

以n=3为例,演示记忆化搜索的执行流程:

  1. 调用 dfs(1,3)memo[1][3] 为0,进入计算;
  2. 遍历i=1,2,3:
    • i=1:
      • 左子区间 dfs(1,0) → 0(边界);
      • 右子区间 dfs(2,3) → 首次计算:
        • i=2:左 dfs(2,1)=0,右 dfs(3,3)=0 → 花费2+0=2;
        • i=3:左 dfs(2,2)=0,右 dfs(4,3)=0 → 花费3+0=3;
        • dfs(2,3) 取min(2,3)=2,存入 memo[2][3]=2
      • 当前花费:1 + max(0, 2) = 3;
    • i=2:
      • 左子区间 dfs(1,1)=0,右子区间 dfs(3,3)=0
      • 当前花费:2 + max(0, 0) = 2;
    • i=3:
      • 左子区间 dfs(1,2) → 首次计算(过程类似,结果为1);
      • 当前花费:3 + max(1, 0) = 4;
  3. minCost 取min(3,2,4)=2,存入 memo[1][3]=2,返回2。

可见,memo[2][3]memo[1][2] 等子区间的结果被存储后,若后续其他父区间需要调用,可直接读取,大幅减少计算量。

六、复杂度分析与优势

时间复杂度

  • 记忆化搜索中,每个区间[left, right]只计算一次,区间总数为O(n²)(left和right各有n种可能);
  • 每个区间的计算需要遍历O(n)个猜测点i;
  • 总时间复杂度:O(n³),n=200时为200³=8,000,000,完全可接受。

空间复杂度

  • 备忘录数组大小为O(n²)(201×201≈40,000);
  • 递归栈深度为O(n)(最坏情况区间从1到n,递归深度n);
  • 总空间复杂度:O(n²)。

与暴力递归的对比

维度暴力递归记忆化搜索
时间复杂度O(n!)O(n³)
空间复杂度O(n)(递归栈)O(n²)(备忘录+栈)
核心优化点存储重复子问题结果

七、坑点总结:实现时的注意事项

  1. 备忘录的尺寸与初始化

    • 若n最大为200,备忘录需至少为201×201(因为数字从1开始),避免数组越界;
    • 初始值需与“有效结果”区分:若用0表示未计算,需确保有效结果不为0(此题中只有边界条件为0,其他情况均为正数,因此安全)。
  2. 循环遍历的范围

    • 猜测点i必须遍历[left, right]的所有可能值,不能遗漏。例如n=3时若漏掉i=2,会错误地得到4作为结果。
  3. “最坏情况”的取法

    • 必须用 max(leftCost, rightCost) 而非 min,因为我们需要确保“无论实际数字在左还是右,都有足够的钱完成猜测”。取max才能覆盖最坏情况。
  4. 递归边界的判断

    • left >= right 时返回0,涵盖了left > right(空区间)和left = right(单数字区间)两种情况,无需额外拆分。

八、举一反三:为下一篇做铺垫

这道题的记忆化搜索思路可直接迁移到「329. 矩阵中最长递增路径」:

  • 两者都存在大量重复子问题(矩阵中每个单元格的最长路径 vs 区间[left, right]的最小花费);
  • 都需要用备忘录存储子问题结果(二维数组 memo);
  • 递归逻辑都依赖“当前状态的解 = 子状态的解的组合”(此题是min(i + max(…)),329题是max(上下左右路径+1))。

提前掌握“识别重复子问题→设计备忘录→递归计算+存储”的流程,能帮你快速解决下一篇的问题。

九、总结

「猜数字大小 II」的核心是通过记忆化搜索解决“区间拆分导致的重复子问题”:

  • 暴力递归因重复计算子问题,复杂度高达O(n!),无法实用;
  • 记忆化搜索用二维备忘录存储区间[left, right]的结果,将复杂度降至O(n³),实现高效求解;
  • 关键思路是“对每个区间尝试所有可能的猜测点,取最坏情况下的最小花费”,体现了“最大化最小值”的决策逻辑。

下一篇,我们将运用同样的记忆化搜索思想,解决力扣 329. 矩阵中最长递增路径,进一步巩固“用备忘录优化递归”的核心能力。

最后欢迎大家在评论区分享你的代码或思路,咱们一起交流探讨~ 🌟 要是有大佬有更精妙的思路或想法,恳请在评论区多多指点批评,我一定会虚心学习,并且第一时间回复交流哒!
在这里插入图片描述这是封面原图~ 喜欢的话先点个赞鼓励一下呗~ 再顺手关注一波,后续更新不迷路,保证让你看得过瘾!😉
在这里插入图片描述

http://www.xdnf.cn/news/16526.html

相关文章:

  • K8s集群两者不同的对外暴露服务的方式
  • React--》实现 PDF 文件的预览操作
  • [2025CVPR-图象分类]ProAPO:视觉分类的渐进式自动提示优化
  • ubuntu22.04 安装 petalinux 2021.1
  • B+树高效实现与优化技巧
  • 微信小程序私密消息
  • 聚铭安全管家平台2.0实战解码 | 安服篇(三):配置保障 自动核查
  • yolov11的简单实例
  • 【密码学】4. 分组密码
  • 关闭 UniGetUI 自动 Pip 更新,有效避免 Anaconda 环境冲突教程
  • Python Pandas.merge函数解析与实战教程
  • 软件测试之功能测试
  • Ubuntu系统完整配置教程
  • unbuntn 22.04 coreutils文件系统故障
  • RabbitMQ快速入门
  • 基于FPGA和DDS原理的任意波形发生器(含仿真)
  • 【Unity】Application类常见路径一览表
  • 基于LangGraph Cli的智能数据分析助手
  • 主要分布于内侧内嗅皮层的层Ⅲ的网格-速度联合细胞(Grid × Speed Conjunctive Cells)对NLP中的深层语义分析的积极影响和启示
  • OpenCV(05)直方图均衡化,模板匹配,霍夫变换,图像亮度变换,形态学变换
  • nvim cspell
  • 基于 OpenCV 与 sklearn 的数字识别:KNN 算法实践
  • 123页PPT麦肯锡49个思维工具和方法论PPT
  • 一个典型的微控制器MCU包含哪些模块?
  • Java Collections工具类
  • 达梦有多少个模式
  • 页面性能优化
  • Java基础-IO流
  • 【灰度实验】——图像预处理(OpenCV)
  • 商用车的自动驾驶应用场景主要包括七大领域