leetcode 198 打家劫舍 (一維動態規劃、基礎)
題目:
你是一個職業盜賊,鎖定了一條大街準備今晚作案,街上每棟房子裡都有固定量的財務。但是相鄰的房子之間有報警器,一旦兩個相鄰的房子在同一晚被盜,就會觸發報警器。現已知一系列非負整數表示每個房子裡的財務數目,請計算在不觸發報警器的情況下,你今晚可以盜取的最大值。
思路:
因為我可以選擇偷或者不偷,所以我們會有兩個分條件。假設共有四個房子ABCD。那麼我們會有以下兩個分選擇:
於是我們可以知道,雖然我們開始想要得到在ABCD中進偷竊的最大收益,但是現在這個問題變成了兩個子問題:
計算在CD中的最大收益,然後加上A房子的收益
計算在BCD中的最大收益
然後,我們比較以上兩個選項,就能知道在ABCD中選擇的最大收益。
針對在BCD中選擇房子,獲得最大收益的問題,我們可以同樣的進行拆解。
這時候,我們發現 計算在CD中選擇的最大收益。這個問題在ABCD的時候也出現了。我們可以重新計算一遍,但是也可以把之前的答案儲存下來,這樣就不需要計算了。
那麼我們需要儲存的答案都有哪些呢?ABCD、BCD、CD、D。也就是從一個節點到終點的情況。於是我們可以構建陣列memo[x],其中的x表示的是剩餘元素的個數。
於是上述的四種情況,分別對應memo[4]、memo[3]、memo[2]、memo[1]。
因此,我們的目標是memo[4],它的結果可以拆解為兩個子問題:
memo[2] + Value_A
memo[3]
同理,當我們計算memo[3]的時候,它的結果可以拆解為兩個子問題:
memo[2] + Value_B
memo[1]
然後選出這兩種情況的最大值,遞迴執行,直到index<0。
public int solve(int index, int[] nums){
if(index < 0){
return 0;
}
int max = Math.max(nums[index] + solve(index - 2, nums), solve(index - 1, nums));
return max;
}
public int rob(int[] nums) {
return solve(nums.length-1, nums);
}
此種暴力方法在執行第56個測試用例時,超出時間限制。
假設我們搶n-1家,那麼接下來的執行方案:
n-1 ->(n-3, n-4, n-5)
假設我們搶n-2家,那麼接下來的方案為:
n-2 ->(n-4, n-5)
那麼我的兩種決策方式只是影響能不能搶n-3,在n-3之後都是隨便搶的;通過觀察上述兩種方案,我們發現了n-4,n-5被重複計算。因此,每一家都有兩種可能,搶或者不搶。則該演算法的時間複雜度為:O(2n)。
為了避免上述的重複計算(優化為動態規劃方法),我們初始化一個數組來記錄此下標是否被計算過,將陣列初始化為-1,如果當前index被算過,就記錄下來。
如果這個index被計算過,那麼我們直接返回這個index對應的值即可,這就是去冗餘,採用空間換時間的方法。因此當n-1房屋的最優解算過後,就能推匯出n房屋的最優解。這就是動態規劃的思想。
因此,我們考慮使用動態規劃,設定result[]陣列記錄搶奪該房屋可能的最大收益,同時用來記錄此房屋是否被搶過。自頂向下解法
class Solution {
public static int[] result;
public int solve(int index, int[] nums){
if(index < 0){
return 0;
}
if(result[index] >= 0){
return result[index];
}
result[index] = Math.max(nums[index] + solve(index-2 , nums), solve(index-1, nums));
return result[index];
}
public int rob(int[] nums) {
result = new int[nums.length];
for(int i=0; i < result.length; i++){
result[i]=-1;
}
return solve(nums.length-1, nums);
}
}
對於每個房屋我們都算了一次,那麼時間複雜度為O(n)
自底向上解法
public int rob(int[] nums) {
if (nums.length == 0){
return 0;
}
if (nums.length == 1){
return nums[0];
}
if (nums.length == 2){
return Math.max(nums[0], nums[1]);
}
int[] result = new int[nums.length];
result[0] = nums[0];//只有一家的情況下,只能搶這一家了
result[1] = Math.max(nums[0], nums[1]);//有兩家可選的情況下,搶價值最大的那一家
for(int index=2; index < result.length; index++){
result[index] = Math.max(nums[index] + result[index-2], result[index -1]);
}
return result[nums.length -1];
}
}
自底向上,編碼簡單,遞推(自頂向下為遞迴)。既然自底向上不需要遞迴,那麼就不需要solve函數了。我們只要處理好邊界條件,然後計算即可。
C++程式碼:
非常簡潔:
使用dp[i]表示到第i個房間時得到的最大金額數,得到狀態轉移方程:
dp[i]=max{dp[i-1],dp[i-2]+money[i]};
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size()==0) return 0;
int i,dp[100000];
dp[0]=nums[0]; //必須先初始化前兩個值
dp[1]=nums[0]>nums[1]?nums[0]:nums[1];
for(i=2;i<nums.size();i++){
dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[nums.size()-1];
}
};
通常利用動態規劃的共性:
本質:遞迴