What do self-driving cars, music recommendation apps, and weather forecasts have in common? They all rely on people who are good at solving computational problems. Programming is not mainly about memorizing commands in a language; it is about learning how to think clearly and systematically so that a computer can help you solve real problems.
In this lesson, we explore how the process of programming is really the process of solving computational problems step by step. You will see how an idea becomes a precise problem, then an algorithm, and finally working code that runs on a machine.
Before writing a single line of code, a programmer needs a problem to solve. A computational problem is a task that can be solved by a series of mechanical steps that a computer could follow.
Every computational problem has at least three key parts:
For example, consider building a program that calculates the average of test scores:
The process might be described mathematically as: if the scores are \(s_1, s_2, s_3, s_4\), then the average \(\bar{s}\) is
\[\bar{s} = \frac{s_1 + s_2 + s_3 + s_4}{4}.\]
A problem is computational when it can be turned into a finite list of unambiguous steps that an automatic machine can perform. Tasks like “judge which song is better” are vague; tasks like “sort songs by number of plays” are concrete and computational.
Computational problems also usually involve constraints, such as limits on time, memory, or accuracy. For instance, finding a route for a delivery truck must be done quickly enough to be useful, and often with limited fuel or time.
Most interesting problems do not start out nicely defined. Part of programming is turning messy, real-world goals into precise computational problems the computer can actually solve.
This involves several thinking steps:
Consider a bus arrival display at a bus stop. The goal is: “Show how many minutes until the next bus arrives.” To turn this into a computational problem, we might decide:
If the current time is \(14:05\) and the next bus is at \(14:17\), then the remaining time in minutes can be written as
\[t_{\textrm{remain}} = 17 - 5 = 12.\]
Another example is designing a simple game. The vague request “Make a fun game” is not yet a computational problem. But if we say, “Create a game where a character moves left or right with arrow keys and loses a life when touching an obstacle,” then we can identify inputs (key presses, positions), processes (update positions, detect collisions), and outputs (new positions, scores, remaining lives).
Good programmers ask many questions at this stage. Solving the wrong well-defined problem perfectly is still a failure if it does not match the real need.
Once a problem is defined, the next step is to design an algorithm. An algorithm is a precise, step-by-step procedure that takes inputs and produces the correct outputs. It is like a recipe: each step must be clear, in order, and unambiguous.
Algorithms are central because they represent the logic of your solution, independent of any programming language. The structure of this logic, with decisions and loops, is what you would see in a flowchart such as the one in [Figure 1], where each box represents a specific step or decision in the process.
Key properties of a good algorithm include:
As a concrete example, consider the problem: given three numbers, determine the largest. One possible algorithm in plain language is:
If the three numbers are \(5, 9, 4\), you would start with \(\textrm{max} = 5\), then compare with \(9\) and update \(\textrm{max} = 9\), then compare with \(4\) and leave \(\textrm{max} = 9\). The output is \(9\).
Algorithms are often written in pseudocode, a mix of natural language and code-like structure, to make the logic clear without worrying about exact syntax. For the max-of-three example, pseudocode might look like:
<pre><code>max = a
if b > max then
max = b
if c > max then
max = c
output max</code></pre>
Another example is an algorithm to count how many test scores are at least \(80\) in a list. You could outline it as:
If the scores are \(72, 81, 90, 67, 88\), then the scores at least \(80\) are \(81, 90, 88\), so the output is \(3\).

To turn algorithms into programs, we need to represent both data and the control flow of the steps. Programming languages give us tools for both.
Data is represented using variables and data types. For example, an integer variable might store a score, a real-number variable might store a temperature like \(21.5\), and a string might hold a name like “Alex”. A variable is a named place in memory whose value can change over time as the program runs.
The control of the algorithm is represented using three main structures, which form the backbone of most programs, as illustrated in [Figure 2]:
A simple sequence might be: read input, compute result, print result. For example, to compute the area of a rectangle with length \(l\) and width \(w\), a program might:
A basic example of selection is grading. Suppose we give a grade of “Pass” when a score \(s\) satisfies \(s \ge 50\), and “Fail” otherwise. The condition \(s \ge 50\) controls which branch the program takes.
Iteration appears whenever we need to repeat similar steps, like checking every score in a list or simulating many frames in a game. If we want to compute the sum of the integers from \(1\) to \(n\), we could use a loop:
Mathematically, the final sum is
\[\textrm{sum} = 1 + 2 + 3 + \dots + n.\]
All real programs are built from combinations of data representations (variables, arrays, objects) and these control structures. When you write code in any modern language, you are just expressing your algorithm using these basic building blocks.

