题目:猜数字游戏的规则如下:
每轮游戏,系统都会从 1 到 n 随机选择一个数字。 请你猜选出的是哪个数字。
如果你猜错了,系统会告诉你,你猜测的数字比系统选出的数字是大了还是小了。
你可以通过调用一个预先定义好的接口 guess(int num)
来获取猜测结果,
返回值一共有 3 种可能的情况(-1,1 或 0):
-1 : 你猜测的数字比系统选出的数字大
1 : 你猜测的数字比系统选出的数字小
0 : 恭喜!你猜对了!
输入: n = 10, pick = 6
输出: 6
public class Solution extends GuessGame {
public int guessNumber(int n) {
int start = 1, end = n;
while (start < end) {
int mid = ((end - start) >> 1) + start;
if (guess(mid) == 0) return mid; //note1
else if (guess(mid) == -1) end = mid - 1;
else start = mid + 1;// note2
}
return start;
}
}
注意二分的坑!!!!!!!!!!!
注意,因为上面有直接返回的语句(note1),说明下一次二分的时候,mid不能取!!
所以 note2 这里start = mid + 1; 而不是start = mid;
否则会死循环:比如 n=10,pick=6
在第二次循环之后, start=5, end=6; mid一直是5, start 也会继续被设置成5
这样start=5 end = 6 死循环了。
题目:我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。
每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。
然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。
n = 10, 我选择了8.
第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。
游戏结束。8 就是我选的数字。
你最终要支付 5 + 7 + 9 = 21 块钱。
给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏。
首先是怎么才确保能赢呢?就是说不管你从哪个数开始猜起,都必须把对方的数给“猜没了”。
所以不要去考虑对方到底选了哪个数,而是考虑你从哪个数开始猜。
而你每一次猜都去考虑最坏的情况,这样才能考虑到带最少的钱还能确保赢。
比如说n = 5,当你第一轮猜2的时候,肯定没猜中,然后再考虑向左边还是右边猜,那怎么选择呢?
向哪边猜花费的更多就向哪边猜!
而当对面就剩一个数让你选择的时候,是不可能猜不中的,这一步就不需要掏钱,游戏结束。
理解这一点很重要。在这个基础上,再去选择猜数的策略来达到最小值。
顺着这个思路其实很自然而然的就有了一个递归,
首先选定任意一个值作为起点,然后将整个区间一分为二,依次递归下去,
求出这个值为对应的“确保费用”。遍历区间,选出最小的“确保费用”。
但是递归的效率太低了。用动态规划:
设置一个dp数组,dp[i][j] 表示从数字 i 到 j ,保证猜中所选数字所需的最小花费。
在数字 i 到 j 之间进行猜测时,我们选择数字 i < k < j,则花费为:
k + max(dp[i][k-1], dp[k+1][j])
也就是说,花费是 k 加上其左右两段中的最大者,这样才能保证猜中数字。
所以,遍历从 i 到 j 之间的数字k,得到最小的花费,即是 dp[i][j] 的值。
就这样依次更新记录在 dp 数组中,就可以不超时。
所以,其实不是二分,而是n分;二分是每个序列中间开始分,n分是序列的每个数都当成隔板来分一次。
dp[i][j]表示从[i,j]中猜出正确数字所需要的最少花费金额.(dp[i][i] = 0)
假设在范围[i,j]中选择x, 则选择x的最少花费金额为:
max(dp[i][x-1], dp[x+1][j]) + x
用max的原因是我们要计算最坏反馈情况下的最少花费金额(选了x之后, 正确数字落在花费更高
dp初始化为(n+2)*(n+2)数组的原因: 处理边界情况更加容易, (类似于戳气球那道题的首尾冗余)
例如对于求解dp[1][n]时:
x如果等于1, 需要考虑dp[0][1](0不可能出现, dp[0][n]为0)
x如果等于n时, 需要考虑dp[n+1][n+1](n+1也不可能出现, dp[n+1][n+1]为0)
相应的代码更新dp矩阵:
dp[i][j] = min(dp[i][j],max(dp[i][x-1], dp[x+1][j]) + x)), x~[i:j],
1. x的for循环(代码里是k的循环)这里为什么i和j都要取呢?
因为x代表在当前[i,j]区内选中的数,如果不包含i,j 说明在这个区间判断的时候,没有选择i,j,
会导致情况不完整,dp[i][j]更新错误, 显然是不对的。
2. 关于i和j的遍历方向:
不用管x,只看行列的依赖关系。比如遍历到x的时候,对于列,依赖的是x-1,所以列索引j从小到大
同样的,对于行,以来的是x+1,说明行索引i需要从大到小遍历。
这里题干要求的最少,其实就是我们在最坏的情况下(一直选花费多的那半边试探),所以用max比较x左右区间
因为这样才能保证我们一定可以猜到。
但是比较dp[i][j]的时候,因为是对于[i,j]内所有x的情况都考虑了,在这个基础上,取最小值,所以外面用min
public int getMoneyAmount(int n) {
if (n == 1) return 0;
int[][] dp = new int[n+2][n+2];
for (int i = n; i >= 1; --i) {//注意 i 下界是1不是0
for (int j = i; j <= n; ++j) {
if (i == j) {
dp[i][j] = 0;//可以省略,这里为了使得逻辑清晰
}else {
dp[i][j] = Integer.MAX_VALUE;
for (int x = i; x <= j; ++x) {// 这里为什么取到i和j,见上面1
dp[i][j] = Math.min(dp[i][j],
Math.max(dp[i][x-1], dp[x+1][j]) + x);
}
}
}
}
return dp[1][n];
}