Computer science | Thinking clearly
Problem solving: turn a challenge into a plan
A practical guide to understanding a task, finding a useful model, designing a solution, and checking that it works—from the first question to a dependable program.
What problem solving means
A problem is a gap between a current situation and a desired outcome. Problem solving is the deliberate process of understanding that gap, choosing actions, and checking whether they actually close it. In computing, the outcome is often a program that transforms input into useful output.
A task that sounds straightforward can hide important decisions. “Sort these records” could mean alphabetical order or newest first; it may need to handle missing names, repeated entries, and records that must keep their original order when keys match. A good solution starts by making those decisions visible.
- Goal: what should be true when the work is finished?
- Inputs: what information or resources are available?
- Constraints: what must or must not happen?
- Success check: what observable evidence shows the goal was met?
Step 1: understand before you code
Read the task carefully, then restate it in your own words. Separate stated requirements from assumptions you have filled in. If an assumption affects the result, ask a question or make the assumption explicit in the program's contract.
Write down input, output, and rules
Describe the input's shape and valid range, the output's meaning, and the rules that connect them. Include failure behavior: should invalid input be rejected, corrected, skipped, or reported? These choices are part of the task, not details to decide accidentally in code.
| Question | Example: calculate a class average | Why it matters |
|---|---|---|
| What is the input? | A list of numeric scores | Clarifies data type and format |
| What is the output? | One average value | Defines what the caller receives |
| What values are valid? | Scores from 0 to 100 | Creates checks for out-of-range data |
| What about an empty list? | Report that no average exists | Prevents division by zero and ambiguous output |
| How should rounding work? | Keep full precision internally; format for display | Avoids silently losing information |
Identify constraints and priorities
Constraints limit possible solutions: a memory budget, a response-time target, a required language, accessibility needs, or a rule that existing data cannot be changed. Priorities help compare trade-offs. For a small classroom list, clarity may matter more than speed; for millions of records, scale may be essential.
If a requirement is unclear, record the uncertainty instead of hiding it in an implementation choice.
Worked example: count passing scores
Suppose scores are passing at 60 or above. For [72, 55, 60, 41, 88], the answer should be 3. Before writing code, define the rule precisely: count each score that is at least 60, including a score exactly equal to 60.
Plan with a trace
count = 0
for each score:
if score is at least 60:
count = count + 1
return count
A trace is a hand-worked record of how values change. Here it catches both common off-by-one mistakes: using > 60 rather than >= 60, or starting the count at 1 instead of 0.
| Score being checked | Passes? | Count after check |
|---|---|---|
| 72 | Yes | 1 |
| 55 | No | 1 |
| 60 | Yes, boundary included | 2 |
| 41 | No | 2 |
| 88 | Yes | 3 |
Check boundary examples
- An empty list returns
0: no score passed. - A list containing only
60returns1. - A list containing only
59returns0. - Several passing values are each counted once.
The trace supports the plan, but it does not prove that every implementation is correct. The next step is to turn the rules into tests and keep checking as the program changes.
Step 2: break the problem into smaller parts
Decomposition means dividing a large task into smaller tasks with clear responsibilities. A registration system, for example, may need to validate a form, store a record, send a confirmation, and show a result. Each part can be understood and tested separately.
Make parts small, meaningful, and connected
A useful part has a clear job and a clear boundary. “Handle everything” is too broad; “check whether the email field is present” is specific enough to implement and test. At the same time, avoid splitting work so finely that every tiny operation needs its own layer. Decompose where it reduces complexity or clarifies responsibility.
- List the main actions the solution must perform.
- Group actions that use the same information or belong to one responsibility.
- Write what each part receives and what it returns or changes.
- Order the parts according to their dependencies.
- Test each part, then test the connections between them.
In program design, a function can represent one step; a module can group related functions. Decomposition also applies outside code: gather requirements, sketch a screen, create test data, and review results are separate activities in a project.
Spot a dependency
If step B needs the output from step A, A must happen first. For example, a program must parse a date before it can compare that date with a deadline. Writing dependencies down prevents a plan from assuming information exists before it has been produced.
Step 3: choose a useful representation
A representation is a way to describe the parts of a problem so that useful operations become clear. A travel route can be a list of street names, a drawing, or a graph of intersections and roads. The best representation depends on the questions you need to answer.
| Problem shape | Possible representation | Useful when |
|---|---|---|
| Ordered sequence | Array or list | Items have positions or are processed in sequence |
| Unique lookup by key | Dictionary or hash table | You frequently ask “what value belongs to this key?” |
| Parent-child hierarchy | Tree | Folders, categories, or nested expressions need structure |
| Connections among entities | Graph | Routes, friendships, and dependencies form relationships |
| Step-by-step state changes | State diagram or table | Behavior depends on the current mode and an event |
Sketches, tables, and diagrams are legitimate problem-solving tools. A state table can expose a missing case; a graph can reveal disconnected groups; a small example can show that a proposed rule fails. Pick the simplest representation that preserves the information needed to solve the task.
Use abstraction carefully
Abstraction keeps the details needed for a question and temporarily ignores the rest. To estimate a delivery route, you may model locations as points and roads as weighted connections, ignoring the color of each building. A model is useful because it is simpler than reality, but its assumptions determine what conclusions it can support.
Ask what the model leaves out. If travel time changes with traffic, treating each road as having a fixed distance will not predict arrival time well. If a form must work for screen-reader users, a model that only considers visual layout is incomplete.
Step 4: design and compare solutions
There may be many correct ways to solve a problem. Begin with a direct baseline that you can explain. Then compare alternatives using relevant criteria: correctness, clarity, time, memory, maintainability, and behavior on unusual inputs.
Write an algorithm before choosing syntax
Pseudocode describes logic without committing to a programming language. It lets you review the idea while avoiding distractions from punctuation, library details, or type syntax.
find the earliest appointment that fits:
for each appointment in chronological order:
if its start is not before the requested time
and its duration is long enough:
return that appointment
return "no appointment found"
Notice the assumptions: the appointments are already in chronological order, and “duration is long enough” has a defined meaning. If the list is not sorted, either sort it first or change the search strategy. An algorithm is only as good as its stated conditions.
Compare options with evidence
When comparing two approaches, consider the complete task. Sorting a list costs time, so binary search may not help if there is only one lookup. A simple scan may be clearer and fast enough for a short list. For repeated lookups, building an index may pay off. Measure realistic cases rather than relying on a slogan such as “hash tables are always faster.”
| Approach | Strength | Question to ask |
|---|---|---|
| Brute force | Simple baseline; often easy to verify | Does its work scale to the expected input? |
| Divide and conquer | Splits work into smaller subproblems | Can independent results be combined correctly? |
| Greedy choice | Can be efficient and straightforward | Is there a proof that local choices lead to a global solution? |
| Dynamic programming | Reuses answers to overlapping subproblems | What state and recurrence capture all needed information? |
| Heuristic | May find useful answers quickly on hard tasks | What guarantee is missing, and how is quality evaluated? |
Step 5: test ideas and find mistakes
A test checks behavior against an expected result. A test is useful when it targets a rule, an assumption, or a likely failure—not merely because it uses a different input value.
Build a small but purposeful test set
- Typical case: ordinary input that represents common use.
- Boundary case: an empty collection, minimum value, or exact threshold.
- Invalid case: malformed or out-of-range input, if the contract specifies behavior.
- Repeated case: duplicates, repeated calls, or the same operation in a sequence.
- Large case: enough data to reveal performance or resource problems.
For the passing-score counter, the threshold cases 59 and 60 are more informative than two arbitrary passing values. For a search function, test a match at the first position, a match at the last position, duplicates, and a missing value.
Separate kinds of failure
- Syntax error: the language cannot parse the program.
- Runtime error: the program stops unexpectedly while running.
- Logic error: it runs but produces an incorrect result.
- Requirement error: it produces a result, but not the behavior users needed.
When a test fails, reproduce the smallest case that still shows the problem. Compare actual and expected results, then inspect the first point where the program's state diverges from your trace. Fix the cause rather than patching only the observed output.
Use properties as well as examples
A property describes something that should hold for many or all valid inputs. For a sorting function, the output should be ordered and contain the same items as the input. For a counter, adding one passing score should increase the count by one. Property-based testing can generate many inputs and check such rules automatically, but the properties themselves still need careful thought.
Step 6: improve with evidence
Once a solution is correct for its intended inputs, consider whether it needs improvement. First identify what matters: speed, memory, clarity, accessibility, or ease of change. Then measure or review the relevant behavior. Optimization without a target can make a program harder to understand without delivering a meaningful benefit.
- Record a baseline: correctness results and, when relevant, timing or memory use.
- Choose one bottleneck or quality issue to address.
- Change one part so the effect can be understood.
- Run the same tests and measurements again.
- Keep the change only if it preserves correctness and improves the chosen measure.
Complexity analysis helps predict growth as input size increases. Benchmarking checks actual implementations on actual workloads. Both are useful: a theoretically efficient approach can have high overhead for small inputs, while a quick test on tiny data may hide a scaling problem.
Problem solving is a communication skill
Software work usually involves other people: users who explain needs, teammates who review code, and future maintainers who must understand decisions. Share the goal, constraints, assumptions, and evidence behind a solution. A clear explanation makes it easier to find a mistaken assumption before it becomes expensive code.
Ask focused questions
Instead of asking “What should I do?”, describe what you know and what decision blocks progress: “The list can contain duplicate names. Should the result keep every duplicate, or should it show each name once?” A focused question gives others enough context to answer usefully.
Consider impact and responsibility
A solution can satisfy a technical specification and still cause harm if the specification ignores users or context. Consider privacy, accessibility, security, error costs, and who can challenge an automated decision. Test with representative users and data where appropriate; do not treat a successful demo as evidence that every user is served equally well.
A compact glossary
- Constraint
- A rule or limit that a valid solution must respect.
- Decomposition
- Breaking a large task into smaller parts with clear responsibilities.
- Abstraction
- A simplified model that retains details relevant to a question.
- Invariant
- A statement that should remain true throughout a process or loop.
- Test case
- A selected input, action, and expected behavior used to check a program.
- Heuristic
- A practical strategy that may find a useful answer without guaranteeing an optimum.
Questions for further study
- Choose a daily task and write its goal, inputs, constraints, and success check.
- For the passing-score example, what changes if scores can be missing? State a clear policy and add tests.
- Sketch two representations for a transit map. Which questions can each one answer?
- Find the smallest input that would reveal an off-by-one error in a threshold check.
- Pick one program you use. What user needs or failure cases might its specification omit?
References and further reading
The following resources offer complementary introductions to computational thinking, design, and testing.
- Jeannette M. Wing. “Computational Thinking.” Communications of the ACM, 49(3), 2006. Article DOI.
- MIT OpenCourseWare. 6.0001 Introduction to Computer Science and Programming in Python. Course materials.
- Barbara Liskov and John Guttag. Program Development in Java: Abstraction, Specification, and Object-Oriented Design. Addison-Wesley, 2000. Publisher information.
- University of Helsinki. Python Programming MOOC, introductory programming materials and exercises. Course site.
Try it yourself
- Write a precise problem statement for finding the shortest word in a list. Decide what happens for an empty list and ties.
- Design a trace table for counting values greater than zero. Trace
[-2, 0, 5, 5]. - Break a “library book checkout” feature into smaller responsibilities. State the input and result of each part.
- Write five tests for a function that checks whether a number is within a permitted range, including both boundaries.
- Pick one proposed optimization and explain what you would measure before and after applying it.
Remember: clarify the goal, surface assumptions, choose a model, make a small plan, and use evidence to check each step.
Keep exploring
Try another topic
A fresh topic suggestion is ready for you.
Explore a topic