题目
给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。
已知存在楼层 f ,满足 0 <= f <= n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。
每次操作,你可以取一枚没有碎的鸡蛋并把它从任一楼层 x 扔下(满足 1 <= x <= n)。如果鸡蛋碎了,你就不能再次使用它。如果某枚鸡蛋扔下后没有摔碎,则可以在之后的操作中 重复使用 这枚鸡蛋。
请你计算并返回要确定 f 确切的值 的 最小操作次数 是多少?
示例1
输入:k = 1, n = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,肯定能得出 f = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,肯定能得出 f = 1 。
如果它没碎,那么肯定能得出 f = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 f 是多少。
示例2
输入: k = 2, n = 6
输出: 3
示例3
输入: k = 3, n = 14
输出: 4
题解
分析
假设1
假设有1个鸡蛋,100层楼;需要多少次?
需要100次,因为只能从1层,2层,3层这么一层一层的试试
假设2
有2个鸡蛋,100层楼;需要多少次?
答案有很多
答案1
比如第一个鸡蛋在50层,最坏的结果【bia ji】碎了;剩下50层只能一层一层试,需要50次;
在这个方法中第一个鸡蛋扔1次,第二个鸡蛋需要扔49次
答案2
比如第一个鸡蛋在10层,最坏的结果【bia ji】碎了;剩下10层然后一层一层试,需要11次;不是最坏的结果,最坏的结果是10,20,30,40,50,60,70,80不碎,在90层碎了;最坏情况需要19次才能得到结果
在这个方法中第一个鸡蛋扔9次,第二个鸡蛋需要扔10次
答案3
再比如:
第一个鸡蛋第1次在14层;假设在这一层鸡蛋碎了,只需要13+1次得到结果。
第2次在14+13=27层 ;假设在这一层碎了,只需要12+1+1次可以得到结果
第3次在27+12=39层;
第4次在39+11=50层;
...
第10次在95+4=99层;
第一个鸡蛋被扔了12次,第个鸡蛋最多被扔14次;
使用上面的方式,最多经过14次可以得到 f 确切的值。
使用了3中方式扔鸡蛋,有没有发现什么规律,就是鸡蛋最好抛差不多的次数才能接近最优解;
结论
对于任意层数N和任意鸡蛋K;
对于任意层x扔鸡蛋
如果鸡蛋碎了,可以确定f在x之下;确定f需要的次数为f1 = f(k-1,x);k-1的意思是,k个鸡蛋,在这层碎了一个;
如果鸡蛋没碎,可以确定f在x之上;确定f需要的次数为f2 = f(k,n-x);k个鸡蛋,在x层没有碎
最后的答案是考虑最坏结果,所以要去f1、f2较大值+1;result = Math.max(f1,f2)+1;
理解到这里就可以写代码了
暴力代码
var superEggDrop = function (K, N) {
return helper(K, N)
function helper(k, n) {
if (k === 1) return n
if (k === 0 || n === 0) return 0
if (n === 1) return 1
let result = n;
// 这里;需要从1-n层找到最小次数
for (let i = 1; i <= n; i++) {
const max = Math.max(helper(k - 1, i - 1), helper(k, n - i))
result = Math.min(result, max + 1)
}
return result
}
}
思路应该没啥问题,但是循环+递归还有的数据量,肯定是超时;
记忆化
因为在递归过程中有大量的重复计算,比如100层,2个鸡蛋的最优解是14;这个结果第一次计算出来后,后续可以直接使用;所以增加map空间换时间方式优化代码;
增加map
记忆化优化代码
var superEggDrop = function (K, N) {
const map = new Map()
return helper(K, N)
function helper(k, n) {
const key = n * 100 + k
if (map.has(key)) {
return map.get(key)
} else {
if (k === 1 || n === 1 || n === 0) return n
let result = n
for (let i = 1; i <= n; i++) {
const max = Math.max(helper(k - 1, i - 1), helper(k, n - i))
result = Math.min(result, max + 1)
}
map.has(key,result)
return result
}
}
}
还是不行啊,还是循环+递归,虽然用map存储的大量的数据,但是循环+递归的复杂度还是没有降下来;
这里就用到了分析,假设2中的结论
鸡蛋要尽可能的多扔;所以for循环中进行了一些不必要的运算,这一部分可以减去;
裁剪掉哪一部分运算呢?可以看下图
对于在区间[1,N]任意x层;[1,x]确定f所在位置需要的次数随着x的增加而增加;
同时,同时x的大小影响着区间[x,N]确定f所在位子需要的次数;并且次数随着x增加儿减少;
一定要理解上面的两句换;这两句话是核心。一个递增,一个递减;那么他们的相交点就是确定f需要的最小次数;对不对?
根据上述代码,将for循环改为二分
var superEggDrop = function (K, N) {
const map = new Map()
return helper(K, N)
function helper(k, n) {
const key = n * 100 + k
if (!map.has(key)) {
if (k === 1) return n
if (k === 0 || n === 0) return 0
if (n === 1) return 1
let left = 1
let right = n
while (left + 1 < right) {
// 中间值
const m = (left + right) >> 1
const low = helper(k - 1, m - 1)
const high = helper(k, n - m)
if (low < high) {
left = m
} else if (low > high) {
right = m
} else {
left = m
right = m
}
}
const t1 = Math.max(helper(k - 1, left - 1), helper(k, n - left))
const t2 = Math.max(helper(k - 1, right - 1), helper(k, n - right))
map.set(key, Math.min(t1, t2) + 1)
}
return map.get(key)
}
}
递归+二分+记忆话搜索;恭喜你,你可以AC本题;
后续
查看题解,寻找大神代码
大神代码赏析
var superEggDrop = function (K, N) {
let array = Array(K + 1).fill(0)
let result = 0
while (array[K] < N) {
result++
for (let i = K; i > 0; i--) {
array[i] = array[i - 1] + array[i] + 1
}
}
return result
}
我要着脑子有何用;睡觉睡觉
记于:2022.01.10夜