打家劫舍問題(典型動態(tài)規(guī)劃)

寫在前

本類型題目要求不能偷盜相鄰房屋,即對于當前房屋我們選擇偷或者不偷兩種狀態(tài),對應我們的最大收益值的更新(動態(tài)規(guī)劃)。

1.打家劫舍(198-中)

題目描述:你是一個專業(yè)的小偷,計劃偷竊沿街的房屋。每間房內(nèi)都藏有一定的現(xiàn)金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會自動報警。

給定一個代表每個房屋存放金額的非負整數(shù)數(shù)組,計算你 不觸動警報裝置的情況下 ,一夜之內(nèi)能夠偷竊到的最高金額。

示例

輸入:[1,2,3,1]
輸出:4
解釋:偷竊 1 號房屋 (金額 = 1) ,然后偷竊 3 號房屋 (金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4 。
     
輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
     偷竊到的最高金額 = 2 + 9 + 1 = 12 。

思路:動態(tài)規(guī)劃實現(xiàn):由于相鄰的兩家店不能偷。對于第 n 家店,我們只能選擇偷或者不偷,然后取兩種情況的最大值。

  • dp[n] 數(shù)組:前 n 天能夠帶來的最大收益
  • 狀態(tài)轉(zhuǎn)移方程:dp[n] = Math.max(dp[n - 2] + nums[n - 1], dp[n - 1] )

代碼:

public int rob1(int[] nums) {
    int n = nums.length;
    if (nums == null || n == 0) {
        return 0;
    }
    int[] dp = new int[n + 1];
    dp[0] = 0;
    dp[1] = nums[0];
    for (int i = 2; i < n + 1; i++) {
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
    }
    return dp[n];
}

我們更新dp[i]的時候,只需要dp[i - 1]以及dp[i - 2]的信息(這里分別用pre2和pre1表示),再之前的信息就不需要了,所以可以進行空間優(yōu)化。

public int rob2(int[] nums) {
    if (nums == null || nums.length == 0) {
        return 0;
    }
    int pre1 = 0, pre2 = 0;   
    for (int i : nums) {
        int tmp = Math.max(pre2, pre1 + i);
        pre1 = pre2;
        pre2 = tmp;
    }
    return pre2;
}

2.打家劫舍II(213-中)

題目描述:你是一個專業(yè)的小偷,計劃偷竊沿街的房屋,每間房內(nèi)都藏有一定的現(xiàn)金。這個地方所有的房屋都 圍成一圈 ,這意味著第一個房屋和最后一個房屋是緊挨著的。同時,相鄰的房屋裝有相互連通的防盜系統(tǒng),如果兩間相鄰的房屋在同一晚上被小偷闖入,系統(tǒng)會自動報警 。

給定一個代表每個房屋存放金額的非負整數(shù)數(shù)組,計算你 在不觸動警報裝置的情況下 ,今晚能夠偷竊到的最高金額。

示例

輸入:[1,2,3,1]
輸出:4
解釋:偷竊 1 號房屋 (金額 = 1) ,然后偷竊 3 號房屋 (金額 = 3)。
     偷竊到的最高金額 = 1 + 3 = 4 。
     
輸入:[2,7,9,3,1]
輸出:12
解釋:偷竊 1 號房屋 (金額 = 2), 偷竊 3 號房屋 (金額 = 9),接著偷竊 5 號房屋 (金額 = 1)。
     偷竊到的最高金額 = 2 + 9 + 1 = 12 。

思路:這道題區(qū)別案例1就是數(shù)組是環(huán)形,即偷了第一家就不能偷最后一家。

  • 偷第一家,也就是求出在前n - 1家中偷的最大收益,也就是不考慮最后一家的最大收益。

  • 不偷第一家,也就是求第2家到最后一家中偷的最大收益,也就是不考慮第一家的最大收益。

然后只需要返回上邊兩個最大收益中的銜接的即可。圖示的話就是下邊的兩個范圍。

X X X X X X
^       ^

X X X X X X
  ^       ^

dp數(shù)組和狀態(tài)轉(zhuǎn)移方程均相同,區(qū)別在于我們進行狀態(tài)轉(zhuǎn)移的區(qū)間有兩個,取最大值。具體見代碼。

代碼:

public int rob(int[] nums) {
    //邊界條件
    int n = nums.length;
    if (n == 0) return 0;
    if (n == 1) return nums[0];
    return Math.max(robHelper(nums, 0, n - 2), robHelper(nums, 1, n - 1));
}

private int robHelper(int[] nums, int start, int end) {
    int pre1 = 0, pre2 = 0;
    for (int i = start; i <= end; ++i) {
        int cur = Math.max(pre2, pre1 + nums[i]);
        pre1 = pre2;
        pre2 = cur;
    }
    return pre2;
}

3.打家劫舍III(337-中)

題目描述:在上次打劫完一條街道之后和一圈房屋后,小偷又發(fā)現(xiàn)了一個新的可行竊的地區(qū)。這個地區(qū)只有一個入口,我們稱之為“根”。 除了“根”之外,每棟房子有且只有一個“父“房子與之相連。一番偵察之后,聰明的小偷意識到“這個地方的所有房屋的排列類似于一棵二叉樹”。 如果兩個直接相連的房子在同一天晚上被打劫,房屋將自動報警。

計算在不觸動警報的情況下,小偷一晚能夠盜取的最高金額。

示例 :

輸入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

輸出: 7 
解釋: 小偷一晚能夠盜取的最高金額 = 3 + 3 + 1 = 7.

思路:標準的動態(tài)規(guī)劃,為了避免過多重復計算,用HashMap存放某一節(jié)點能偷到的最大值

法1:相互連接的父子節(jié)點不能同時偷,即轉(zhuǎn)化為偷還是不偷root節(jié)點。

  • 偷root節(jié)點,可以偷root的孫子結(jié)點,只要節(jié)點不為空,以此類推...

  • 不偷root節(jié)點,可以偷root的左右子結(jié)點,只要左右節(jié)點不為空,以此類推...

法2:另外比較好的思路,以每個節(jié)點狀態(tài)為基本狀態(tài),返回一個節(jié)點偷和不偷的最大值

  • 分別得到出當前節(jié)點,左右子節(jié)點偷與不偷的利潤列表

  • 根據(jù)規(guī)則進行更新,res[0]代表偷,res[1]代表不偷,本質(zhì)和法1類似。

代碼1:比較常規(guī)思路。

public int rob(TreeNode root) {
    // 存放某一節(jié)點偷到的最大值
    HashMap<TreeNode, Integer> map = new HashMap<>();
    return rob1(root, map);
}

private int rob1(TreeNode root, HashMap<TreeNode, Integer> map) {
    if (root == null) return 0;
    if (map.containsKey(root)) return map.get(root);  
    
    // 計算偷root節(jié)點和孫子節(jié)點的情況
    int num = root.val;
    if (root.left != null) {
        num += rob1(root.left.left, map) + rob1(root.left.right, map);
    }
    if (root.right != null) {
        num += rob1(root.right.left, map) + rob1(root.right.right, map);
    }
    
    // 與只偷左右子節(jié)點比較取較大值
    int res = Math.max(num, rob1(root.left, map) + rob1(root.right, map));
    map.put(root, res);    
    return res;
}

代碼2:每個節(jié)點都有兩個狀態(tài),偷或者不偷,自底向上的遞歸求解。

public int rob(TreeNode root) {
    int[] result = rob2(root);
    return Math.max(result[0], result[1]);
}

// 返回值為偷或者不偷的root的最大值
public int[] rob2(TreeNode root) {
    //{0, 0}
    if (root == null) return new int[2];  
    int[] res = new int[2];
    // 左右孩子節(jié)點偷或者不偷情況
    int[] left = rob2(root.left);   
    int[] right = rob2(root.right);
    // res[0]代表偷,res[1]代表不偷
    res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    res[1] = right[0] + left[0] + root.val;

    return res;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容