Recursion sample problem
The second midquarter assessment task will be recursive problem-solving. The problem statement will follow the structure of the many recursive examples we have seen in lecture, section and assignments. You are given a problem description and function prototype and are asked to design and implement a solution. Your answer is to be presented in C++ code. We will not be especially picky about details of C++ syntax, but answer should be much closer to valid code than, say, pseudocode. It is totally okay to not get to the point that you have a complete solution by the time that you start the discussion period with your section leader – part of your time during the discussion period can be spent fleshing out the final details of the complete solution alongside your section leader.
In the discussion portion of the assessment, you will present your solution. You should be prepared to describe your recursive strategy for solving the problem. You may do this by sketching a decision tree, comparing your approach to that taken by a similar problem, and/or tracing through the operation on a simple example.
You should also be be prepared to analyze your code and explain such things as the mechanics of how your code operates, how state is maintained through the recursive calls, how different choices are explored, how base cases are structured, what test cases would be useful to confirm the correctness of your code, and so on.
You may be also asked to discuss the effect of certain changes to the code (what happens if move this base case ahead of that one?) or to propose changes that would be necessary to achieve a certain effect (how could you change the function to count all solutions instead stop at first?)
The primary skills being assessed for this task is your facility with recursive problem-solving and understanding of the mechanics of recursion and backtracking.
Sample problem statement
Write a recursive function divide that uses backtracking to divide the letters from word w into two words s1 and s2. Each letter from w must be used exactly once either in s1 or s2, and the relative order of the letters must be preserved, meaning that s1 and s2 must both be subsequences of w.
For example, the word "friendly" can be divided into "find" + "rely". This solution is valid because s1 and s2 together contain all of the letters from w, the letters in s1 and s2 appear in the same relative order as they did in w, and both s1 and s2 are valid words.
For some words, no valid division is possible. For example, consider dividing the word "stream". It would not be valid to divide into "star" + "me" (order of letters not preserved), nor "stem" + "ram" (reuses 'm'), nor "stem" + "a" ('r' not used), nor "seam" + "tr" (not in dictionary).
You will be passed two parameters: a string w to divide, and a reference to a Lexicon of dictionary words. If the function finds a valid division, it prints the two words and searches no further. If no valid division is found, the function prints nothing. The function should also return a bool value of true if a division was found and printed, or false if not.
Use the Lexicon passed to ensure that the divided words are both found in the dictionary. You must also use it to optimize your exploration by avoiding paths that cannot possibly lead to a valid solution.
The following table lists some example calls to your function, the expected return values, and possible output to appear on the console, given a Lexicon called lex:
| Call | Returns | Possible output |
divide(lex, "friendly") |
true | find + rely |
divide(lex, "standing") |
true | sting + and |
divide(lex, "midterm") |
true | mid + term |
divide(lex, "recuperate") |
true | cure + repeat |
divide(lex, "stream") |
false | Â |
If there are multiple valid divisions for word w, you may print any of them. Regardless of which division you print, your code must stop after finding a single solution; do not find/print all possible solutions.
You will need to write a helper function.
For full credit, your solution must obey the following restrictions.
- Do not declare any global variables.
- Your overall algorithm must be recursive and must use backtracking techniques to generate the results.
- Efficiency: While your code is not required to have any particular Big O, you may lose points if your code is extremely inefficient or repeatedly re-explores the same path multiple times. Pass data structures by reference to avoid making copies.
void divide(Lexicon& lex, string input) {
divideHelper(lex, input, "", "");
}
bool divideHelper(Lexicon& lex, string input, string left, string right) {
if (input.empty()) {
if (lex.contains(left) && lex.contains(right)) {
cout << left << " + " << right << endl;
return true;
} else {
return false;
}
} else if (!lex.containsPrefix(left) || !lex.containsPrefix(right)) {
return false;
} else {
char ch = input[0];
string rest = input.substr(1);
return (divideHelper(lex, rest, left + ch, right)
|| divideHelper(lex, rest, left, right + ch));
}
Additional practice
Other selected recursive problems for review/practice.
- Lecture examples
evaluate,partitionable,reducible,generateSequences,permute,listSubsets
- Section problems
isSubsequence,canMakeSum,isMeasurable,crack,longestCommonSubsequence,fewestCoinsFor,listPossiblePayments
- Textbook exercises
- Chapter 8: 8-4
countSubsetSumWays, 8-7countFifteens, 8-10listMnemonics - Chapter 9: 9-1
shortestPathLength, 9-4fillRegion, 9-5placeQueens, 9-6Knights tour, 9-9formsDominoChain, 9-10cutStock
- Chapter 8: 8-4
- Code Step by Step drills
- codestepbystep.com has a neat online practice tool. Its collection of C++ recursion/backtracking problems includes many of the above problems plus others. The practice tool has integrated unit tests that allow you to confirm the correctness of your solution.
General advice
Here is some on recursive problem solving in the context of exams/assessments:
- Many section leaders agree that recursion is one of the most difficult exam topics. They suggest that you read any recursion problems early in the exam, but if you don’t see the answer quickly, move on to other problems. This way, you can be thinking about it in the background the entire time, and you’re more likely to hit on the insight needed to solve the problem.
- Between the recursive examples from lecture, section, assignments, and textbook, you have a lot of problems for practice. Practice is an excellent way to strengthen your skills for recursion.
- Many recursion problems are variations on classic themes (permutations, subsets, and so on). When you first read the problem, try to identify what other problems it resembles that you've already seen. How could adapt a previous solution to help you solve this new problem?
- Most likely the most difficult tasks will be coming up with an appropriate recursive decomposition. Focus on these three things: what part of the problem can be handled at the current step, what are the smaller, simpler recursive call(s) to make from here, and how to combine the results from the current step and the recursive call(s) to solve the current version.
- Try to come up with the simplest base cases possible.
- Trace your recursive cases on some sample input and ensure you are always making progress toward a base case and solving the whole problem.
- Sometimes you’ll need a wrapper. If it looks like you don’t have enough data given the parameters as given, think of what other data might be necessary and see if you can incorporate this into additional arguments passed by the wrapper to the real recursive function.