Summary
This lesson contains no code, and not by oversight: the algorithm comes before the program, and whoever cannot describe precisely what must be done will not be able to say it in C++ either. It first teaches how to specify a problem, that is, how to state what data it receives, what result it must deliver and what relation binds them, and it shows that one and the same statement admits different specifications. It then defines the algorithm as a finite, precise and general procedure, and writes in numbered steps the algorithm of division by repeated subtraction. It teaches how to trace an algorithm by hand with a trace table, which throughout the course will be the first instrument for understanding and for debugging. It presents the three ways of combining steps (sequence, decision and repetition), with which every algorithm is written, and it shows how to justify that a repetition terminates by means of a quantity that decreases on every iteration and cannot go down indefinitely.
Learning objectives
- Specify a simple problem by stating its inputs, its outputs and the conditions they must satisfy.
- Describe an algorithm in numbered steps, without ambiguity, with explicit decisions and repetitions.
- Trace an algorithm by hand with a trace table and determine its result for a given input.
- Distinguish, in a given procedure, sequence, decision and repetition, and justify that a repetition terminates.
What a computational problem is
A computational problem is a relation between two kinds of data: those that are received, called the input, and those that must be delivered, called the output. Solving it does not consist in obtaining an answer for one case, but in having a method that, for every admissible input, produces an output satisfying the relation. “What is 17 divided by 5?” is a question; “given two integers, obtain their quotient and their remainder” is a problem, because it speaks of all pairs of integers at once.
Now, ordinary language is imprecise, and the same statement can be understood in several ways. That is why, before solving, one specifies: one writes down what goes in, what comes out and what conditions each thing must satisfy. A specification has three parts. The input lists the data with their kind (integers, decimals, words) and the conditions required of them, which are called the precondition: whoever uses the solution undertakes to respect them. The output lists the results. The relation, or postcondition, states what the output must satisfy with respect to the input: whoever writes the solution undertakes to guarantee it whenever the precondition has been respected. It follows that a specification is a contract between whoever poses the problem and whoever solves it, and that outside the precondition the solution promises nothing.
Ordinary statement: “find the largest of three numbers.” Specification:
- Input: three numbers , , (integers or decimals, with no other condition).
- Output: a number .
- Relation: is one of the three given values, and none of them is greater than ; that is, , and .
The relation does not say “the largest of the three,” which is precisely what was to be made precise, but two checkable conditions. The first requires to be one of the given values (without it, the number 1000 would satisfy the second for almost any input); the second, that none of them exceed it. And the specification settles in passing a doubt that the statement left open: if two values tie for the maximum, as in , the output is , and there is nothing wrong with that.
One and the same statement admits different specifications, and choosing among them is a decision, not an oversight. The most instructive case is integer division. For integers and , everyone agrees: the quotient and the remainder are the only integers that satisfy
Thus, for and they are and , because . But the statement “divide two integers” does not say what to do if is negative, and the question is not idle: for and there are two reasonable answers.
First specification: the remainder is never negative, . Then , and the output is , . This is the one studied in mathematics, in the lesson “Divisibility and division with remainder” of the course Algebra and Geometry I.
Second specification: the quotient is the result of the division with decimals, with its decimal part discarded, that is, rounded toward zero; the remainder is what is missing, and it has the sign of the dividend. Then is truncated to , and : the output is , .
Both satisfy ; they differ in the condition imposed on the remainder. Neither is an error, but a program that uses one believing that it uses the other will give wrong results with negative numbers. Let us anticipate a fact: C++ adopts the second, as will be seen in the lesson Arithmetic; whoever expects the first is in for a surprise.
There is a third option, which is the most common in practice: excluding the case through the precondition. If the specification requires , the problem of negative numbers disappears, not because it has been solved, but because whoever uses the solution has undertaken not to pose it. What cannot be done is to leave the question undecided. And for the same reason the specification requires : with there are no and that satisfy the relation, and the contract must say so.
A frequent error consists in specifying with an example: “the output for 17 and 5 is 3 and 2.” An example illustrates, but it does not specify, because it does not say what happens with the other inputs; infinitely many different relations agree on one case. A specification is written as a general condition, and examples serve afterward to check it.
Algorithms
Once the problem has been specified, one looks for a method to solve it. An algorithm is a sequence of instructions that, applied to any input satisfying the precondition, produces in a finite number of steps an output satisfying the postcondition. The definition contains three requirements.
- Finiteness. The algorithm is written with a finite number of instructions and, for every admissible input, it terminates after executing a finite number of them. A procedure that for some input goes on forever is not an algorithm for that input.
- Precision. Each instruction has a single meaning, and whoever executes it knows at every moment what to do and which instruction comes next. No decision is left to the judgment of whoever executes it.
- Generality. The algorithm solves the problem for all admissible inputs, not just for one case.
With some latitude, the name algorithm is also given to a procedure that is precise and terminates but that, for some admissible input, does not satisfy the postcondition: strictly speaking it is an algorithm for another problem, and it will be said to be incorrect for the specified problem. That is how the problems of this lesson will speak, since they refute incorrect algorithms with a counterexample.
A cooking recipe resembles an algorithm, and that is why it is the usual comparison; but almost no recipe is one. “Add a pinch of salt” is not precise, because a pinch is not a quantity; “bake until golden” depends on a judgment that two people would make differently; “beat several times” does not say how many. A cook fills those gaps with experience; a computer has none. From this follows the requirement that governs the whole course: an algorithm must be followable by someone who does not understand the problem, without guessing anything. If one has to know what a step is for in order to execute it, the step is badly written.
The algorithms of this course are first written in numbered steps, in English, with a few conventions. The data that change during the algorithm are named with letters or words, such as or , and are called variables. One writes “ takes the value ” to say that is computed with the current value of and the result becomes the new value of ; the previous value is lost. In the abbreviated form, which is used when taking notes, the same instruction is written with a left-pointing arrow, which on the keyboard is r <- r - b. The instructions are executed in the order of their numbers, unless one of them says otherwise.
The problem is the one specified in the previous section: input, two integers and ; output, two integers and with and . The idea is that of sharing out: from one removes groups of as long as possible, counts the groups removed, and what is left over is the remainder.
- takes the value , and takes the value .
- While holds, repeat steps 3 and 4; when it stops holding, continue at step 5.
- takes the value .
- takes the value ; go back to step 2.
- Return and , and stop.
Step 2 is a question that is asked every time: before the first iteration and after each one. Steps 3 and 4 form the body of the repetition, what is repeated. Nothing is left to the judgment of whoever executes it: at each moment there is a single next instruction.
The algorithm is slow (to divide a million by three it makes more than three hundred thousand iterations), and the school method of division is much faster; but it is correct, and its simplicity makes it ideal for learning to reason about algorithms. The course will return to the difference between a correct algorithm and an efficient one in the lesson The cost of an algorithm.
Finally, it is worth separating the algorithm from its writing. The algorithm of division by subtraction is the same whether it is written in English, in a diagram, in C++ or in any other programming language: what defines it is the sequence of operations, not the words. A program is the writing of an algorithm in a language that a machine can run, and the lesson Programs, compilers and C++ will explain how that execution takes place. If this is so, a program can be wrong in two very different ways: because the algorithm is incorrect, and then no language saves it, or because the algorithm is correct but was badly written. Telling the two cases apart is half the work of debugging, and that is why the course always writes the algorithm first.
Tracing an algorithm by hand
Reading an algorithm is not enough to know what it does; one has to execute it. Following it by hand, as the machine would, without skipping steps or guessing results, is the most useful skill of a programmer: with it one checks an algorithm before writing it, predicts the output of a program before running it and finds the error in one that fails.
The instrument is the trace table: a column for the number of the step being executed, one column per variable and, if useful, one for the conditions being evaluated; and one row for each step executed. In each row one writes the new value of the variable that changed in that step and leaves blank what did not change, so that the current value of a variable is the last one written in its column. Conditions are written down with their result.
We trace division by repeated subtraction with and . The arrow in the figure points to the step just executed; the dotted line recalls that step 4 goes back to step 2.
Proof
Step 1: the initial values are given. The input satisfies the precondition, because and .
First iteration. In step 2 it is checked that , so the body is executed: step 3 removes a group of five and step 4 counts it.
Second iteration: , and the body is executed again.
Third iteration: , and the body is executed again.
In step 2 the condition fails, because : the algorithm continues at step 5 and returns and . The output satisfies the postcondition.
The complete table has twelve rows: step 1, three iterations of three rows each (steps 2, 3 and 4), the check that fails and the return.
Two observations about the table. First: the number of iterations equals the quotient, , and step 2 was executed once more than the body, times; this always happens in a repetition of this form, because the last check is the one that fails. Second: the trace shows something that the text of the algorithm does not say at first sight; if , for example and , the condition fails the first time, the body is executed zero times, and the output is , , which is correct, because . Forgetting the case in which the repetition is not executed is one of the most frequent errors in designing algorithms, and a trace with small inputs uncovers it.
The trace of one case does not prove that the algorithm is correct: it checks one case. But a well-chosen trace (a common case, a boundary case, the case in which the repetition is not executed) uncovers most errors, and the one that fails says where they are. The lesson Designing correct loops will teach how to go from cases to a proof.
Sequence, decision and repetition
Seen up close, algorithms are built from very few pieces. There are elementary instructions (giving a value to a variable, returning a result) and three ways of combining them.
- Sequence: one instruction after another, in order. Steps 3 and 4 of the division are a sequence: first one subtracts, then one counts.
- Decision: depending on whether a condition holds or not, one part or another is executed (or one part or nothing). “If , takes the value ” is a decision.
- Repetition: while a condition holds, a part is executed again and again. Step 2 of the division, with its body, is a repetition.
Each form has a single entry and a single exit, and its interior can in turn be any of the three: the body of a repetition can contain decisions, a branch of a decision can contain a repetition, and so on. If this is so, with the three forms one builds algorithms of any complexity, just as any building is built with a few pieces that fit together. That they suffice to express every algorithm is a well-known result of the theory of programming (Corrado Böhm and Giuseppe Jacopini proved it in 1966); it is not proved here, but the whole course confirms it: the programs to be written in it, however long, will use nothing other than these three forms, written in C++ as if for decision and while or for for repetition, in the lessons Decisions and Loops.
A useful notation for seeing the shape of an algorithm is the flowchart: each instruction is a rectangle, each condition a diamond with two exits (satisfied, which the figure labels , and not satisfied, ), and the arrows say which step comes next. It is a notation, not an end in itself: it serves to take in a small algorithm at a glance, but it does not scale, and nobody writes a large program by drawing it.
The figure draws the three forms with generic instructions and and a condition , and then composes them in the diagram of division by repeated subtraction.
Proof
Sequence: is executed and then ; there is a single entry and a single exit.
Decision: if holds, is executed; if not, . Both branches meet in a single exit.
Repetition: is evaluated; if it holds, is executed and is evaluated again; if not, the repetition is left. The condition is evaluated once more than the body.
Composition: division by repeated subtraction is a sequence (the initial values, the repetition, the return) whose repetition has as its body another sequence (subtract, count).
Recognizing the three forms in a procedure written in ordinary language is the first step toward translating it. The sign of a decision is an “if,” an “in case” or a “depending on”; that of a repetition, a “while,” an “until,” a “for each” or an “as many times as.” A procedure without any of those words is a sequence, and if it must handle inputs of variable size (a list of numbers, a text), it lacks a repetition.
Does it terminate?
Finiteness is part of the definition of an algorithm, and a repetition is the only place where it can be lost: a sequence of five steps executes five steps, and a decision executes one of its branches; but a repetition executes its body as many times as the condition holds, and nothing guarantees in advance that it will stop holding.
To justify that a repetition terminates, one looks for a quantity, computed from the variables, that satisfies two conditions: it is a natural number (an integer and nonnegative) while the repetition continues, and it strictly decreases on every iteration. If this is so, the repetition terminates: a strictly decreasing sequence of natural numbers cannot be infinite, because each iteration lowers it by at least one, and from a natural number there are no more than rungs down to zero. Such a quantity is called a variant or bound function, and the lesson Designing correct loops will use it systematically.
In division by repeated subtraction, the variant is . While the repetition continues, , so is a natural number. On every iteration, takes the value , and since (precondition), the new value is strictly smaller. Therefore the repetition terminates, and it does so after at most iterations (in fact, after exactly iterations, the quotient).
Note where the precondition was used. If the algorithm is executed with , step 3 leaves unchanged, the condition always holds and the repetition never terminates: the algorithm is not wrong; it was given an input that the contract excluded. And if were negative, would grow on every iteration. The precondition is not an ornament: it is exactly what is needed for the reasoning to work.
Both conditions of the variant are necessary, and it is easy to get either of them wrong. Consider this procedure, which claims to decide whether a natural number is even: “while , takes the value ; if zero is reached, was even.” The quantity decreases on every iteration, and even so the procedure does not terminate if is odd: with it passes through , , , , and the condition never fails. What is not satisfied is the other condition: stops being a natural number, because the condition of the repetition does not prevent it from going below zero. The fix consists in repeating while and deciding at the end according to the value that remains, or ; now is a natural number on every iteration, because one subtracts only when , and it decreases: the procedure terminates.
Deciding whether a repetition terminates is not always easy. A famous example: starting from a natural number , while , if is even it is divided by two, and if it is odd it is replaced by . With the sequence is , and it terminates. Nobody has found an for which it does not terminate, and it has been checked with computers for enormous numbers of values; but nobody has managed to prove that it always terminates: this is the Collatz conjecture, an open problem in mathematics. No known quantity decreases on every iteration. The moral for whoever programs is modest and firm: an algorithm whose termination one cannot justify is not finished.
Frequent errors
- Ambiguous steps. “Repeat several times,” “until it is right,” “take a suitable number.” Each step must be executable without understanding the problem; otherwise, it is badly written.
- Forgetting the case in which the repetition is not executed. Every repetition with the condition at the beginning can execute its body zero times. One must always ask what the algorithm returns in that case, and a trace with a minimal input answers it.
- Specifying with an example. “For 17 and 5 it gives 3 and 2” is not a specification; the relation is written as a general condition, and the example illustrates it.
- Confusing the algorithm with its program. A program that fails may have a wrong algorithm or a wrong writing of it. Whoever did not write the algorithm beforehand does not know which of the two things to look for.
- Taking termination for granted. That a quantity decreases is not enough: it must also be a natural number while the repetition goes on.
Solved problems
Write an algorithm that satisfies the specification of the maximum of three numbers from the first section, and trace it by hand for the inputs and .
Solution
Idea: one goes through the input keeping the largest value seen so far in a variable . First step: takes the value . After it, is the maximum of the values seen, which are only .
Second step: if , takes the value . Third step: if , takes the value . Fourth step: return . These are three instructions in sequence, two of them decisions without an alternative branch.
Trace for : after the first step ; in the second, is false and does not change; in the third, is false and does not change. It returns .
Trace for : after the first step ; in the second, and becomes ; in the third, is false and does not change. It returns , which is correct: the tie requires no special treatment.
Justification: after each step, is one of the values seen and none of them exceeds it. This is true after the first step, and each decision preserves it: if the new value is greater, it becomes ; if not, it does not exceed . At the end all three have been seen, and satisfies the postcondition.
Had been written instead of , the result would be the same, because in case of a tie an equal value is assigned. The choice between and would matter if the position of the maximum were also requested: with the first of the tied positions is returned, and with the last; the formula calls the position .
In the Gregorian calendar, a year is a leap year if it is divisible by , except those divisible by , which are leap years only if they are also divisible by . A student proposes the algorithm “if the year is divisible by , return yes; if not, return no.” Find a year in which it fails and write a correct algorithm, with its specification.
Solution
Specification. Input: an integer (the Gregorian calendar has been in force since October 1582). Output: yes or no. Relation: the output is yes exactly when is divisible by and not by , or is divisible by .
The proposed algorithm is right for most years, and that is why it seems correct. To refute it, a year divisible by and not by suffices: is divisible by , and the algorithm says yes; but was not a leap year.
Corrected algorithm, in three chained decisions. Step 1: if is divisible by , return yes and stop. Step 2: if is divisible by , return no and stop. Step 3: if is divisible by , return yes; if not, return no.
The order of the questions is essential: they go from the most particular rule to the most general. If divisibility by were asked about first, the year would receive yes before reaching the exception. Trace for four years, one for each case: (step 1: yes), (step 2: no), (step 3: yes), (step 3: no).
The moral is not only about calendars: an algorithm is refuted by a single counterexample, and counterexamples are sought at the boundaries of the rules, not in the common cases. Testing with , and would not have uncovered anything.
Trace by hand division by repeated subtraction with and , and with and . Then justify that for every admissible input the algorithm terminates after exactly iterations and that its output satisfies the postcondition.
Solution
Trace with , : passes through , , , , , and through , , , , . The fifth check, , fails. Output: , . Here the boundary case is exact division: the fourth iteration takes place because , and that is why the condition must be and not .
Trace with , : the first check, , fails; the body is not executed even once. Output: , , which is correct.
A relation that is preserved. Before the first iteration, . On every iteration decreases by and increases by , so does not change. Therefore the equality holds after every iteration.
Termination. While the repetition goes on, , and is a natural number; on every iteration it decreases by . A strictly decreasing sequence of natural numbers is finite: the repetition terminates.
On termination, the condition has failed: . Moreover , because is subtracted only when . Together with the relation that is preserved, the output satisfies the postcondition; and since counts the iterations, the number of iterations is the quotient.
Online exercises
Trace the following algorithm by hand and write in the box the value it returns, just the number.
- takes the value , and takes the value .
- While holds, repeat steps 3 and 4; when it stops holding, continue at step 5.
- If is even, takes the value ; if it is odd, takes the value .
- takes the value ; go back to step 2.
- Return .
Your browser checks your answer instantly, without grading it.
Hint
Make the table: goes from to in the first iteration. Count one iteration for each change of , until equals .
Show solution
7The following algorithm, written in the abbreviated form, should compute the quotient and the remainder of division by repeated subtraction, but the condition of the repetition is missing. Replace the line of underscores with the correct condition, without changing anything else.
Your browser checks your answer instantly, without grading it.
Hint
Trace the algorithm by hand with and : the second iteration must take place even though the remainder is exactly .
Show solution
Input: a (integer, a >= 0), b (integer, b > 0)1. q <- 02. r <- a3. while r >= b repeat: r <- r - b q <- q + 14. return q, rProposed problems
Each problem has a hint, best opened only after attempting it, and an answer, which appears after the hint is opened.
Write the specification of the problem “sort three numbers” (input, output and relation) and point out which decisions it forces that the statement did not make. Then check that the specification rules out these two outputs for the input : and , and that it admits .
Hint
A “sorted” output may have nothing to do with the input. What condition links the numbers of the output with those of the input, including the repeated ones? Increasing or decreasing? Strict or not?
Answer
Input: three numbers , , . Output: three numbers , , . Relation: , and is a rearrangement of , that is, it contains the same values with the same repetitions. Decisions that the statement did not make: the direction (here, from smallest to largest); that the order is not strict ( and not , because with repeated values a strict order is impossible); and that the output must be a rearrangement. is sorted but contains a that was not there; has four values; satisfies both conditions. Without the rearrangement condition, would be a correct output for any input.
Write an algorithm in numbered steps that receives a natural number and returns its digits from the last to the first (for , it returns , , , ), using only the quotient and the remainder of division by . Trace it by hand for and for , and justify that it terminates for every .
Hint
The remainder of dividing by is the last digit, and the quotient is the number without it. Decide whether the condition is asked before or after returning a digit, and look at what happens with , which has one digit.
Answer
A correct algorithm: (1) return the remainder of dividing by ; (2) takes the value of the quotient of dividing by ; (3) if , go back to step 1; if not, stop. The question comes after the body, so the body is executed at least once: for it returns , which is its only digit. If the question were asked at the beginning (“while ”), for nothing would be returned; this is the error of forgetting the case in which the repetition is not executed. Trace for : , , , are returned, and passes through , , , . Termination: the variant is , a natural number; if one goes back to step 1 it is because , and for the quotient by is strictly smaller than , so strictly decreases on every iteration.
Coins of , and units are available, in unlimited quantity. Specification: input, a natural number ; output, three natural numbers , , with and the smallest possible number of coins, . Someone proposes the following algorithm. Find an input on which it fails, explain each defect and correct it.
- , and take the value .
- If , takes the value and takes the value .
- While : takes the value and takes the value .
- While : takes the value and takes the value .
- Return , and .
Hint
The sum of the coins is always ; what can fail is the number of coins. Try an exact multiple of and an amount of more than , and ask yourself which way of combining steps step 2 uses and which one it should use.
Answer
There are two defects. First, step 3 uses : with it returns no coin of and returns ten of , instead of one. Second, step 2 is a decision where a repetition is needed: with , the algorithm as written returns one coin of , six of and ten of (seventeen coins), and even with step 3 already corrected it would return one of and seven of (eight), instead of two of and two of (four). The input shows both defects: the algorithm returns one of , four of and ten of , fifteen coins, instead of two. Correction: step 2, “while , …”; step 3, “while , …”. Corrected in this way, the algorithm always takes the largest coin that fits, and with these three coins that gives the smallest number: the units digit of can only be paid with coins of , and of what remains it is best to pay as much as possible with coins of , because five of are worth as much as one of . With other coins, for example of , and for , always taking the largest one no longer gives the minimum ( versus ).
Completed
Sign in to save your progress