LeetCode 337. House Robber III

The thief has found himself a new place for his thievery again. There is only one entrance to this area, called the "root." Besides the root, each house has one and only one parent house. After a tour, the smart thief realized that "all houses in this place forms a binary tree". It will automatically contact the police if two directly-linked houses were broken into on the same night.
Determine the maximum amount of money the thief can rob tonight without alerting the police.
Example 1:

Paste_Image.png

Maximum amount of money the thief can rob = 3 + 3 + 1 = 7.
Example 2:
Paste_Image.png

Maximum amount of money the thief can rob = 4 + 5 = 9.
Credits:Special thanks to @dietpepsi for adding this problem and creating all test cases.
Subscribe to see which companies asked this question.
在上次打劫完一條街道之后和一圈房屋之后,竊賊又發現了一個新的可以打劫的地方,但這次所有的房子組成的區域比較奇怪,聰明的竊賊考察地形之后,發現這次的地形是一顆二叉樹。與前兩次偷竊相似的是每個房子都存放著特定金額的錢。你面臨的唯一約束條件是:相鄰的房子裝著相互聯系的防盜系統,且當相鄰的兩個房子同一天被打劫時,該系統會自動報警。
算一算,如果今晚去打劫,你最多可以得到多少錢,當然在不觸動報警裝置的情況下。
樣例
Paste_Image.png

竊賊最多能偷竊的金錢數是 3 + 3 + 1 = 7.
Paste_Image.png

竊賊最多能偷竊的金錢數是 4 + 5 = 9.

分析

Step I -- Think naively

初看這個問題,顯然這個問題與遞歸和最優子結構有關:如果我們想要從根節點rob到最大值,我們當然希望我們對它的左子節點和右子節點做相同的操作。
所以i,如果我們定義一個rob函數,rob(root)將會返回我們能從根節點開始rob的最大值,那么現在的問題的關鍵就在于如何從rob(root)得到rob(root.left)和rob(root.right)。
顯然對于樹的問題我們可以使用遞歸的方法,顯然當這棵樹為空的時候,結果是0,直接返回就可以了。
對于root節點來說,只有兩種情況,一種就是rob打劫root節點,一種就是不打劫root節點。首先我們分析第一種情況,打劫root節點

  • 如果我們已經打劫了root節點,那么根據題目的要求,我們就不能打劫root的左子節點,和右子節點,所以,接下來的情況就有四種(root.left.left, root.left.right, root.right.left, root.right.right),就是root的孫子輩的節點。把孫子輩節點的值加起來就行了。
  • 如果我們不打劫root節點,那么根據題目要求,我們可以打劫左子樹和右子樹。
    我們把這兩種情況分別求出來,最后取最大值就是root節點的值。
public int rob(TreeNode root) {
            if(root == null)
                return 0;
            
            int val = root.val;
            
            if(root.left != null) {
                val += rob(root.left.left) + rob(root.left.right);
            }
            
            if(root.right != null) {
                val += rob(root.right.left) + rob(root.right.right);
            }
            
            return Math.max(val, rob(root.left) + rob(root.right));
        }
Paste_Image.png

Step II -- Think one step further

在第一種方法中,我們只考慮了最優子結構,而沒有考慮子問題的重復計算,我們分析一下,我們為了獲得rob(root),我們需要計算rob(root.left), rob(root.right), rob(root.left.left), rob(root.left.right), rob(root.right.left), rob(root.right.right);但是我們計算rob(root.left)的時候我們又會去計算一遍rob(root.left.left), rob(root.left.right),這樣就重復計算了,實際上是沒有必要的部分,所以我們考慮將已經計算過的結果存起來,如果下次需要直接存取就行了,而不需要重復計算一遍,所以這就變成了動態規劃的問題:“最優子結構”+“重復子問題”,我們在本題中用一個hashmap來存儲子問題的解。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int rob(TreeNode root) {
            if(root == null)
                return 0;
            HashMap<TreeNode, Integer> map = new HashMap<>();
            return helper(root, map);
        }

        private int helper(TreeNode root, HashMap<TreeNode, Integer> map) {
            if(root == null)
                return 0;
            if(map.containsKey(root))
                return map.get(root);
            
            int val = root.val;
            
            if(root.left != null)
                val += helper(root.left.left, map) + helper(root.left.right, map);
            
            if(root.right != null)
                val += helper(root.right.left, map) + helper(root.right.right, map);
            
            val = Math.max(val, helper(root.left, map) + helper(root.right, map));
            
            map.put(root, val);
            
            return val;
        }

}
Paste_Image.png

Step III -- Think one step back

我們繼續思考,為什么我們會出現重復子問題的計算,這是因為我們沒有記錄每個子問題的狀態的類別,我們知道每個節點就兩種情況,打劫不打劫,但是前面兩種都沒有記錄這種狀態信息,所以在計算時就會重復計算。我們考慮,我們考慮保存每個解的狀態,是打劫還是沒打劫。

If we were able to maintain the information about the two scenarios for each tree root, let's see how it plays out. Redefine rob(root) as a new function which will return an array of two elements, the first element of which denotes the maximum amount of money that can be robbed if root is not robbed, while the second element signifies the maximum amount of money robbed if it is robbed.
因為只有兩種狀態所以我們用一個數組來保存,當0時,就為不打劫當前節點,當為1時,就為打劫當前節點。
所以當為0時,我們只需要返回左子樹和右子樹的最大和即可,當為1時,我們只需要將root節點的值加上不打架左子樹和右子樹的結果返回,也就是左子樹狀態碼0和右子樹狀態碼為0的狀態。

/**
 * Definition for a binary tree node.
 * public class TreeNode {
 *     int val;
 *     TreeNode left;
 *     TreeNode right;
 *     TreeNode(int x) { val = x; }
 * }
 */
public class Solution {
    public int rob(TreeNode root) {
    int[] res = robSub(root);
    return Math.max(res[0], res[1]);
}

private int[] robSub(TreeNode root) {
    if (root == null) return new int[2];
    
    int[] left = robSub(root.left);
    int[] right = robSub(root.right);
    int[] res = new int[2];

    res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
    res[1] = root.val + left[0] + right[0];
    
    return res;
}

}
Paste_Image.png

通過上面的問題的一步步分析,我們更好的理解了動態規劃的意義,并且將算法優化到最佳

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

推薦閱讀更多精彩內容