Programming puzzles like those on LeetCode may have clear specifications and short solutions compared to real-world computer programs, but that doesn’t make them easy. Something about the math and algorithms required to solve them makes them challenging for the average programmer (and many above-average programmers!) This year, once again, I allocated some time each day to work on the LeetCode Daily Problem, both to keep my problem-solving muscles in shape and to find effective problem-solving techniques.
As I wrote in last year’s end-of-year review and in my How to LeetCode post in the LeetCode discussions this year, the foundation for learning LeetCode-style problems is spaced repetition. While solving the daily problem and reading the discussions helps expose you to new problems and solutions, it’s far from enough. Solving a problem the first time provides an introduction to that problem and its associated concepts. But you have to solve a problem repeatedly to really understand it.
One benefit of spaced repetition is the repetition part. But it’s not just a matter of solving the same problem five times in a row. While that type of repetition might help you remember it the next day, it won’t do much for remembering it a week or a month later. That’s where the spaced part comes in. Gradually increasing the time between repetitions helps transfer the solution into long-term memory, where it’s more useful when you need to retrieve it during an interview.
To wrap up this year, I’d like to explore a specific goal of spaced repetition: learning a solution at multiple levels of abstraction.
In my How to LeetCode post, I explained why it’s useful to write a detailed model solution before investing in spaced repetition practice for a problem. I said this was a way to “polish the solution before you learn it.” In the comments, a reader thought this was backwards. His objection: “I would have thought you’d have to learn it first, and then polish it.”
I think both options can be correct. It’s important to create the best solution you can before you start practicing it. If you’re going to invest time in learning a solution, you want it to be as good as you can make it. But it’s also true that practicing a solution can illuminate parts of it that you didn’t notice when you first studied it. So it’s a virtuous cycle: as you practice a solution, you learn more about it, which helps you understand it better, which makes it easier to remember, which makes future practice more effective, which allows you to see new aspects of the problem that you can incorporate into your practice.
Levels of Abstraction
A model solution works best when it expresses the solution to a problem at different levels of abstraction, from most general to most specific: Begin with a summary of the problem and the overall solution approach. Then explain the algorithm step by step in natural language, without any code or pseudocode. (Though variable names may be useful at this point to make the explanation clearer). Next, write the algorithm in pseudocode, avoiding any language-specific details. Finally, write a real implementation that compiles, runs, and passes all the test cases.
This multi-layered approach helps you understand the solution better, and therefore remember it better. Each level of abstraction explains the problem from a different point of view. At the top level, learning the problem and solution summary for multiple problems helps you recognize problem types and remember which algorithms or techniques are required to solve them. At the next level is the natural language description, which is the core of the solution. It’s how you’ll describe the solution to an interviewer so they can approve what you’re about to implement. After that, you have the pseudocode solution, which a detailed description of the algorithm itself. Then, since the purpose of a coding interview is to prove that you can actually write code, you have to take the process a step farther and actually implement the pseudocode in a real programming language. (It’s worth noting that at least one well-known algorithm textbook doesn’t bother with this step, and just sticks with pseudocode).
Forgetting the Answer
The spaced repetition process can help you discover how well you know a solution, as measured by how much time elapses before you forget it. For example, if you practice a solution one day, and you can still successfully reproduce it the next day, that gives you some information about how well you know it. The next step is to increase the length of time between repetitions. So you might practice it again in 3 days and successfully reproduce it again, but then find that you’re unable to reproduce it 7 days after that. So the appropriate repetition time based on your current level of understanding is greater than 3 days but less than 7 days. According to learning theory, you’ll get the best results by practicing again during that interval.
In addition to narrowing down the appropriate practice interval, spaced repetition can also narrow down what part of the solution needs the most practice. To do this, ask yourself after an unsuccessful solution attempt what part of the solution you forgot. If you tried to solve the problem with the wrong algorithm, then you need to start from the beginning and learn the high-level summary. If you knew the general approach but couldn’t make much progress on the details, you can study the natural language solution. If you have a fairly good understanding of the solution details, but you missed a few steps, studying the pseudocode can help. And if you can write a pseudocode solution but have trouble translating it into a programming language, it might be best to review language syntax and common idioms used in LeetCode-type solutions.
Improving Your Model Solution
Spaced repetition practice can give you very specific feedback about your model solution. I was recently practicing LeetCode 227: Basic Calculator II, which I wrote about at the beginning of this year. At the time I wrote the article, I had practiced the solution at a few different time intervals, and it seemed fairly clear to me. But coming back to it late in the year, I noticed that I didn’t know exactly how the
result variables were related. If I was updating the model solution now, I might explain it like this: We need two variables because the problem defines two priorities for operator precedence: multiplication/division (higher priority) and addition/subtraction (lower priority). Since addition/subtraction is the lowest priority, we can update
result immediately after any addition/subtraction operation. To account for the higher priority of multiplication/division, we need a separate variable
prev that only gets updated after those operations.
No doubt that explanation can still be improved. But you don’t have to guess about when your solution needs to be updated. By keeping track of which parts of the problem you find difficult during spaced repetition practice, you can improve your model solution and remember it better the next time around. As long as you continue practicing a problem, you can keep polishing the model solution, making gradual adjustments to improve its effectiveness as a study tool. The only time a model solution should stop changing is when you know a solution well enough that you can reproduce it on demand whenever you need to.