Jekyll2023-12-11T21:22:01+00:00https://blog.jameswo.dev/feed.xmlJames’ BlogHouse Robber 2 - Problem Reduction2021-05-19T18:29:00+00:002021-05-19T18:29:00+00:00https://blog.jameswo.dev/lc/algorithms/house-robber/2021/05/19/House%20Robber%202%20-%20Problem%20Reduction<h2 id="introduction">Introduction</h2>
<p>I like House Robber 2 because we do not have to do much to get the solution. It showcases a cool example of problem reduction - reducing a problem down to another problem. In problem reduction, if you have the solution to the equivalent problem, you also have the solution to the original problem! So the only challenge here is recognizing the problem is similar to one you have done before (in this case, it’s House Robber 1!)</p>
<p>The problem specification: The objective, constraints, and input are the same as the ones in House Robber 1. The difference is that the houses are arranged in a circle, meaning the first house is next to the second house…and that’s the only difference.</p>
<p>Original problem can be found <a href="https://leetcode.com/problems/house-robber-ii/" target="_blank">here</a></p>
<h2 id="recognition">Recognition</h2>
<p>Important observation: Because the first and last houses are adjacent, we only have two options. The first option is that we consider choosing the first and ignore the last, or we consider choosing the last and ignore the first (recall in House Robber 1: if we pick a house, we no longer consider adjacent houses). Either way, we of course will consider all elements in between as well.</p>
<p>Let’s rephrase the two options. For the first option, since we ignore the last house, our answer can be found by finding the maximum total value when considering the subarray [2:n] (I will use 1-indexing in this article). For the second option, since we ignore the first house, our answer can be found by finding the maximum value when considering the subarray [1:n-1].</p>
<h2 id="side-note">Side Note</h2>
<p>It’s important to realize that the solution to the subproblem considering the first n-1 elements will not always pick the first element. Likewise, the solution to the subproblem considering the last n-1 elements will not always pick the last element. Indeed, the overall solution may exclude both. This may not sit right with the reader as it feels incorrect. Why exclude both? Well, that happens when you pick the second element and the second to last element. Let’s complicate our solution a little bit to remedy our skepticism.</p>
<p>Let’s boil down the 3 possible scenarios:</p>
<ol>
<li>The optimal solution involves choosing the first house, in which we ignore the last house. This solution is achieved with our subproblem considering the first n-1 houses.</li>
<li>The optimal solution involves choosing the last house, in which we ignore the first house. This solution is the solution to our subproblem considering the last n-1 houses.</li>
<li>A solution that chooses neither the first or last house. <strong>This solution is achieved by either of the solutions in 1 and 2</strong> This means the solution can be found by only considering the subarray[2:n-1], which is contained in the first 2 solutions</li>
</ol>
<p>So rest assured, our two options will get the optimal solution in every scenario.</p>
<h2 id="algorithm">Algorithm</h2>
<p>The algorithm kind of follows immediately from recognition - you can just run your solution to House Robber 2 two times, the only thing that changes is input. You can then return the best of the two options</p>
<h2 id="code">Code</h2>
<p>My solution:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Solution {
public int rob(int[] nums) {
if(nums.length ==1) return nums[0];
return Math.max(originalRobberProblem(Arrays.copyOfRange(nums,0,nums.length-1)), originalRobberProblem(Arrays.copyOfRange(nums,1,nums.length)));
}
public int originalRobberProblem(int[] nums){
int prevBest = 0;
int currentBest = 0;
for(int num : nums){
//choice of taking num
int temp = currentBest;
currentBest = Math.max(num + prevBest, currentBest);
prevBest = temp;
}
return currentBest;
}
}
</code></pre></div></div>Introduction I like House Robber 2 because we do not have to do much to get the solution. It showcases a cool example of problem reduction - reducing a problem down to another problem. In problem reduction, if you have the solution to the equivalent problem, you also have the solution to the original problem! So the only challenge here is recognizing the problem is similar to one you have done before (in this case, it’s House Robber 1!)House Robber 1 - Classic DP2021-05-16T03:20:00+00:002021-05-16T03:20:00+00:00https://blog.jameswo.dev/lc/algorithms/house-robber/2021/05/16/House%20Robber%201<h2 id="introduction">Introduction</h2>
<p>The House Robber problem introduces a pretty canonical dynamic programming approach so it’s a good starting point to getting comfortable with DP. I’ll probably write an introductory article eventually about DP, but this post will asssume some basic understanding.</p>
<p>The problem specification is pretty straight forward - you have an array of integers, where each number represents a value (presumably representing a house with some value waiting to be robbed?). You, as a robber, need to strategically pick values (rob the houses) such that you <strong><em>maximize the total value</em></strong> while <strong><em>avoid picking two adjacent values</em></strong>. So in typical fashion, you have an objective and a constraint.</p>
<p>Problem can be found <a href="https://leetcode.com/problems/house-robber/" target="_blank">here</a></p>
<h2 id="first-intuition-greedy-approach">First intuition: Greedy Approach?</h2>
<p>When I first read this problem after taking an algo class at my uni, I didn’t even recognize it as a DP problem (that’s usually 50% of the challenge with DP, recognizing it’s DP…). my first instinct was a greedy algorithm, where we naively compare two options. The first option would be to pick the even-indexed numbers (numbers at index 0, 2, 4, …). The second option would be to pick the odd-indexed numbers (numbers at index 1, 3 , 5, …). After all, these are the only two options that can allow us to pick the maximum number of values, no? Why even ignore a house when you can greedily pick it?</p>
<h2 id="problem-with-greedy-approach">Problem with Greedy Approach</h2>
<p>Well, maximizing number of values chosen != maximizing total value</p>
<p>Consider the following array: [100, 1, 1, 100, 1]</p>
<p>With our first approach, we get a total value of 102. With our second, we get a total value of 101. But we know the answer is 200 here.</p>
<p>So what can we do?</p>
<h2 id="brief-dp-discussiondetour">Brief DP Discussion/Detour</h2>
<p>Again, I’ll probably write another post talking about DP more in depth, but two important points/reminders:</p>
<ol>
<li>DP will always involve optimal substructure. This just means the answer to a problem will depend on the answer to the problem’s subproblems</li>
<li>Because a problem can be broken down into subproblems, we can form some sort of recurrence relation</li>
</ol>
<h2 id="algorithm">Algorithm</h2>
<p>So with those 2 points, let’s make an important observaton here:</p>
<p>Assume we have an instance of the problem, say [5,4,6,7,1,1]. Let’s say we already know the maximum value we can get when considering the first 2 elements (5,4), which is 5. Let’s say we also already know the maximum value we can get by only considering the first 3 elements (5,4,6), which is 11. Now, can we quickly find the maximum value we can get by considering the first 4 elements?. After all, we didn’t really change the problem all that much right? By considering one more element, there isn’t too much burden with overchoice here: <strong><em>Really, we have two choices: we can either choose to include 7, or we choose not to include 7.</em></strong></p>
<p>Let’s say we choose 7. Well, that just means we cannot pick 6, since it is adjacent to 7. With this restriction, we can reason that <strong><em>the best we can do is try to find the maximum total value that ignores the value 6.</em></strong> Recall the original problem we’re trying to solve is finding the maximum value considering the first 4 elements. Right now, we’re choosing 7 (the 4th element) and consequently we are forced to ignore 6 (the 3rd element). So now we must ask: how can we optimally choose the remaining values (the 1st and 2nd element)? After all, if we know the maximum sum for the remaining values, adding 7 this sum will give us the maximum value when considering the first 4 elements and picking the fourth element. The trick is that this maximum sum is already known! Recall that we assumed we already know the maximum value we can get by considering the first 2 elements, so calculating this is really fast!</p>
<p>Let’s consider the only other option: not picking 7. If you don’t pick 7, you’re free to pick 6 if you would like. There’s no restriction here. If we don’t pick 7, then our answer would just be the maximum total value we can get by considering the rest of the values (5,4,6). Do we know this? Yes! We already know the maximum value we can get when considering the first 3 elements.</p>
<p>So similar to the greedy approach, we’re considering two options and picking the maximumm of the two. The only difference is that our two options are much more informed :)</p>
<p>Let’s tie things up real quick. We now have the solution when we consider the first 4 elements. We also have the solution when considering the first 3 elements. Now, we can repeat the logic above to get the answer for the first 5 elements! You can see that we can repeat this process until we reach the solution when considering all the elements!</p>
<p>So the recurrence relation should be clear now. More formally, if we know f(i-1) and f(i-2), we can find out f(i) quickly (kind of like fibonacci right?). To clarify, f(i) here would represent the maximum value we can get when considering the first i elements. f(i) would then be the maximum between values[i] + f(i-2) and f(i-1).</p>
<p>If 50% of the challenge is to recognize the problem as DP, the other 40% or so is developing the recurrence relation. Other 10% is just translating the recurrence relation into code.</p>
<h2 id="code">Code</h2>
<p>The following code does specifically bottom up dynamic progrmaming instead of memoization dynamic programming. The difference will probably be discussed in another post.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Solution {
public int rob(int[] nums) {
int prevMax = 0;
int currentMax =0;
for(int num : nums){
int temp = currentMax;
currentMax = Math.max(num + prevMax, currentMax);
prevMax = temp;
}
return currentMax;
}
}
</code></pre></div></div>Introduction The House Robber problem introduces a pretty canonical dynamic programming approach so it’s a good starting point to getting comfortable with DP. I’ll probably write an introductory article eventually about DP, but this post will asssume some basic understanding.