10C. Planning Loops


Planning based on pre-simulation

Start by solving one or two examples by hand.

A good way to plan a loop starts with doing an example by hand. For example, suppose that we want to compute xn, where x and n are integers and n > 0. Computing 34 seems like a reasonable example.

Start with 3, and successively multiply by 3 a total of 3 times. If the power of 3 is stored in variable p, then p changes as follows.

     p
     3
     9
    27
    81


Decide on control variables

It is clear that we need variable p. But that cannot be enough. How do we know when we have the result? When doing the computation by hand, we are keeping a count in our heads. That needs to be made explicit. So let's have another control variable, k.

Do a hand simulation of how you want the loop to work.

We don't have a loop to simulate, but we can still show a simulation of what the loop should do. You can do this in any what you like that you think will work. Here is one way to do it.
     p    k    n
     3    1    4
     9    2
    27    3
    81    4

Write the pieces of the loop based on the pre-simulation

Examining the pre-simulation, initialization of p and k should clearly be
  p = x;
  k = 1;
The following statements serve to update p and k.
  p = p * x;
  i = i + 1;
How can we recognize the last line of the pre-simulation? What makes it different from the other lines? Clearly, k = n. The loop should keep going as long as condition k = n is false.

Put the pieces together

Here is a definition of function power(x,n) based on that plan.
  // power(x,n) returns x to the n-th power.
  //
  // Requirement: n > 0.

  int power(const int x, const int n)
  {
    int k = 1;
    int p = x;
    while(k != n)
    {
      p = p * x;
      k = k + 1;
    }
    return p;
  }

Check your work with a hand simulation of what you wrote.

The loop that you wrote should give exactly the same hand simulation as the pre-simulation. Here is the entire simulation for x = 3 and n = 4.
     p    k    n
     3    1    4
     9    2
    27    3
    81    4


Another example: greatest common divisor

Suppose that x and y are two nonnegative integers, not both 0. The greatest common divisor gcd(x, y) of two integers x and y is the largest integer that is a divisor of x and a divisor if y. For example, gcd(15,40) = 5.

The ancient Greek mathematician Euclid determined the following facts, where x mod y is the remainder when you divide x by y (x % y in C++).

gcd(x, 0) = x  (x ≠ 0)
gcd(x, y) = gcd(y, x mod y)  (x and y not both 0)

Let's plan and write function gcd(x, y).

Try an example by hand

You can compute gcd(15,40) by hand using Euclid's equations.
   gcd(15, 40) = gcd(40, 15)      since 15 mod 40 = 15
               = gcd(15, 10)      since 40 mod 15 = 10
               = gcd(10, 5)       since 15 mod 10 = 5
               = gcd(5, 0)        since 10 mod 5 = 0
               = 5                by the first equation above

Decide on control variables

At each step, there are two numbers, the two parameters of gcd. Let's call them m and n.

Do a hand simulation of what you want

Let's do the computation of gcd(15, 40) by showing the two parameters m and n of each gcd expression, including the one at the beginning.
      m     n
     15    40
     40    15
     15    10
     10     5
      5     0

Write the pieces of the loop based on the pre-simulation

The initialization should be clear.
  m = x;
  n = y;
How should m and n be updated? Let's try something.
  m = n;
  n = m % n;
The desired hand simulation contains the following two lines in a row.
      m     n
     15    40
     40    15
Let's do a careful hand simulation, starting with
      m     n
     15    40
The first statement of the proposed update code stores the value of n into m.
      m     n
     40    40
The second statement stores 40 % 40, which is 0, into n.
      m     n
     40     0
That does not match the desired pre-simulation, so we need to change the update code.

Would it work to write the statements in the opposite order?

  n = m % n;
  m = n;
That still does not work. Let's use names m′ and n′ for the updated values of m and n. Correct definitions of m′ and n′ are
  n′ = m % n;
  m′ = n;
The problem is that the new value of m is the old value of n and the new value of n is computed using the old values of m and n. Here is one approach that works.
  int oldm = m;
  int oldn = n;
  m = oldn;
  n = oldm % oldn;
We don't really need oldn. The change of n is the last thing done, so it cannot affect any of the computations.
  int oldm = m;
  m = n;
  n = oldm % n;

Now we have the initialization for the loop and the loop body. Looking at the pre-simulation, it is easy to see what is special about the last line: n = 0.


Put the pieces together

  int gcd(const int x, const int y)
  {
    int m = x;
    int n = y;
    while(n != 0)
    {
      int oldm = m;
      m = n;
      n = oldm % n;
    }
    return m;
  }


Using loop invariants

Look at the pre-simulation for power, with x = 3 and n = 4.

     p    k    n
     3    1    4
     9    2
    27    3
    81    4

  1. In the first line, p = 3 and k = 1. Notice that, for those values of p and k, p = xk, since 3 = 31.

  2. In the second line, p = 9 and k = 2. Notice that, for the values in the second line, p = xk since 9 = 32.

In fact, at every line, p = xk. We say that assertion p = xk is a loop invariant of this loop.

A loop invariant is an assertion about the current values of the variables that is true whenever the program is at the beginning of the loop. It cannot talk about prior values of the variables or what their values will be in the future. It only mentions the current values of the variables.

What is the point of a loop invariant? If the loop invariant is true at every line of the hand simulation, then it must be true for the last line. But in the last line, we know that k = n. (If that were false, the loop would keep going.) Putting together the two facts

p = xk
k = n

that are both true for the last line, we get that p = xn. That gives a bare-bones proof that our algorithm is correct, and increases our confidence in our function definition.

Can you find an interesting loop invariant for the loop in the gcd function? Remember that it must only concern the current values of the variables. It can talk about the current values of all of the variables, x, y, m and n.

A useful invariant for the gcd loop is

gcd(m, n) = gcd(x, y)

That holds for every line of the hand simulation. So it must hold for the last line, where n = 0. So, in the last line,

gcd(m, 0) = gcd(x, y)

Since Euclid tells us that gcd(m, 0) = m, it is clear that m is the correct result.


Summary

A good starting point for writing a function definition that involves a loop is to solve one or two examples by hand. Do a hand simulation of your work, using any variables that you need.

Once you have some sample hand simulations, determine how variables are initialized and how they are updated. Determine how to know when the loop should stop. Then put the function definition together using those three components.

Loop invariants are useful, but most students have difficulty understanding them. As you learn more, you might find that they begin to make sense. At that point, you are well on your way to becoming an expert.


Exercises

  1. Suppose that n is a positive integer. A proper divisior of n is an integer k where 0 < k < n and k is a divisor (or factor) of n. For example, 2 is a proper divisor of 6. Notice that, if 0 < k < n, you can tell whether k is a proper divisor of n by testing whether n%k == 0.

    Say that n is perfect if n is equal to the sum of all of its proper divisors. For example, 6 is perfect because the proper divisors of 6 are 1, 2 and 3 and 1 + 2 + 3 = 6.

    Write a definition of function isPerfect(n), which returns true if n is perfect. Plan the loop before starting to code.

    Answer