經典動態規劃:打家劫舍系列問題House Robber盜賊

參考:

leetcode 337. 打家劫舍 III java

經典動態規劃:打家劫舍系列問題

【LeetCode】打家劫舍系列(I、II、III)

線性村莊

import java.util.Arrays;

/**
 * https://www.cnblogs.com/gzshan/p/11188104.html
 * https://www.jianshu.com/p/11684bd0115e
 * <p>
 * 你是一個專業的小偷,計劃偷竊沿街的房屋。
 * 每間房內都藏有一定的現金,影響你偷竊的唯一制約因素就是相鄰的房屋裝有相互連通的防盜系統,
 * 如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
 * 給定一個代表每個房屋存放金額的非負整數數組,計算你在不觸動警報裝置的情況下,能夠偷竊到的最高金額。
 */
public class RobbersLineTest {

    public static void main(String[] args) {
        int[] nums = {6, 9, 2, 6, 9, 4};

        System.out.println(rob(nums));
        System.out.println(rob2(nums));
        System.out.println(rob3(nums));

        System.out.println(rob4(nums, 0));

        // 初始化備忘錄,所有元素爲默認值-1
        memo = new int[nums.length];
        Arrays.fill(memo, -1);
        System.out.println(rob5(nums, 0));

        System.out.println(rob6(nums));
    }

    /**
     * 用一個二維數組記錄訪問每一個房間時可以得到的錢數,其中,
     * money[i][0]表示不搶當前房間可以得到的錢數,
     * money[i][1]表示搶劫當前房間可以得到的錢數。
     */
    public static int rob(int[] nums) {
        if (nums.length == 0)
            return 0;
        int[][] money = new int[nums.length][2];
        money[0][0] = 0;//不搶
        money[0][1] = nums[0];//搶
        for (int i = 1; i < nums.length; i++) {
            //不搶當前房間,前一個可以搶也可以不搶,取最大值
            money[i][0] = Math.max(money[i - 1][0], money[i - 1][1]);
            money[i][1] = money[i - 1][0] + nums[i];
        }
        return Math.max(money[nums.length - 1][0], money[nums.length - 1][1]);
    }


    /**
     * 動態規劃
     */
    public static int rob2(int[] nums) {
        if (nums.length == 0)
            return 0;
        if (nums.length == 1)
            return nums[0];
        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(nums[0], nums[1]);
        for (int i = 2; i < nums.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
        }
        return dp[nums.length - 1];
    }

    /**
     * https://mp.weixin.qq.com/s/4sh8lwsc-C1XaLGvkWyEmw
     */
    public static int rob3(int[] nums) {
        int sumOdd = 0;
        int sumEven = 0;

        for (int i = 0; i < nums.length; i++) {
            if (i % 2 == 0) {
                sumEven += nums[i];
                sumEven = Math.max(sumOdd, sumEven);
            } else {
                sumOdd += nums[i];
                sumOdd = Math.max(sumOdd, sumEven);
            }
        }
        return Math.max(sumOdd, sumEven);
    }

    /**
     * 遞歸
     */
    private static int rob4(int[] nums, int start) {
        if (start >= nums.length) {
            return 0;
        }

        return Math.max(
                // 不搶,去下家
                rob4(nums, start + 1),
                // 搶,去下下家
                nums[start] + rob4(nums, start + 2)
        );
    }

    private static int[] memo;

    /**
     * 遞歸 + 備忘錄
     * 遞歸存在重疊子問題,可以用備忘錄進行優化
     * 返回 rob[start..] 能搶到的最大值
     */
    private static int rob5(int[] nums, int start) {
        if (start >= nums.length) {
            return 0;
        }
        // 避免重複計算
        if (memo[start] != -1) return memo[start];

        int res = Math.max(
                rob5(nums, start + 1),
                nums[start] + rob5(nums, start + 2)
        );
        // 記入備忘錄
        memo[start] = res;
        return res;
    }

    static int rob6(int[] nums) {
        int n = nums.length;
        // 記錄 dp[i+1] 和 dp[i+2]
        int dp_i_1 = 0, dp_i_2 = 0;
        // 記錄 dp[i]
        int dp_i = 0;
        for (int i = n - 1; i >= 0; i--) {
            dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
            dp_i_2 = dp_i_1;
            dp_i_1 = dp_i;
        }
        return dp_i;
    }

}

環形村莊

/**
 * https://www.cnblogs.com/gzshan/p/11188104.html
 * https://www.jianshu.com/p/11684bd0115e
 * <p>
 * 你是一個專業的小偷,計劃偷竊沿街的房屋,每間房內都藏有一定的現金。
 * 這個地方所有的房屋都【圍成一圈】,這意味着第一個房屋和最後一個房屋是緊挨着的。
 * 同時,相鄰的房屋裝有相互連通的防盜系統,如果兩間相鄰的房屋在同一晚上被小偷闖入,系統會自動報警。
 */
public class RobbersCircleTest {

    public static void main(String[] args) {
        int[] nums = {6, 9, 2, 6, 9, 4};

        System.out.println(rob(nums));
        System.out.println(rob2(nums));
    }

    public static int rob(int[] nums) {
        /**
         * 思路:由於首尾也屬於相鄰,因此需要分別判斷,以第一家是否打劫分成兩個問題
         * 第一家搶:最後一家一定不能搶,從第0個到len-2做動態規劃
         * 第一家不搶:從1到len-1做動態規劃
         * 然後比較找出最大值
         */
        if (nums == null || nums.length == 0)
            return 0;
        int len = nums.length;
        if (len == 1)
            return nums[0];
        int[] dp1 = new int[len];
        int[] dp2 = new int[len + 1];

        //第一家搶
        dp1[0] = 0;
        dp1[1] = nums[0];
        for (int i = 2; i < len; i++)
            dp1[i] = Math.max(dp1[i - 1], dp1[i - 2] + nums[i - 1]);

        //第一家不搶
        dp2[0] = 0;
        dp2[1] = 0;
        for (int i = 2; i <= len; i++)
            dp2[i] = Math.max(dp2[i - 1], dp2[i - 2] + nums[i - 1]);

        return Math.max(dp1[len - 1], dp2[len]);
    }