Once an algorithm is clear, a programmer translates it into a specific programming language such as Python, Java, or JavaScript. Each language has its own syntax, just like human languages have grammar rules.
For example, the max-of-three algorithm from earlier might be written in a Python-like style as:
<pre><code>max_value = a
if b > max_value:
max_value = b
if c > max_value:
max_value = c
print(max_value)</code></pre>
The underlying logic is identical to the pseudocode; only the surface form (syntax) changes. This shows why clear algorithms are so important: once the logic is right, switching between languages is mostly about learning different ways to say the same thing.
Languages also differ in whether they are compiled or interpreted:
Either way, the computer ultimately performs low-level operations like addition, comparison, and moving data in memory, following the algorithm you defined.
When writing code, programmers must watch out for different kinds of errors:
Identifying and fixing these errors is a crucial part of the programming process.
Programming is rarely a straight line from idea to perfect solution. It is an iterative process: you design, code, test, debug, and refine, then repeat, as the cycle in [Figure 3] illustrates.
Testing means running the program with specific inputs, called test cases, to check whether the outputs are correct. Good testing includes:
For example, if you wrote a program to compute the average of \(n\) scores, you should test cases like \(n = 1\), \(n = 2\), a large \(n\), and perhaps \(n = 0\) to see how the program behaves.
Debugging is the process of finding and fixing the cause of incorrect behavior. Common debugging strategies include:
After a program is correct, programmers often refine it. Refinement can involve making the code easier to read, or making the algorithm more efficient in time or memory. For example, the sum \(1 + 2 + 3 + \dots + n\) does not actually need a loop; we can use the formula
\[S = \frac{n(n + 1)}{2}.\]
For a large \(n\), using this formula is much faster than adding each number separately, because it involves just a few arithmetic operations instead of \(n\) of them.
This cycle of designing, coding, testing, debugging, and improving continues until the program meets its requirements and performs well enough in the real world. Later, when new features are requested or conditions change, the cycle starts again, which is why seeing programming as an ongoing problem-solving process is so important.

Experienced programmers use specific strategies to handle complex problems. Many of these strategies are also useful outside of computing.
1. Decomposition
Decomposition means breaking a large problem into smaller, more manageable subproblems. For instance, building a simple social media app could be decomposed into:
Each subproblem becomes its own computational problem with its own inputs, processes, and outputs. This is often called modular design.
2. Pattern Recognition
Pattern recognition means noticing similarities between different problems. If you recognize that counting how many students passed an exam is structurally similar to counting how many pixels in an image are bright, you can reuse the same looping pattern and just change the data and the condition.
3. Abstraction
Abstraction means focusing on the important details and ignoring irrelevant ones. For example, when designing an algorithm for online payments, you might abstract away the exact design of the bank’s internal systems and think in terms of “request payment”, “confirm payment”, and “record transaction”.
In code, functions and classes are tools for abstraction. A function that computes \(\textrm{distance} = \sqrt{(x_2 - x_1)^2 + (y_2 - y_1)^2}\) between two points lets you ignore how the distance is calculated every time you call it.
4. Stepwise Refinement
Stepwise refinement means starting with a high-level description of the solution and gradually filling in more detail. For example:
This approach helps prevent getting lost in details too early and mirrors how many complex systems are actually built in industry.
The process you are learning—defining problems, designing algorithms, coding, and refining—is used in almost every modern field.
In all of these examples, professionals follow the same general process you are practicing: understand the problem, carefully define it, design and analyze algorithms, implement them in code, and then test and improve them.
Programming is fundamentally about solving computational problems using a systematic process. You begin by turning a vague real-world situation into a well-defined problem with clear inputs, processes, and outputs. You then design an algorithm—a precise, step-by-step plan—with properties like correctness, clarity, and efficiency.
Next, you represent this algorithm using the tools of programming: data (variables and types) and control structures (sequence, selection, and iteration), as shown conceptually in [Figure 2]. You translate the algorithm into code in a specific language, handle syntax and runtime issues, and use iterative testing and debugging, following the cycle in [Figure 3], to move closer to a correct and efficient solution.
Throughout, you apply strategic thinking techniques such as decomposition, pattern recognition, abstraction, and stepwise refinement to manage complexity. The same principles power systems used in medicine, transportation, finance, social media, and cybersecurity. Seeing programming as a disciplined form of problem solving prepares you not only to write code, but also to think more clearly about complex challenges in many areas of life. 🚀