347,猜数字大小 II
想了解更多数据结构以及算法题,可以关注微信公众号“数据结构和算法”,每天一题为你精彩解答。也可以扫描下面的二维码关注
我们正在玩一个猜数游戏,游戏规则如下:
我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。
每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。
然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。
示例:
n = 10, 我选择了8. 第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。
游戏结束。8 就是我选的数字。
你最终要支付 5 + 7 + 9 = 21 块钱。
给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏。
答案:
public static int getMoneyAmount(int n) {
return DP(1, n);
}
public static int DP(int left, int right) {
if (left >= right)
return 0;
int res = Integer.MAX_VALUE;
for (int i = left; i <= right; i++) {
int tmp = i + Math.max(DP(left, i - 1), DP(i + 1, right));
res = Math.min(res, tmp);
}
return res;
}
解析:
在数字1-n之间,假如我们选择了x,数组就会分成3个部分,[1,x-1],x,[x+1,n]。那么会有3种情况,第一种我们选中了,所以这时候花费现金最少,第二种是我们选大了,第三种是我们选小了,用函数f(m,n)表示从数字m到n中所花费的最小金额,如果选择了在范围(i,j)中保证所花费最少(i<=x<=j),我们有下面这样一个公式。money=x+max(f(i,x-1)+f(x+1,j));代码第10行我们找的是最大值,因为题目说了至少花费多钱现金才能赢得游戏,我们只需要找到所有的花费最大值中的最小值即可。但是上面代码效率明显不是很高,因为递归的原因会出现重复计算,我们只需要用一个临时数组存储每次计算的结果,防止重复计算即可,我们来优化一下
public static int getMoneyAmount(int n) {
int[][] table = new int[n + 1][n + 1];
return DP(table, 1, n);
}
public static int DP(int[][] table, int left, int right) {
if (left >= right)
return 0;
if (table[left][right] != 0)
return table[left][right];
int res = Integer.MAX_VALUE;
for (int i = left; i <= right; i++) {
int tmp = i + Math.max(DP(table, left, i - 1), DP(table, i + 1, right));
res = Math.min(res, tmp);
}
table[left][right] = res;
return res;
}
除了递归的写法以外,我们还可以使用动态规划的方式解决,先看一下代码
public int getMoneyAmount(int n) {
int[][] table = new int[n + 1][n + 1];
for (int i = 2; i <= n; i++) {
for (int j = i - 1; j > 0; j--) {
int globalMin = Integer.MAX_VALUE;
for (int k = j + 1; k < i; k++) {
int localMax = k + Math.max(table[j][k - 1], table[k + 1][i]);
globalMin = Math.min(globalMin, localMax);
}
table[j][i] = j + 1 == i ? j : globalMin;
}
}
return table[1][n];
}
前两个for循环会组成一个封闭的空间[j,i],然后第3个for循环再从这个封闭的空间中找出所有花费最大值的最小值即可,这个最小值也只不过是在区间[j,i]之间的,然后通过外面两层的for循环最终会找出区间[1,n]的值。第10行表示如果j和i仅仅相差1的话,那么第3个for循环根本就不会执行,我们要猜最小的才能花费最少,所以选择j。下面来画个图加深一下理解
这里我们来举个例子简单说下,比如n等于5,为什么只需要6块钱就一定能赢得游戏,结合上面的分析,我们知道当我们先猜2,或者4的时候结果都是6,我们以先猜2来分析一下,
1, 如果选择的是1,我们猜2,说明大了,下一步直接猜1就行了,所以只花了2块钱。
2, 如果选择的是2,我们猜2,说明猜对了,一分没花。
3, 如果选择的是3,我们猜2,说明猜小了,下一步猜4,说明大了,在下一步直接猜3,猜对了,所以总共花了2+4=6块钱。
4, 如果选择的是4,我们猜2,说明猜小了,下一步猜4,猜中了,只花了2块钱。
5, 如果选择的是5,我们猜2,说明猜小了,下一步猜4,又小了,在下一步直接猜5,猜对了,所以总共花了2+4=6块钱。
综上所述,当n等于5的时候,我们只需要6块钱就一定能赢,
思考:
这题我估计很多人都听说过,或者看过类似的这种题,很多时候我们猜这样的题都喜欢从中间来猜,显然通过上面的分析,如果我们从中间猜不一定会有最优的结果,比如当n=5的时候,当我们选择4,或者5的时候,如果我们从中间来猜,先猜3,小了,再猜4,这时候无论是猜对了还是猜小了,所花费的肯定是大于6的,很明显不是最优解。