The Art of Problem-Solving
On a rainy Monday night in downtown Boston, I sat around a small conference room table with three other programmers. I was at a Python meetup and each group was given a problem to solve together.
Although, that’s not what happened — at least for my group.
The meetup leader pulled up the problem statement on his computer and asked us where we should begin. I pulled out my red notebook and pen planning to actively think about it: jot down my thoughts, brainstorm ideas, and make a plan of attack.
But I didn’t get to do that.
By “begin” the meetup leader wanted input about what to type. People chimed in, and lines of code were being written shortly thereafter. I, on the other hand, mentally froze as the speed went from zero to one hundred in a matter of seconds.
No discussion on how to simplify the problem. No discussion on the best data structure to use. No discussion on what the problem was really asking us to do.
Instead of thinking, we rushed right into doing, and consequently we ran into roadblock after roadblock. The frustration was high and the progress was very low; we hardly made a dent.
What Not to Do
I used to take the “go one hundred miles an hour” approach to problem-solving, which is often accompanied by that “spinning your wheels” feeling.
You may have used it yourself. It’s when you glance at a problem and rush to your editor to try out the first idea that pops in your head. You get an error. You re-run the program, and get the same error. You make a tweak and re-run the program. The process continues until a new idea pops into your head, and then it begins again.
It seems like you’re making progress because you type line after line of code trying to get something — anything — to work. In reality, though, you keep running into a brick wall hoping it’ll topple down, but it stays right there.
This approach isn’t effective. It’s a drain on my time and mental energy. And it certainly didn’t work at my meetup.
A Better Way
I’ve since changed tactics.
Of the total time I spend on a problem, I’d guess that I spend about one-third of that time thinking about it. I’m not talking about daydreaming. I’m talking about an active, purposeful thinking process.
The process starts by pulling out a clean sheet of paper and writing the aim, given, and constraints for the problem. I get this information by answering these questions:
- Aim — what’s the problem asking me to do?
- Given — what details are provided?
- Constraints — what restrictions are there?
These initial steps are modeled off of a similar three-part approach mathematician George Pólya advocates in his book How to Solve It.
He writes about problem-solving in a mathematical context, but Pólya’s ideas can be applied to programming — particularly understanding the problem, which is what the three-step process helps with. It offers several benefits.
First, writing answers to these questions slows me down mentally and gets me paying attention to the details. This helps prevent me from skimming the problem statements and prematurely jumping to a conclusion.
Second, it helps me organize the information and get a bit more comfortable with what I’m being asked to do. This is because I phrase my answers in a way that’s meaning for me, which helps make the problem and its components clear in my mind.
For example, say a problem statement tells me to determine if a sentence is a pangram. For the “aim”, I’d write: “see if a sentence contains all of the letters of the alphabet.” That’s easier for me to wrap my head around.
If these steps seem trivial, think again. “It is foolish to answer a question that you do not understand,” Pólya writes.
He’s right. If you don’t really understand the problem, you’ll spend a lot of time working on the wrong problem. How many of us are guilty of such a thing? I know I am.
And so was my meetup group. We didn’t understand the problem because we never took the time to fully vet it.
Prevent unnecessary setbacks by writing out the aim, given, and constraints before you start to code. Not only will this process help ensure your understanding, but it’ll also help you eliminate the unimportant details and see how the important ones relate to each other.
That’s just a start. The thinking process continues with other tactics, like making a sketch of what I’m trying to do or solve for. This makes the abstract concrete, and therefore much easier to think about.
For one problem I made a diagram of what I was trying to find. I was given a string with newlines that represented a number matrix (eg, “1 2\n3 4”); I needed to return specific rows and columns from that matrix.
That’s a lot to keep in my head, so I drew it out. I made a little table with rows and columns, and used a red pen to circle what I needed to return.
Other active thinking tactics include writing out a list of action steps to take, brainstorming ideas about the best data structure, or simplifying the problem.
This last tactic doesn’t get enough credit. Simplifying makes a difficult problem much more manageable. It’s a lot easier to work with a handful of items than dozens of them. If I’m given a string of fifty characters, for example, I’ll get the problem working with a string of five characters.
I may need to alter the given problem a bit in order to solve a smaller variant of it. And that’s fine because the point of this step isn’t to solve the actual problem. Rather it’s intended to generate ideas, spot patterns, and reveal hidden nuances. Only then do I apply my findings to the problem at hand.
Actively thinking about a problem by writing, drawing, or solving a variant of it will help you see exactly what you need to do and the ideal data structure to use before you start working on the actual problem. That way, you enter your coding session with purpose.
Is It Worth It?
“But that seems like a lot of time!” you may say.
It’s true, it takes time to think. However, I’m convinced doing so saves you more time in the long run, not to mention frustration. When you take time to think, you’re intentional with the time spent doing.
Even still, it’s not a foolproof method. Things don’t always go as planned, and I get stuck.
So it’s back to thinking.
I draw a few more sketches. I simplify the problem in a different way. I jot down more notes. I question my ideal data structure, and think of another one. I refer to a related problem for inspiration.
Then, back to my editor to write some code and try it out. I continue this iterative process of active thinking, which can take any number of approaches, and purposeful doing. Doing so helps me see a problem from other perspectives, thereby revealing hidden layers. And that’s key.
What I’ve learned about problem-solving is this: to solve problems effectively, you have to move beyond the surface level — beyond what hits you at first glance. When you patiently think through a problem in different ways, you’ll uncover its layers.
What’s Art Got to Do with It?
The process reminds me of the many times I’ve stood in front of a piece of artwork and my initial thought is, “I have no idea what’s going on.” (I admit, this is sometimes my first reaction to some of the problems I solve.)
What to make of a banana that’s duct-taped to a wall? What to make of a few scribble-like marks on a large canvas? What to make of a massive aluminum and violet plexiglass box in the middle of the gallery?
Instead of moving on, I think. I move around the artwork, observing it from other perspectives. I look at the painting’s date and think about what historical connections I can make.
I consider my point of view, as well as the artist’s, curator’s, and — a personal favorite — that of other museum-goers. With more time, I pull out some books and do a bit of research. Not just about the artwork in question, but also history and philosophy.
Only once you go beyond the surface level and attack it from multiple perspectives does an artwork, much like a programming problem, begin to make sense.
I no longer only saw drops of paint dripped, poured, and swirled over a canvas when standing in front of Jackson Pollock’s Autumn Rhythm (Number 30).
This is a painting filled with emotion. The spontaneous and gestural brushstrokes lends credence to Pollock’s famous statement, “I want to express my feelings rather than illustrate them.” I came to realize — and appreciate — how his drip paintings were a radical departure from the figural painting that preceded it in the centuries before.
Studying art has taught me an important skill that I use each time I work on a problem: patience. Only then do the layers of an artwork reveal themselves and I begin to figure it out — which, as it turns out, is just like solving a problem.