Skip to content

Commit f24abcc

Browse files
authored
Merge pull request labuladong#207 from xiaochuhub/english
translated the house robber series and modified the REAMD.MD, fixed labuladong#205
2 parents 6f4032a + e9c843e commit f24abcc

File tree

10 files changed

+230
-1
lines changed

10 files changed

+230
-1
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ This command specifies the `english` branch and limit the depth of clone, get ri
8383
* [KMP Algorithm In Detail](dynamic_programming/KMPCharacterMatchingAlgorithmInDynamicProgramming.md)
8484
* [动态规划详解](dynamic_programming/动态规划详解进阶.md)
8585
* [团灭 LeetCode 股票买卖问题](dynamic_programming/团灭股票问题.md)
86-
* [团灭 LeetCode 打家劫舍问题](dynamic_programming/抢房子.md)
86+
* [House Robber Series](dynamic_programming/HouseRobber.md)
8787

8888
* V. Common Knowledge
8989
* [Difference Between Process and Thread in Linux](common_knowledge/linuxProcess.md)

dynamic_programming/HouseRobber.md

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
# House Robber series
2+
3+
**Translator**: [cooker](https://github.com/xiaochuhub)
4+
5+
**Author**: [labuladong](https://github.com/labuladong)
6+
7+
I find that this series of problems are highly praised. They are representative and skillful dynamic planning problems.
8+
Today I will introduce an generalized method to solve all of these problems.
9+
10+
House robber series includes three problems. The difficulty design is very reasonable and progressive.
11+
The first ([house robber](https://leetcode.com/problems/house-robber/)) is a standard dynamic programming problem.
12+
13+
The second ([house robber ii](https://leetcode.com/problems/house-robber-ii/)) include incorporates the condition of a circular array.
14+
15+
The third ([house robber iii](https://leetcode.com/problems/house-robber-iii/)) is quite amazing which combines the bottom-up and top-down solutions of dynamic programming with a binary tree.
16+
If you haven't done it, I highly recommend this series of problems.
17+
18+
### House Robber I
19+
20+
![title](../pictures/robber/rob1.jpg)
21+
22+
```java
23+
public int rob(int[] nums);
24+
```
25+
26+
The problem is easy to understand and the characteristics of dynamic programming are quite obvious. We have summarized the [Dynamic programming Detailed Explanation] before, **the key points to solve the dynamic programming problem is to find [state] and [choice]**.
27+
28+
Imagine that you are this professional robber. You walk through this row of houses from left to right. There are two **choices** in front of each house: `rob` or `not rob`.
29+
30+
`rob`: If you rob this house, then you **definitely** can't rob the adjacent houses, you can only start the next rob from the house after next.
31+
32+
`not rob`: If you don't rob this house, then you can walk to the next house and continue making choices.
33+
34+
When you walked past the last house, you don't have to rob. The money you could rob is obviously 0 (**base case**).
35+
36+
The above logic is very simple. In fact, the **state** and **choice** have been clearly defined: **The index of the house in front of you is the `state`, and rob or not rob is `choice`**.
37+
38+
![1](../pictures/robber/1.jpg)
39+
40+
In these two choices, you need to choose a larger result each time. You end up with the most money you can rob:
41+
```java
42+
// main function
43+
public int rob(int[] nums) {
44+
return dp(nums, 0);
45+
}
46+
// return nums[start..] Maximum value that can be robbed
47+
private int dp(int[] nums, int start) {
48+
if (start >= nums.length) {
49+
return 0;
50+
}
51+
52+
int res = Math.max(
53+
// not rob, walk to the next house
54+
dp(nums, start + 1),
55+
// rob,walk to the house after next
56+
nums[start] + dp(nums, start + 2)
57+
);
58+
return res;
59+
}
60+
```
61+
62+
After clearing the state transition, we can find that there is an overlap sub-problem for the same `start` position, such as the following figure:
63+
![2](../pictures/robber/2.jpg)
64+
65+
Thieves have multiple choices to go to this position. Wouldn't it be a waste of time if they entered recursion every time? So there are overlapping sub-problems that can be optimized with memos:
66+
67+
```java
68+
private int[] memo;
69+
// main function
70+
public int rob(int[] nums) {
71+
// initialize the memos
72+
memo = new int[nums.length];
73+
Arrays.fill(memo, -1);
74+
// robber robs from house 0
75+
return dp(nums, 0);
76+
}
77+
78+
// return dp[start..] Maximum value that can be robbed
79+
private int dp(int[] nums, int start) {
80+
if (start >= nums.length) {
81+
return 0;
82+
}
83+
// Avoid duplicate processing
84+
if (memo[start] != -1) return memo[start];
85+
86+
int res = Math.max(dp(nums, start + 1),
87+
nums[start] + dp(nums, start + 2));
88+
// record the result to memos
89+
memo[start] = res;
90+
return res;
91+
}
92+
```
93+
94+
This is the top-down dynamic programming solution. We can also modify it slightly and write the **bottom-up** solution:
95+
```java
96+
int rob(int[] nums) {
97+
int n = nums.length;
98+
// dp[i] = x: start rob at i-th house, the maximum money you can get is x
99+
// base case: dp[n] = 0
100+
int[] dp = new int[n + 2];
101+
for (int i = n - 1; i >= 0; i--) {
102+
dp[i] = Math.max(dp[i + 1], nums[i] + dp[i + 2]);
103+
}
104+
return dp[0];
105+
}
106+
```
107+
108+
We also found that the state transition is only related to the two recent states of `dp [i]`, so it can be further optimized to reduce the space complexity to O(1).
109+
110+
```java
111+
int rob(int[] nums) {
112+
int n = nums.length;
113+
// record dp[i+1] and dp[i+2]
114+
int dp_i_1 = 0, dp_i_2 = 0;
115+
// record dp[i]
116+
int dp_i = 0;
117+
for (int i = n - 1; i >= 0; i--) {
118+
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
119+
dp_i_2 = dp_i_1;
120+
dp_i_1 = dp_i;
121+
}
122+
return dp_i;
123+
}
124+
```
125+
126+
The above process has been explained in detail in [Dynamic programming Detailed Explanation]. I believe that everyone can catch it. I think the next problem is more interesting, and we need to make some clever changes based on our current thinking.
127+
128+
### House Robber II
129+
130+
![title](../pictures/robber/rob2.jpg)
131+
132+
This question is basically the same as the first description. The robber still cannot rob adjacent houses. The input is still an array, but these houses are not in a row but arranged in a **circle**.
133+
134+
In other words, the first house and the last house are also adjacent and cannot be robbed at the same time. For example, if the input array `nums = [2,3,2]`, the result returned by the algorithm should be 3 instead of 4, because the beginning and end cann't be robbed at the same time.
135+
136+
It seems that this constraint should not be difficult to solve. We mentioned a solution for circular arrays in [a monotonic stack solve Next Greater Number]. So how to deal with this problem?
137+
138+
First of all, the first and last rooms cannot be robbed at the same time, then there are only three possible situations: case I, either they are not robbed; case II the first house is robbed and the last one is not robbed; case III, the first house is not robbed and the last one is robbed;
139+
140+
![3](../pictures/robber/3.jpg)
141+
142+
That's easy. The solution is the maximum of these three cases. However, in fact, we don't need to compare three cases, just compare case II and case III. **Because these two cases have more room to choose than the case I, the money in the house is non-negative. So the optimal decision result is certainly not small if we have more choice**.
143+
144+
So just modify the previous solution slightly:
145+
146+
```java
147+
public int rob(int[] nums) {
148+
int n = nums.length;
149+
if (n == 1) return nums[0];
150+
return Math.max(robRange(nums, 0, n - 2),
151+
robRange(nums, 1, n - 1));
152+
}
153+
154+
// Calculate the optimal result for only the closed interval [start, end]
155+
int robRange(int[] nums, int start, int end) {
156+
int n = nums.length;
157+
int dp_i_1 = 0, dp_i_2 = 0;
158+
int dp_i = 0;
159+
for (int i = end; i >= start; i--) {
160+
dp_i = Math.max(dp_i_1, nums[i] + dp_i_2);
161+
dp_i_2 = dp_i_1;
162+
dp_i_1 = dp_i;
163+
}
164+
return dp_i;
165+
}
166+
```
167+
168+
At this point, the second problem has also been solved.
169+
170+
### House Robber III
171+
172+
The third question changes the pattern again. The house now is arranged not a row, not a circle, but a binary tree! The house is on the node of the binary tree. The two connected houses cannot be robbed at the same time. It is indeed a legendary high IQ crime:
173+
![title](../pictures/robber/rob3.jpg)
174+
175+
The overall thinking hasn't changed at all, we need to choose the option of robbing or not robbing, and make choice with higher returns. We can even write the code directly according to this routine:
176+
```java
177+
Map<TreeNode, Integer> memo = new HashMap<>();
178+
public int rob(TreeNode root) {
179+
if (root == null) return 0;
180+
// Eliminating overlapping subproblems with memos
181+
if (memo.containsKey(root))
182+
return memo.get(root);
183+
// rob, walk to the house after next
184+
int do_it = root.val
185+
+ (root.left == null ?
186+
0 : rob(root.left.left) + rob(root.left.right))
187+
+ (root.right == null ?
188+
0 : rob(root.right.left) + rob(root.right.right));
189+
// not rob, walk to the next house
190+
int not_do = rob(root.left) + rob(root.right);
191+
192+
int res = Math.max(do_it, not_do);
193+
memo.put(root, res);
194+
return res;
195+
}
196+
```
197+
198+
This problem is solved, the time complexity O (N), `N` is the number of nodes.
199+
200+
But what makes me think that this problem is clever is that there are more beautiful solutions. For example, here is a solution I saw in the comment:
201+
```java
202+
int rob(TreeNode root) {
203+
int[] res = dp(root);
204+
return Math.max(res[0], res[1]);
205+
}
206+
207+
/* return an array of size 2 arr
208+
arr [0] means the maximum amount of money you get if you do not rob root
209+
arr [1] means the maximum amount of money you get if you rob root */
210+
int[] dp(TreeNode root) {
211+
if (root == null)
212+
return new int[]{0, 0};
213+
int[] left = dp(root.left);
214+
int[] right = dp(root.right);
215+
// rob, walk to the house after next
216+
int rob = root.val + left[0] + right[0];
217+
// not rob, The next home can be robbed or not, depending on the size of the income
218+
int not_rob = Math.max(left[0], left[1])
219+
+ Math.max(right[0], right[1]);
220+
221+
return new int[]{not_rob, rob};
222+
}
223+
```
224+
225+
The time complexity is O (N). The space complexity is only the space required by the recursive function stack, and no extra space is needed for the memo.
226+
227+
His thinking is different from ours. He has modified the definition of recursive functions and slightly modified his thinking so that the logic is self-consistent, he still gets the correct answer, and the code is more beautiful. This is a characteristic of the dynamic programming problem that we mentioned earlier in [Different Definitions Generate Different Solutions].
228+
229+
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, which reduces the complexity of data operations, so the actual operation efficiency will be faster.

pictures/robber/1.jpg

-83.2 KB
Loading

pictures/robber/2.jpg

-79.3 KB
Loading

pictures/robber/3.jpg

229 KB
Loading

pictures/robber/rob1.jpg

159 KB
Loading

pictures/robber/rob2.jpg

162 KB
Loading

pictures/robber/rob3.jpg

121 KB
Loading

pictures/robber/title.png

-111 KB
Binary file not shown.

pictures/robber/title1.png

-35.3 KB
Binary file not shown.

0 commit comments

Comments
 (0)