After reading this article, you can go to Likou to win the following topics:
-----------
Some readers asked me privately how to do LeetCode "House Robber" series of questions (English version is called House Robber). I found that this series of topics are highly praised. They are more representative and skillful dynamic programming topics. Come today. Talk about this topic.
There are a total of three courses in the family and robbery series. The difficulty design is very reasonable and progressive. The first one is a relatively standard dynamic programming problem, while the second one incorporates the conditions of a circular array, and the third one is even more absolute. It combines the bottom-up and top-down solutions of dynamic programming with binary trees. I think it is very Inspiring. If you have never done it before, it is recommended to study.
Below, we start from the first analysis.
House Robber I
public int rob(int[] nums);
The topic is easy to understand, and the characteristics of dynamic programming are obvious. As we summarized in the previous article "Detailed Explanation of Dynamic Programming", to solve the problem of dynamic programming is to find "status" and "choice", nothing more .
Imagine that you are the professional robber, walking through this row of houses from left to right. There are two choices in front of each house : grab or not.
If you grab this house, then you definitely can't grab the next house next door, you can only choose from the next house.
If you don't grab this house, then you can go to the next house and continue to make choices.
After you have walked through the last house, you don't have to grab it, and the money you can grab is obviously 0 ( base case ).
The above logic is very simple, in fact, the "state" and "choice" have been clarified: the index of the house in front of you is the state, and whether to grab or not to grab is the choice .
In the two choices, choose the larger result each time, and the final result is the most money you can grab:
// The main function public int rob(int[] nums) { return dp(nums, 0); } // Return the maximum value that nums[start..] can grab private int dp(int[] nums, int start) { if (start >= nums.length) { return 0; } int res = Math.max( // Don’t grab, go to the next house dp(nums, start + 1), // grab, go to the next house nums[start ] + dp(nums, start + 2) ); return res; }
After clarifying the state transition, you can find that there start
is an overlapping sub-problem for the same location, as shown in the following figure:
Thieves have many options to get to this position. Wouldn't it be a waste of time if they enter recursion every time they get here? So there are overlapping sub-problems, which can be optimized with memo:
private int[] memo; // Main function public int rob(int[] nums) { // Initialization memo memo = new int[nums.length]; Arrays.fill(memo, -1); // The robber starts at 0 The house started to rob return dp(nums, 0); } // Return the maximum value that dp[start..] can grab private int dp(int[] nums, int start) { if (start >= nums.length) { return 0; } // Avoid double calculation if (memo[start] != -1) return memo[start]; int res = Math.max(dp(nums, start + 1), nums[start] + dp( nums, start + 2)); // Enter the memo memo[start] = res; return res; }
This is the top-down dynamic programming solution. We can also slightly modify it and write a bottom-up solution:
int rob(int[] nums) { int n = nums.length; // dp[i] = x means: // start the robbery from the i-th house, and the maximum amount of money you can grab is x // base case: dp [n] = 0 int[] dp = new int[n + 2]; for (int i = n-1; i >= 0; i--) { dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]); } return dp[0]; }
We also found that the state transition is only dp[i]
related to the two most recent states, so it can be further optimized to reduce the space complexity to O(1).
int rob(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; }
The above process has been explained in detail in our "Dynamic Planning Detailed Explanation", I believe everyone will be able to grasp it. I think it is very interesting that the follow up of this issue requires some clever adaptations based on our current thinking.
PS: I have written more than 100 original articles carefully , and I have hand-in-hand brushed with 200 buckle questions, all of which are published in labuladong's algorithm cheat sheet , which is continuously updated . It is recommended to collect, brush the questions in the order of my articles , master various algorithm routines, and then cast them into the sea of questions.
House Robber II
This question is basically the same as the first description. The robber still cannot rob neighboring houses. The input is still an array, but it tells you that these houses are not a row, but a circle .
In other words, the first house and the last house are now adjacent to each other and cannot be looted at the same time. For example, for an input array nums=[2,3,2]
, the result returned by the algorithm should be 3 instead of 4, because the beginning and the end cannot be robbed at the same time.
It seems that this constraint should not be difficult to solve. We mentioned a solution to the circular array in the previous "Monotonic Stack Solving Next Greater Number", so how to deal with this problem?
First of all, the first and last rooms cannot be robbed at the same time, so there are only three possible situations: either none is robbed; or the first house is robbed and the last one is not robbed; or the last house is robbed and the first is not robbed.
That's simple. In these three cases, the kind of result is the biggest, which is the final answer! However, in fact, we don’t need to compare the three cases, just compare Case 2 and Case 3, because these two cases have a larger choice of houses than the case, and the amount of money in the house is non-negative, so there is a lot of choice. , The optimal decision result is certainly not small .
So just slightly modify the previous solution:
public int rob(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] 的最优结果 int robRange(int[] nums, int start, int end) { int n = nums.length; int dp_i_1 = 0, 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; }
At this point, the second question has also been resolved.
House Robber III
The third question managed to change his mind again. The robber discovered that the house he was facing was not a row, not a circle, but a binary tree! The house is at the node of the binary tree, and the two connected houses cannot be robbed at the same time. It is indeed a legendary crime of high intelligence:
The overall thinking has not changed at all. It is still the choice of grabbing or not grabbing, and going to the choice with greater profit. We can even write the code directly according to this routine:
Map<TreeNode, Integer> memo = new HashMap<>(); public int rob(TreeNode root) { if (root == null) return 0; // Use memo to eliminate overlapping sub-problems if (memo.containsKey(root)) return memo.get(root); // grab, and then go to the next home 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)); // Don't grab, then go to the next house int not_do = rob(root.left) + rob(root.right); int res = Math.max(do_it, not_do); memo.put(root, res); return res; }
This problem is solved, the time complexity is O(N), the N
number of nodes.
PS: I have written more than 100 original articles carefully , and I have hand-in-hand brushed with 200 buckle questions, all of which are published in labuladong's algorithm cheat sheet , which is continuously updated . It is recommended to collect, brush the questions in the order of my articles , master various algorithm routines, and then cast them into the sea of questions.
But the clever point of this question is that there are more beautiful solutions. For example, the following is a solution I saw in the comment area:
int rob(TreeNode root) { int[] res = dp(root); return Math.max(res[0], res[1]); } /* returns an array of size 2 arr arr[0] means no If you grab root, the maximum amount of money you can get arr[1] represents the maximum amount of money you get if you grab root */ int[] dp(TreeNode root) { if (root == null) return new int[]{0, 0 }; int[] left = dp(root.left); int[] right = dp(root.right); // If you grab, you can't grab the next home int rob = root.val + left[0] + right[ 0]; // Do not grab, the next home can be grabbed or not, depending on the size of the income int not_rob = Math.max(left[0], left[1]) + Math.max(right[0], right[1] ); return new int[]{not_rob, rob}; }
The time complexity is O(N), and the space complexity is only the space required by the recursive function stack, without the extra space of the memo.
You see that his thinking is different from ours. He modified the definition of the recursive function and slightly modified the thinking to make the logic self-consistent, and still got the correct answer, and the code is more beautiful. This is a characteristic of the dynamic programming problem that we mentioned in the previous article "Different definitions produce different solutions".
In fact, this solution runs much faster than our solution, although the time complexity of the algorithm analysis level is the same. The reason is that this solution does not use additional memos, reducing the complexity of data operations, so the actual operating efficiency will be faster.