1 題目描述

有一排房子,每個房子裡面有一定的錢,但是不能連續偷兩個房子,否則會報警,在不觸發報警的狀況下求最多可以偷到多少錢。
2 解法
按照之前的介紹LeetCode 課前預習 - 掌握 Dynamic Programming 的思維指南,我很推薦把每一種步驟都嘗試寫出來看看,分別是:
- Step1: Recursive
- Step2: Recursive with Memoization
- Step3: Iterative + Tabulation
- Step4: Iterative with Constant Space
Step1: Recursive
一道很典型的通過子問題去解決原問題的題目,所以可以透過遞歸以及動態規劃解決。
假設:知道了前 n - 1 家店最多能偷的錢數,前 n - 2 家店最多能偷的錢數。
關係式:如果我們想要知道前 n 家商店最多能偷多少錢,我們可以選擇偷
- 偷:偷
第 n 家商店 + 前 n - 2 家商店最多能偷的錢數。
- 不偷:不偷
第 n 家商店 + 前 n - 1 家商店最多能偷的錢數。
最小問題:如果今天 n = 0,那麼就是0,如果n = 1,那麼就是nums[0]。
n = 0:表示沒有店家可以偷
n = 1:表示只有一家店可以偷,那也只能偷了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| def rob(self, nums: List[int]) -> int: """ time: O(2^n) - 對於長度為 n 的數組,時間複雜度為 O(2^n)。 - 因為每次遞歸函數都會將問題分成兩個子問題,並且繼續遞歸處理,這種情況會產生一個大小為 2^n 的遞歸樹。
space: O(n) 遞迴的深度 - 由於遞歸呼叫使用了函數呼叫棧,空間複雜度等於遞歸的深度。 - 每次遞歸呼叫時,棧的深度會增加 1。最壞情況下,遞歸深度為 O(n),因此空間複雜度為 O(n)。 """ n = len(nums) if n == 0: return 0 if n == 1: return nums[0] steal = self.rob(nums[:-2]) + nums[-1] not_steal = self.rob(nums[:-1]) return max(steal, not_steal)
|
Step2: Recursive with Memoization
那我們就思考,是不是某些已經算過的子問題,我們可以直接拿來用,這樣就不用重複計算了。
因此我們需要一個dict來記錄已經算過的子問題。使用dict的好處是可以使用O(1)的時間複雜度來查找。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| def rob2(self, nums: List[int]) -> int: """ time: O(n) 每個子問題只會被計算一次,並且儲存在 dp 字典中 space: O(n) - call stack space O(n): 最壞情況下,遞歸深度為 O(n),即 n 次遞歸呼叫會佔用 O(n) 的堆疊空間。 - dp dict O(n): dp 字典中儲存了 n+1 個子問題的結果,因此需要 O(n) 的額外空間。 """ def helper(nums: List[int]) -> int: n = len(nums) if n in dp: return dp[n] else: steal = helper(nums[:-2]) + nums[-1] not_steal = helper(nums[:-1]) dp[n] = max(steal, not_steal) return dp[n]
dp = {0: 0, 1: nums[0]} return helper(nums)
|
Step3: Iterative + Tabulation
接下來,因為Recursive的缺點是有call stack size的限制,所以我們可以使用Iterative,來避免這個問題
1 2 3 4 5 6 7 8 9 10 11 12 13
| def rob3(self, nums: List[int]) -> int: """ time: O(n) 每個子問題只會被計算一次,並且儲存在 dp 字典中 space: O(n) 只需要一個dp list """ dp = [0, nums[0]] if n == 0: return dp[0] elif n == 1: return dp[1] for i in range(2, len(nums)+1): steal = dp[i-2] + nums[i-1] not_steal = dp[i-1] dp.append(max(steal, not_steal)) return dp[-1]
|
Step4: Iterative with Constant Space
最後,我們再思考一下,目前時間複雜度已經到極限了,但是空間複雜度是不是可以再省一點?因為我們只需要前面兩個步驟的結果就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def rob4(self, nums: List[int]) -> int: """ time: O(n) 每個子問題只會被計算一次,並且儲存在 dp 字典中 space: O(1) 只需要constant個變數 """ n = len(nums) if n == 0: return 0 elif n == 1: return nums[0]
prev2, prev1 = 0, nums[0] for i in range(2, len(nums)+1): best = max(prev2 + nums[i-1], prev1) prev2 = prev1 prev1 = best
return prev1
|
3 總結
這題不難,我覺得所有的dp題目最有挑戰性的就是要先知道他的recursive formula,只要能夠實作出來,其他的優化都是小case。