[LeetCode] #198 House Robber


Problem

給一個整數陣列,找最大可能的不相鄰數總合

  • Link
  • 等級:Medium
  • 推薦指數:[] 典型 DP 題,另外有關鍵是識別出可將 DP 的 function call 簡化為 iteration。DP 問題只的就是每一的解會跟其他輪有關係,若後面輪次的解只跟其前面輪次有關係,那自然就可以用 iteration 即可毋需使用到 recursion

有人推薦過的題目的我才會紀錄,所以即使我覺得只有一顆星他依舊是一題有其他人推薦的題目,只是我自己不覺得需要刷
代表我覺得有時間再看就好
代表可以刷
代表一定刷
代表多刷幾次甚至把所有 solution 都弄懂

Solution

DP 的想法其實蠻直覺的,很容易識別出來
比如給定一個數列 [A, B, C, D]
他的解就會是下列兩者中的較大者

  • 『取 A』 加上 以 [C, D] 為題的解
  • (不取 A)以 [B, C, D] 為題的解

如果我們將本題的求解函式定義為 f,參數為 nums 的 offset,代表求以 nums[offset:] 為題的解
那就可以寫成 f(0) = max( nums[0] + f(2), f(1) )
進一步 generalize 就變成 f(i) = max( nums[i] + f(i+2), f(i+1) )
所以就變成了一個 recursive,且其收斂狀況為 i > len(nums) 的時候

class Solution:
    def rob(self, nums: List[int]) -> int:
        d = {}
        n = len(nums)
        
        def _rob(offset):
            nonlocal n
            if offset >= n:
                return 0
            
            if offset in d:
                return d[offset]
            
            maxi = max(_rob(offset+1), nums[offset] + _rob(offset+2))
            d[offset] = maxi
            
            return maxi
            
        return _rob(0)

但我們進一步觀察,f(i)f(i+2)f(i+1) 有關係,整個 recursive 跑起來,整體時間順序上,其實是先跑出 f(n) 的結果,然後跑出 f(n-1),然後 f(n-2),如此直到得到 f(0)
換個角度,如果我們能事先直接算出 f(n)f(n-1),那我們就可以透過 iteration 的方式一路把直到 f(0) 得出來
而我們其實知道 f(n) = 0f(n-1) = nums[n-1]
f(i) = max( nums[i] + f(i+2), f(i+1) ),知道 f(n-2) = max( nums[n-2] + f(n), f(n-1) ),也就是 f(n-2) = max( nums[n-2], nums[n-1] )
以此類推可以得到 f(n-3)f(n-4) … 直到 f(0)

舉例來說,如果 nums[A, B, C, D],我們是先得到了 [] 的解,然後 [D] 的解,然候 [C, D] 的解,以此類推,最後得以得出 [A, B, C, D] 的解

我們已經成功轉化成一個 iteration 而不是 recursive function call 了,但我們再做一個小變化讓 code 更簡潔一點
上面是從 nums 的尾部 iterate 回來,也就是從 n, n-1, n-2 直到 0
但本題其實從頭跑和從尾巴跑並無不同,也就是你要從街頭的第一家開始考慮怎麼搶,或者從街尾那家開始考慮怎麼搶,並無不同
也就是說,如果 nums[A, B, C, D],我們是先得到了 [] 的解,然後 [A] 的解,然候 [A, B] 的解,以此類推,最後也得以得出 [A, B, C, D] 的解,與上面的解並不會不同
他還是呈現一樣的規律:本輪的解,會是下列兩者取其大者

  • 本輪的數字 + 上上輪的解
  • 上一輪的解

也就是一樣 g(x) = max( nums[x] + g(x-2), g(x-1) )
然後若定義 g(0) = nums[0], g(-1) = 0, 那我們要求的就是 g(len(nums)-1)
所以就可以寫成

class Solution:
    def rob(self, nums: List[int]) -> int:
        prev = 0
        curr = nums[0]
        for x in range(1, len(nums)):
            prev, curr = curr, max(nums[x] + prev, curr)
        return curr

可以再稍微簡化成

class Solution:
    def rob(self, nums: List[int]) -> int:
        prev = 0
        curr = 0
        for n in nums:
            prev, curr = curr, max(n + prev, curr)
        return curr

所以其實繞了一圈,我們回頭來看
從尾擴展:f(i) = max( nums[i] + f(i+2), f(i+1) ), f(n) = 0, f(n-1) = nums[n-1]
從頭擴展:g(x) = max( nums[x] + g(x-2), g(x-1) ), g(0) = nums[0], g(-1) = 0
從尾擴展的想法會比較容易先走向 recursive function 作法,因為『本輪的解,會取決於下兩輪的解』
從頭擴展的想法會比較容易先走向 iteration 作法,因為『本輪的解,會取決於前兩輪的解』


Author: Chris Chung
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint policy. If reproduced, please indicate source Chris Chung !
  TOC