    public static int rob2(int[] nums) {
        int n = nums.length;
        if (n == 1) return nums[0];
        return Math.max(
                robRange(nums, 0, n - 2),
                robRange(nums, 1, n - 1)
        );
    }

    // 僅計算閉區間 [start,end] 的最優結果
    private static int robRange(int[] nums, int start, int end) {
        int dp_i_1 = 0;
        int dp_i_2 = 0;
        int dp_i = 0;
        for (int i = end; i >= start; i--) {
            dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
            dp_i_2 = dp_i_1;
            dp_i_1 = dp_i;
        }
        return dp_i;
    }

}

二叉樹村莊

import org.jetbrains.annotations.NotNull;

import java.util.HashMap;
import java.util.Map;

/**
 * https://www.cnblogs.com/gzshan/p/11188104.html
 * https://www.jianshu.com/p/11684bd0115e
 * <p>
 * 在上次打劫完一條街道之後和一圈房屋後,小偷又發現了一個新的可行竊的地區。
 * 這個地區只有一個入口,我們稱之爲“根”。 除了“根”之外,每棟房子有且只有一個“父“房子與之相連。
 * 一番偵察之後,聰明的小偷意識到“這個地方的所有房屋的排列類似於一棵二叉樹”。
 * 如果兩個直接相連的房子在同一天晚上被打劫,房屋將自動報警。
 */
public class RobbersTreeTest {

    public static void main(String[] args) {
        TreeNode root = initTreeNode();

        System.out.println(rob(root));
        System.out.println(rob2(root));
        System.out.println(rob3(root));
    }

    @NotNull
    private static TreeNode initTreeNode() {
        TreeNode root = new TreeNode(3);

        TreeNode treeNode10 = new TreeNode(4);
        TreeNode treeNode11 = new TreeNode(5);

        TreeNode treeNode20 = new TreeNode(1);
        TreeNode treeNode21 = new TreeNode(3);
        TreeNode treeNode22 = new TreeNode(1);

        root.setLeft(treeNode10);
        root.setRight(treeNode11);

        treeNode10.setLeft(treeNode20);
        treeNode10.setRight(treeNode21);

        treeNode11.setLeft(treeNode22);
        //        3
        //       / \
        //      4   5
        //     / \   \
        //    1   3   1
        return root;
    }


    //動態規劃
    //思路:
    //定義一個數組res,長度爲2,
    // res[0]表示不搶該節點可獲得最大值,
    // res[1]表示搶劫該節點可獲得最大值
    //方法dp(root)意爲:在以r爲根節點的樹中,返回搶劫根節點與不搶劫根節點可獲得的最大值
    public static int rob(TreeNode root) {
        int[] res = dp(root);
        return Math.max(res[0], res[1]);
    }

    /**
     * 返回一個大小爲 2 的數組 arr
     * arr[0] 表示不搶 root 的話,得到的最大錢數
     * arr[1] 表示搶 root 的話,得到的最大錢數
     */
    static int[] dp(TreeNode root) {
        if (root == null) return new int[]{0, 0};
        int[] left = dp(root.left);
        int[] right = dp(root.right);
        // 搶,下家就不能搶了
        int rob = root.val + left[0] + right[0];
        // 不搶,下家可搶可不搶,取決於收益大小
        int not_rob = Math.max(
                left[0], left[1])
                + Math.max(right[0], right[1]
        );

        return new int[]{not_rob, rob};
    }


    //遞歸思想(不要深入遞歸函數體,只需知道遞歸函數的功能,以及找到跳出遞歸的邊界條件)
    //思路:
    //能盜取的最高金額爲 搶劫該節點+搶劫該節點的左孩子的左右子樹+搶劫該節點的右孩子的左右子樹
    //與 搶劫該節點的左子樹+搶劫該節點的右子樹的和  的最大值
    //執行用時 1005ms  原因是出現了很多重複的計算,可使用動態規劃解決
    public static int rob2(TreeNode root) {
        if (root == null) return 0;
        int val = 0;
        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(rob(root.left) + rob(root.right), val + root.val);

    }

    static Map<TreeNode, Integer> memo = new HashMap<>();

    public static int rob3(TreeNode root) {
        if (root == null) return 0;
        // 利用備忘錄消除重疊子問題
        if (memo.containsKey(root))
            return memo.get(root);
        // 搶,然後去下下家
        int do_it = root.val
                + (root.left == null ?
                0 : rob(root.left.left) + rob(root.left.right))
                + (root.right == null ?
                0 : rob(root.right.left) + rob(root.right.right));
        // 不搶,然後去下家
        int not_do = rob(root.left) + rob(root.right);

        int res = Math.max(do_it, not_do);
        memo.put(root, res);
        return res;
    }


    public static class TreeNode {
        int val;
        TreeNode left;
        TreeNode right;

        TreeNode(int x) {
            val = x;
        }

        public int getVal() {
            return val;
        }

        public void setVal(int val) {
            this.val = val;
        }

        public TreeNode getLeft() {
            return left;
        }

        public void setLeft(TreeNode left) {
            this.left = left;
        }

        public TreeNode getRight() {
            return right;
        }

        public void setRight(TreeNode right) {
            this.right = right;
        }
    }
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章