leetcode374&375. 猜数字大小总结

leetcode374. 猜数字大小(二分的坑)

题目:猜数字游戏的规则如下:
每轮游戏,系统都会从 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  死循环了。

leetcode375. 猜数字大小 II

题目:我们正在玩一个猜数游戏,游戏规则如下:
我从 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];
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值