Logic and Problem Solving: The “Smash It With A Hammer” Approach

I have a confession: I love toy problems.

I mean, not all the time. When it’s quarter-past-eleven and oh my god just do what I want you to do, please, I would probably have some colorful words to describe my feelings on toy problems. But lately I’ve been working on my metacognition and logical process when approaching these problems. Through this effort, I’ve adopted an approach that’s sped up my reasoning when it comes to toy problems.

Namely: smash it. Break it into the smallest chunks you can, and put those together into something that works.

This seems like a simple concept, but without conscious practice it can be easy to start skipping over steps. The different parts of the problem start to stack until you think you’re trying to leap a mountain instead of a series of molehills.

Here’s how I went through the process on a recent problem.

The Problem

I was prompted to create a function that would take in a string of four digits, and produce all possible “words” (letter combinations, really) that could be produced from those digits using a telephone keypad. The result should be an array of strings, presented in alphabetical order.

The Solution

The first step to any solution has to be figuring out your OICE (Output, Input, Constraints, Edge Cases). What should go into and come out of your function seems like such obvious information, why would you write it down?

Well, it gives you a clear start and end. When you know where you’re at and where you’re going, it’s a lot easier to find your way through all the stuff between. Keeping an eye on your constraints and edge cases also gives you warning signs along the way — circumstances that you know are going to give you trouble and will need to be handled.

Let’s OICE this problem:

`// output: an array of letter combos// input: a string of four digits// edge cases: none// constraints: results must be in alphabetical order`

Great. There’s the start to our roadmap.

Next step: visualization.

Visualization’s often done with white-boarding. I like to use a notebook so I can keep track of my logic across long problems. This was the diagram I made for this problem:

I knew I’d be taking in four digits. I wanted to split them so I could look at each digit at a time. From there, I needed to take each digit and get the possible letter outcomes for each one.

I’d need something to do the letter-lookup for me — the box in my diagram which would turn into a function inside my solution. The first digit’s results was easy, but the second would need to add on to the first’s results. The third would add on to that, and so the fourth. Being able to see that I’d need my function to take each digit and the previous digit’s got me thinking that reduce might be a good way to take care of this.

Now that the roadmap’s starting to become a bit clearer, it’s time to really bust this problem up into nice bite-sized pieces of pseudocode.

Pseudocoding is the step I’m always most tempted to skip, but the one I know I need the most. It takes the vague roadmap and turns it into explicit instructions. And yes, it takes a little time. But not nearly as much as you’ll waste trying to just code straight from a vague notion, getting lost, and having to start over again and again, right?

We’ll go ahead and declare our function, then think about what we need to solve this.

`function telephoneWords (digitString) {  // need a helper object of digit/letter possibilities  // break digit string into single digits  // need a function that looks at each digit and previous results    // need something to catch new results     // if there are no previous results ...      // generate first round of results    // else ...      // take each previous result ...        // and each possible letter ...          // and make a new result    // return the new results`
`}  `

Beautiful. Our picture gets clearer and clearer as we write out the solution in plain English. Turning this into code will be a lot easier than our diagram.

`function telephoneWords (digitString) {  // need a helper object of digit/letter possibilities  let helper = {    0: ['0'], // we want to access each possible letter later    1: ['1'], // so we can make these arrays now for convenience    2: ['A', 'B', 'C'],    3: ... // and so on...   };`
`  // break digit string into single digits  let digits = digitString.split('');`
`  // need a function that looks at each digit and previous results  return digits.reduce((prevRes, curDig) => {    // need something to catch new results    let newResults = [];     // if there are no previous results ...    if (!prevRes.length) {       // generate first round of results      helper[curDig].forEach(letter => newResults.push(letter));    // else ...    } else {       // take each previous result ...      prevRes.forEach(word => {         // and each possible letter ...        helper[curDig].forEach(letter => {          // and make a new result          newResults.push(word + letter);        });      });    }     // return the new results    return newResults;  }, []);  }`

Look at that! Each line of pseudocode turned into one line of code, and we’re done!

Beautiful. Look how easy that was when we took it step by step. Who wouldn’t want to break down a problem into these nice simple pieces? (The answer is me, the first time I tried this, and it took me more hours than I want to talk about).

Long story short: There are no big problems. Just a stack of small ones. Take each problem at a time, so you don’t break yourself trying to carry the whole mountain at once.

Like what you read? Give Marina Cerame a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.