一道有趣的算法--鸡蛋掉落

题目

你将得到 K 个鸡蛋,并能够使用一栋从 1 到 N  共有 N 层楼的建筑。git

每一个蛋的功能都是同样的,若是一个蛋碎了,你就不能再把它掉下去。github

你知道存在楼层 F ,知足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。算法

每次移动,你能够取一个鸡蛋(若是你有完整的鸡蛋)并把它从任一楼层 X 扔下(知足 1 <= X <= N)。swift

你的目标是确切地知道 F 的值是多少。数组

不管 F 的初始值如何,你肯定 F 的值的最小移动次数是多少?ide

提示:优化

1 <= K <= 100ui

1 <= N <= 10000spa

leetcode连接code

入门思路

只有一个蛋

若是只有一个蛋, F的值是多少?

假设楼有N层高,只能从1层挨个丢到N层,直到鸡蛋破裂为止。

有朋友会问若是第一层鸡蛋就坏了,那么是否最小移动次数为1便可?

请注意有个关键信息 不管 F 的初始值如何, 因此最小值确认次数只能为N。

无数个蛋

无数个蛋,就能够用最快的二分方法。

与下面这题的思路一致:

第一个错误的版本

次数为O(logN),向上取整。

两个蛋

这题在leetcode上看到以前,第一次接触到的是两个蛋的版本。

假设如今有100层楼,2个鸡蛋,怎么丢是最好的?

这题最关键是要考虑最差的状况。

假设F为49,使用二分法,50层蛋碎了,那么须要从1-49层挨个丢一次,结果为1 + 49 = 50次。

假设F为74,四等份方式,25, 50,75各丢一次而后从51-74,24 + 3 = 27次。

也有说直接对层数开方,那么最大就是9 + 9 了。(89层)

这里的次数实际上是蛋碎次数的最大值(如分红10分,则最多第9次碎),加上单个分段的最大值(0-10为一段,则最多从1-9,9次)的和。

因此为了尽量平均大小,成立一个等式 1+2+3...+X >= N。 X为知足等式的最小值。

从第N层开始丢,若没碎则从N + (N - 1)层开始丢,以此类推。

若第一次坏了,总次数为1 + (N - 1),若第二次坏了,则为 2 + (N - 2),次数保证为N。

那么若是N为100,能够求得X的值为14。

暴力DP

算法文档地址

原文档代码中有一点小问题,下面是已经修正的代码:

public func drop(numberOfEggs: Int, numberOfFloors: Int) -> Int {
  guard numberOfEggs != 0 && numberOfFloors != 0 else { return 0 }
  guard numberOfEggs != 1 && numberOfFloors != 1 else { return 1 }
  
  var eggFloor: [[Int]] = .init(repeating: .init(repeating: 1, count: numberOfFloors + 1), count: numberOfEggs + 1)
  var attempts = 0
  
  for floorNumber in stride(from: 0, through: numberOfFloors, by: 1) {
    eggFloor[1][floorNumber] = floorNumber
  }
  
  for eggNumber in stride(from: 2, through: numberOfEggs, by: 1) {
    for floorNumber in stride(from: 2, through: numberOfFloors, by: 1) {
      eggFloor[eggNumber][floorNumber] = Int.max
      for visitingFloor in stride(from: 1, through: floorNumber, by: 1) {
        attempts = 1 + max(eggFloor[eggNumber - 1][visitingFloor - 1], eggFloor[eggNumber][floorNumber - visitingFloor])
        
        if attempts < eggFloor[eggNumber][floorNumber] {
          eggFloor[eggNumber][floorNumber] = attempts
        }
      }
    }
  }
  
  return eggFloor[numberOfEggs][numberOfFloors]
}

复制代码

该思路是经过状态转移的方式求得。

主要分两步:

  1. 确认初始状态
var eggFloor: [[Int]] = .init(repeating: .init(repeating: 1, count: numberOfFloors + 1), count: numberOfEggs + 1)
  var attempts = 0
  
  for floorNumber in stride(from: 0, through: numberOfFloors, by: 1) {
    eggFloor[1][floorNumber] = floorNumber
  }
复制代码

初始化一个二维数组,经过eggFloor[eggNumber][floorNumber]的方式存储或者获取当前的结果。

初始化一个蛋的存储内容。

  1. 状态转移
attempts = 1 + max(eggFloor[eggNumber - 1][visitingFloor - 1], eggFloor[eggNumber][floorNumber - visitingFloor])
        
    if attempts < eggFloor[eggNumber][floorNumber] {
      eggFloor[eggNumber][floorNumber] = attempts
    }
复制代码

若是丢第N层时,蛋碎了,那么次数为 (当前蛋的数量-1)在 (N - 1)层中的移动次数 + 1。

若是蛋没有碎,次数为(当前蛋的数量)在 (总层数 - N)层中的移动次数 + 1。

思路比较简单清晰,但有一个时间复杂度的问题,这也是为何称为暴力DP的缘由,在3个for循环套用下来只有,会发现时间复杂度可以达到kN²,也就是蛋数量 * 楼层数量 * 楼层数量。

优化DP

斐波那契数列

爬楼梯

有想法的朋友能够尝试用DP的思路作一次。

斐波那契数列若是用DP的思路能够分为两种。

一种是从前到后推:

f(3) = f(2) + f(1)

f(4) = f(3) + f(2)

......

f(10) = f(9) + f(8)

一种是从后往前:

f(10) = f(9) + f(8)

f(10) = (f(8) + f(7)) + (f(7) + f(6))

......

其中第一种时间复杂度为N, 第二种为2的N次方。

如何优化

func superEggDrop(_ K: Int, _ N: Int) -> Int {
    var dp = [Int](repeating: 0, count: N + 1)
    for floorNum in stride(from: 0, through: N, by: 1) {
        dp[floorNum] = floorNum
    }
    for _ in stride(from: 2, through: K, by: 1) {
        var dp2 = [Int](repeating: 0, count: N + 1)
        var x = 1
        for n in stride(from: 1, through: N, by: 1) {
            while x < n && max(dp[x - 1], dp2[n - x]) > max(dp[x], dp2[n - x - 1]) {
                x += 1
            }
            dp2[n] = 1 + max(dp[x - 1], dp2[n - x])
        }
        dp = dp2
    }
    return dp[N]
}
复制代码

以前的dp初始值为[eggNumber][floorNumber]的二维数组方式存储。

如今只根据[floorNumber]存储。

var dp = [Int](repeating: 0, count: N + 1)
for floorNum in stride(from: 0, through: N, by: 1) {
    dp[floorNum] = floorNum
}
复制代码

初始状态为只有一个鸡蛋时,各个楼层须要的次数。

待更新...

相关文章
相关标签/搜索