[路飞]_鸡蛋掉落(非最优解版)

294 阅读1分钟

887. 鸡蛋掉落

题目

给你 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
  }
}

思路应该没啥问题,但是循环+递归还有10410^4的数据量,肯定是超时;

记忆化

因为在递归过程中有大量的重复计算,比如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循环中进行了一些不必要的运算,这一部分可以减去;
裁剪掉哪一部分运算呢?可以看下图

image.png

对于在区间[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夜