【记忆化搜索】猜数字游戏Ⅱ
文章目录
- 375. 猜数字大小 II
- 解题思路:暴搜 -> 记忆化搜索

375. 猜数字大小 II
375. 猜数字大小 II
我们正在玩一个猜数游戏,游戏规则如下:
- 我从
1
到n
之间选择一个数字。 - 你来猜我选了哪个数字。
- 如果你猜到正确的数字,就会 赢得游戏 。
- 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
- 每当你猜了数字
x
并且猜错了的时候,你需要支付金额为x
的现金。如果你花光了钱,就会 输掉游戏 。
给你一个特定的数字 n
,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。
示例 1:
输入:n = 10
输出:16
解释:制胜策略如下:
- 数字范围是 [1,10] 。你先猜测数字为 7 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $7 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [8,10] 。你可以猜测数字为 9 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $9 。
- 如果我的数字更大,那么这个数字一定是 10 。你猜测数字为 10 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,那么这个数字一定是 8 。你猜测数字为 8 并赢得游戏,总费用为 $7 + $9 = $16 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,6] 。你可以猜测数字为 3 。
- 如果这是我选中的数字,你的总费用为 $7 。否则,你需要支付 $3 。
- 如果我的数字更大,则下一步需要猜测的数字范围是 [4,6] 。你可以猜测数字为 5 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $5 。
- 如果我的数字更大,那么这个数字一定是 6 。你猜测数字为 6 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,那么这个数字一定是 4 。你猜测数字为 4 并赢得游戏,总费用为 $7 + $3 + $5 = $15 。
- 如果我的数字更小,则下一步需要猜测的数字范围是 [1,2] 。你可以猜测数字为 1 。
- 如果这是我选中的数字,你的总费用为 $7 + $3 = $10 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $7 + $3 + $1 = $11 。
在最糟糕的情况下,你需要支付 $16 。因此,你只需要 $16 就可以确保自己赢得游戏。
示例 2:
输入:n = 1
输出:0
解释:只有一个可能的数字,所以你可以直接猜 1 并赢得游戏,无需支付任何费用。
示例 3:
输入:n = 2
输出:1
解释:有两个可能的数字 1 和 2 。
- 你可以先猜 1 。
- 如果这是我选中的数字,你的总费用为 $0 。否则,你需要支付 $1 。
- 如果我的数字更大,那么这个数字一定是 2 。你猜测数字为 2 并赢得游戏,总费用为 $1 。
最糟糕的情况下,你需要支付 $1 。
提示:
1 <= n <= 200
解题思路:暴搜 -> 记忆化搜索
首先这道题先搞清楚题意很重要,虽然我们很习惯性的会去想二分的猜数字,但是在这道题二分不是最优解,因为还得结合节点的值,二分只是确保平均的查找路径是最短的,但是不能确保拿到的是最小现金数,也就是说,这道题其实就是一个暴搜!
但是因为递归树太麻烦了这道题,有太多的路径,所以下面我们将其抽象一下,就得到下图:
其中左右子树是有很多路径的,该图只是简化了,在左右子树还得继续暴力枚举对应区间上的路径可能!
那么既然是暴搜,我们就 让 dfs()
函数帮我们拿到能够确保获胜的最小现金数,因为涉及到区间分割,所以要传两个参数 left
和 right
作为区间的左右边界控制一下,然后返回值就是整型,不过很容易想到!
此时当我们选了 i
元素之后,那么对于 i
元素来说,它的左右子树拿到的就是各自能获胜的最小现金数,此时细节来了,对于 i
整棵子树来说,此时的最小现金数应该是左右子树各自的最小现金数中的最大值!
这是因为左右子树拿到的最小现金数是保证左右子树各自能获胜而使用的最小现金数,但是对于 i
元素来说,是要看整体的,无论是走左边还是右边都必须要赢,所以就必须选择大的那个才符合要求,如下图所示:
代码如下所示,最好结合代码和上面的解释一起理解:
class Solution {
public:
int getMoneyAmount(int n) {
return dfs(1, n);
}
int dfs(int left, int right)
{
// 函数递归出口(left==right的时候说明肯定是找到了,此时也是返回0,不需要支付金额)
if(left >= right)
return 0;
int ret = INT_MAX; // 记录最小现金数
for(int i = left; i <= right; ++i) // 枚举区间所有元素的路径
{
int x = dfs(left, i - 1);
int y = dfs(i + 1, right);
ret = min(ret, i + max(x, y)); // 记得左右子树的最大值要加上i才算当前的现金数,然后再更新最小现金数
}
return ret;
}
};
虽然思路正确,但是这道题暴搜仍然会超时!
很明显,因为是暴搜,不同子树之间存在大量重复的区间,所以我们可以用记忆化搜索优化,将这些出现过的区间的最小现金数记录到备忘录中,下次进入函数前先判断备忘录是否有记录该区间的最小现金数,有的话直接返回即可,大大的提高了效率!
另外初始化备忘录的问题,其实我们 直接初始化为 0
即可,因为出现结果为 0
的情况只有在函数递归出口,所以我们可以将备忘录的判断放到函数递归出口的下面进行判断即可,省去了我们去初始化的操作!
class Solution {
private:
int memory[201][201]; // 二维数组作为备忘录,元素值表示[i, j]区间的最小现金数
public:
int getMoneyAmount(int n) {
return dfs(1, n);
}
int dfs(int left, int right)
{
// 函数递归出口
if(left >= right)
return 0;
// 进入函数前先判断备忘录是否有记录该区间的最小现金数,有的话直接返回
if(memory[left][right] != 0)
return memory[left][right];
int ret = INT_MAX; // 记录最小现金数
for(int i = left; i <= right; ++i)
{
int x = dfs(left, i - 1);
int y = dfs(i + 1, right);
ret = min(ret, i + max(x, y)); // 记得左右子树的最大值要加上当前的i才算当前的现金数,然后再更新最小现金数
}
// 出函数之前,将该区间的结果记录下来
memory[left][right] = ret;
return ret;
}
};