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

題目

你將得到 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
}
複製代碼

初始狀態爲只有一個雞蛋時,各個樓層須要的次數。

待更新...

相關文章
相關標籤/搜索