Advertisement
vladimirVenkov

6 ways to solve recursive problems

Aug 11th, 2018
101
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. Yesterday I told you the core problem with the existing resources out there on recursion: They don’t go deep enough.
  2.  
  3. Simply put, knowing how to compute a Fibonacci sequence or factorials really isn’t going to help you when it comes to the sorts of question you might actually get asked in your interviews. What if you have to find all the permutations of a list? How are those problems even related.
  4.  
  5. And what does everyone say when you’re trying to solve harder problems? “Oh just figure out what the subproblems are.” Oh yeah, thanks mom. Because if it were that easy, wouldn’t we already be doing that?
  6.  
  7. Today I want to show you the 6 patterns that you should know to solve recursive problems. If you understand the basics of these patterns, you should be able to group almost every recursive problem into one of these categories. From there, it only takes some minor adjustments to get to the solution to any given problem.
  8.  
  9. WARNING: The rest of this email is super long and technical. I recommend you pick one pattern that interests you and start there. If you try to learn it all at once, it will be really easy to get overwhelmed. There’s no need to understand this whole email in one sitting.
  10.  
  11. So let’s talk about these different patterns.
  12.  
  13. Iteration
  14. As you hopefully know, any problem that can be solved recursively can also be solved iteratively and vice versa. This is a fundamental concept behind functional languages, where you use recursion to do everything; there are no loops.
  15.  
  16. While this is not something that you’re often going to need to do recursively, this pattern can come in useful once in awhile, particularly if you want to be able to refer back to items you’ve looped through previously. For example, consider the problem of printing a linked list in reverse order. There are plenty of approaches we can take for this problem, but recursion is uniquely concise:
  17.  
  18. public static void printReverse(Node n) {
  19. if (n == null) return;
  20. printReverse(n.next);
  21. System.out.println(n.val);
  22. }
  23. For iteration, we simply make our recursive call on the remainder of our inputs, either by passing in the next node (as we did above) or by incrementing some index. While this doesn’t come up too often, this is one of the simplest applications of recursion.
  24.  
  25. Selection
  26. This is my favorite pattern to test people on because it is one of the most common patterns to come up in recursion (and dynamic programming). In this pattern, we are simply finding all of the combinations of our input that match a certain criteria.
  27.  
  28. Consider for example the 0-1 Knapsack Problem. In this problem, we have a series of items that have weights and values. We want to figure out what the maximum value is that we can achieve while remaining under some fixed weight. Many people recognize this as a dynamic programming problem, since it’s a classic example, but let’s look at it from a recursive standpoint.
  29.  
  30. What would be a brute force solution to this problem? Well we can easily validate for a given combination of items whether it is under the maximum weight, and we can also easily compute the weight for any combination. That means if we could just generate all the different combinations, we could figure out the optimal answer.
  31.  
  32. Figuring out all the combinations is the core of a selection problem. I like the term “selection” because the way our code works is to simply include/exclude, or “select”, each item in our list.
  33.  
  34. The brute force code to generate all combinations might look something like this (this is simplified, but you get the idea):
  35.  
  36. public static List<List<Integer>> combinations(int[] n) {
  37. List<List<Integer>> results = new LinkedList();
  38. combinations(n, 0, results, new LinkedList<Integer>());
  39. return results;
  40. }
  41.  
  42. private static void combinations(int[] n, int i, List<List<Integer>> results, List<Integer> path) {
  43. if (i == n.length) {
  44. results.add(path);
  45. return;
  46. }
  47. List<Integer> pathWithCurrent = new LinkedList(path);
  48. pathWithCurrent.add(n[i]);
  49. // Find all the combinations that exclude the current item
  50. combinations(n, i+1, results, path);
  51. // Find all the combinations that include the current item
  52. combinations(n, i+1, results, pathWithCurrent);
  53. }
  54. Once we understand this basic pattern, we can start to make optimizations. In many cases, we may actually be able to filter out some combinations prematurely. For example, in the Knapsack problem, we can limit our recursion to only consider combinations of items that stay below the prescribed weight.
  55.  
  56. There is much more to this pattern than I can include in this email, but it is one of the most important patterns to understand, so it is worth taking time to go through several examples. 0-1 Knapsack, Word Break, and N Queens are some good places to start.
  57.  
  58. Ordering
  59. This pattern is the permutation to Selection’s combination. Essentially here we’re looking at any case in which we want to consider different orderings of our values. The most straightforward problem here is just to figure out all of the permutations of a given set of elements, although just like with selection, we may add in additional restrictions.
  60.  
  61. Some examples of problems that fall under this category are Bogosort (sorting a list of items by generating all permutations and determining which is sorted), finding all numbers that can be made from a set of digits that match a certain property, determine all palindromic strings that can be made from a set of characters, and more.
  62.  
  63. In its most simple form, this is one way that we can brute force generate all permutations of a list. You can also see an alternative approach here:
  64.  
  65. public static List<List<Integer>> permutations(Set<Integer> n) {
  66. List<List<Integer>> results = new LinkedList();
  67. permutations(n, results, new LinkedList<Integer>());
  68. return results;
  69. }
  70.  
  71. private static void permutations(Set<Integer> n, List<List<Integer>> results, List<Integer> path) {
  72. if (n.isEmpty()) {
  73. results.add(path);
  74. return;
  75. }
  76. for (int i : n) {
  77. // For now ignore concurrent modification issue
  78. n.remove(i);
  79. List<Integer> pathWithCurrent = new LinkedList(path);
  80. pathWithCurrent.add(i);
  81. permutations(n, results, pathWithCurrent);
  82. n.add(i);
  83. }
  84. }
  85. As you can hopefully see, there is a lot of similarity between this solution and the combinations solution above. By understanding these two patterns and their variants, you can cover a huge number of the different possible recursive problems you might be asked in your interview.
  86.  
  87. Subproblems
  88. I know this may come as a surprise, since at the beginning of the email I was bashing on people who tell you to just figure out the subproblems, but there is a time and a place for this. Although I find that many recursive problems don’t have such obvious subproblems as people would lead you to believe, there are some problems in which the subproblems are obvious.
  89.  
  90. One example of this that you certainly know is the Fibonacci problem. In that case, even the mathematical definition of a Fibonacci sequence is recursive.
  91.  
  92. Another problem that frequently comes up is the Towers of Hanoi problem. In this case, we can simply break down our problem by considering the subproblem of moving the top n-1 disks. If we can move the top n-1 disks, we just move all of them to the middle pin, move our bottom disk to the destination pin, and then move the above disks again over to the destination pin. This works because all of the disks above our bottom pin are not affected by any pins below them.
  93.  
  94. In problems like this, you have to look for those subproblems with which to break up the problem. However, these types of problems don’t come up too often, so for the most part you can just memorize specific examples.
  95.  
  96. Divide and Conquer
  97. Hopefully, if you know about some of the common applications of recursion, you saw this one coming. Divide and conquer is the backbone to how we use techniques such as mergesort, binary search, depth first search, and more. In this technique, we attempt to break the problem space in half, solve each half separately, and then come back together.
  98.  
  99. I’m not going to go into too much depth on this pattern because it is primarily used as part of common algorithms that you should already know, such as the one I mentioned above, but there are a handful of problems for which this can be valuable.
  100.  
  101. For example, consider trying to determine all of the unique binary trees that we can generate from a set of numbers. If you pick a root node, you simply need to generate all of the possible variations of a left and right subtree.
  102.  
  103. Consider trying to find the maximum and minimum value of a list using the minimum number of comparisons. One way that we can do this is by splitting the list repeatedly, much the same as how we would do mergesort.
  104.  
  105. This technique generally applies to tree and sorting/searching problems where we will be splitting the problem space and then recombining the results.
  106.  
  107. Depth First Search
  108. This is our final pattern and I left it for last for two reasons:
  109.  
  110. In a lot of cases, this primarily applies to graphs and trees, which is somewhat out of the scope of our discussion on pure recursion.
  111. Almost any other recursive problem can be framed as DFS.
  112. Now in terms of graphs and trees, I’m not going to go into any real detail. DFS is a basic technique that you should really know like the back of your hand. If you don’t, it’s time to get studying.
  113.  
  114. What I would like to discuss, however, is how we can apply other problems to DFS. Almost any of the problems that we’ve discussed above can be understood using DFS.
  115.  
  116. Let’s consider the example of brute force finding all combinations. In this problem our starting point for our DFS is an empty list. Then each step, we can search down one of two different paths: Either we include the current element or we don’t in our result.
  117.  
  118. Here is a diagram of what this looks like in a tree format. combo(i) refers to the recursive call with index i in our array:
  119.  
  120. (If you don't see images below, click "Display images")
  121.  
  122.  
  123.  
  124. As you can see along the bottom, we find every possible combination of elements from the input array {1, 2, 3}.
  125.  
  126. Whether you find this visual helpful, much of recursion can be most easily explained using a tree. Whether you use DFS or one of the other patterns above is relatively unimportant.
  127.  
  128.  
  129.  
  130. Phew. That was kind of a lot, wasn’t it? Here’s a quick recap of the 6 recursive patterns:
  131.  
  132. Iteration
  133. Selection
  134. Ordering
  135. Subproblems
  136. Divide and Conquer
  137. Depth-first Search
  138. If you truly understand these 6 patterns, you will have everything that you need to solve any recursive problem that might come up during your interview.
  139.  
  140. Now I know that this may still be a lot, and this long-ass email barely scratched the surface. Tomorrow, I’m going to show you a better way to learn all of this stuff so that you can really crush your interviews and get that dream job.
  141.  
  142. For now, though, pick one of these patterns and really study it. Code up one or two brute force examples and see if you can optimize them. Work through the code and make sure you really understand it. This will really help accelerate you towards understanding recursion.
  143.  
  144. Best,
  145.  
  146. Sam
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement