Week 03 · Phase 1 · Thinking in steps

Repeating Work: Loops and a Step Counter

for, range, and while — plus counting how many steps your program really takes.

Big question: How do I make the computer repeat work, and how do I count that work?loopsstep counting≈2.5 hours
By the end of this week you can
  • make decisions with if / elif / else;
  • repeat work with a for loop and a while loop;
  • stop or skip a loop early with break and continue;
  • add a counter to any program to measure the work it does;
  • predict the step count of a loop before running it;
  • recognise why a loop inside a loop is a different animal entirely.

3.1Decisions first

Before repetition, one small tool: doing something only in certain cases. Python checks the condition, and runs the indented block only if it is true.

if / elif / else
temperature = 31

if temperature > 30:
    print("Hot")
elif temperature > 15:
    print("Pleasant")
else:
    print("Cold")
Hot

Two things trip up beginners: the colon at the end of the line, and the indentation (four spaces). In Python the indentation is not decoration — it is how the language knows which lines belong to the if.

ComparisonMeans
==is equal to (two signs — one sign is assignment!)
!=is not equal to
> < >= <=greater / less / at least / at most
and or notcombine conditions

The order matters. Python checks the branches top to bottom and takes the first one that is true, then skips the rest. That is why the "Pleasant" branch can safely say just > 15: if the temperature had been above 30 we would already have stopped at "Hot". Read a chain of elifs as "otherwise, if…".

3.2The for loop: do this n times

counting out loud
for i in range(5):
    print("step", i)
step 0 step 1 step 2 step 3 step 4

range(5) produces 0, 1, 2, 3, 4 — five values, starting at zero, stopping before five. Programmers count from zero often enough that you will stop noticing within a fortnight.

Adding up the numbers from 1 to n is the classic first loop:

a running total
n = 100
total = 0

for number in range(1, n + 1):
    total = total + number

print(f"The numbers 1 to {n} add up to {total}")
The numbers 1 to 100 add up to 5050

The pattern — start an accumulator at zero, add to it inside the loop, use it after — is worth memorising. You will write it a hundred times.

3.3The while loop: keep going until

Use for when you know how many repetitions you need, while when you do not.

halving, in code
remaining = 1000
looks = 0

while remaining > 1:
    remaining = remaining // 2   # throw away half the dictionary
    looks = looks + 1

print(f"Pages left: {remaining}, looks needed: {looks}")
Pages left: 1, looks needed: 10

That is the dictionary trick from week 1, now measured by the computer instead of by hand. Change 1000 to 1 000 000 and run it again: the answer goes to 20, not to 10 000.

Infinite loops

If the condition never becomes false, the cell runs forever. In Colab, press the stop button (■) next to the cell. Every programmer does this in their first week; it breaks nothing. The usual cause: you forgot to change, inside the loop, the very thing the condition tests.

3.4Counting the work

Here is the technique that carries the whole course. Add a variable whose only job is to count how many times the interesting line runs.

the step counter
n = 50
steps = 0
total = 0

for number in range(1, n + 1):
    total = total + number
    steps = steps + 1          # one unit of work

print(f"n = {n}, result = {total}, steps = {steps}")
n = 50, result = 1275, steps = 50

Run it with n = 50, then 100, then 200. The step count is always exactly n. Double the input, double the work: a straight-line relationship. In week 8 this gets the name O(n), but you can already see it in your own output.

Why not just time it?

For a loop this small, timing is useless — the numbers bounce around depending on what else your machine is doing. Counting is exact and repeatable. We will add the stopwatch in week 5 and use both together for the rest of the course.

3.5A loop inside a loop

Now the moment that makes people sit up. Put one loop inside another:

every pair
n = 5
steps = 0

for i in range(n):
    for j in range(n):
        steps = steps + 1

print(f"n = {n}, steps = {steps}")
n = 5, steps = 25

The inner loop runs n times for each pass of the outer loop, so the total is n × n. Fill in this table by running the code — do not guess:

nsingle loop stepsnested loop steps
5525
1010100
10010010 000
1 0001 0001 000 000
10 00010 000100 000 000

Ten times more input, a hundred times more work. This is why "it worked fine on my test data" and "it froze on the real data" are the same program. Nested loops are not forbidden — sometimes you genuinely must compare every pair — but from now on, when you write one, you should feel a small alarm go off.

3.6Deciding inside a loop: break and continue

Combining a decision with a loop is where loops earn their keep. Two keywords give you fine control: break leaves the loop immediately, and continue skips the rest of the current pass and moves on to the next.

Early exit with break. Suppose we want the first number whose square is over 1000, and we want to know how many we had to check:

stop as soon as you find it
steps = 0
answer = None

for k in range(1, 1000):
    steps = steps + 1
    if k * k > 1000:
        answer = k
        break              # found it — no need to look further

print(f"first is {answer}, checked {steps} numbers")
first is 32, checked 32 numbers

Without break the loop would grind on to 999 for no reason. Stopping early is exactly what makes a "best case" cheaper than a "worst case" — a distinction we make precise when we search lists in week 4.

Skipping with continue. Add up only the numbers from 1 to 20 that are not multiples of 3:

skip the ones you do not want
total = 0
skipped = 0

for number in range(1, 21):
    if number % 3 == 0:
        skipped = skipped + 1
        continue           # jump straight to the next number
    total = total + number

print(f"total {total}, skipped {skipped}")
total 147, skipped 6

Notice that continue did not save any work in the big-picture sense — the loop still visited all 20 numbers, so the step count is still n. It changed what happens on a pass, not how many passes there are. Keep that separation clear: skipping an item is not the same as never visiting it.

3.7Common mistakes with loops

Four loop bugs account for most of the confusion in these first weeks. Meet them here so you recognise them in your own cells tonight.

MistakeSymptomFix
Off-by-one in rangeLoop stops one short, or runs one too manyRemember range(a, b) stops before b; for 1…n use range(1, n + 1).
Resetting inside the loopYour total or counter is always tiny or wrongSet the accumulator to 0 above the loop, not inside it.
Wrong indentationA line runs once after the loop instead of every pass (or vice versa)Lines inside the loop are indented under it; lines after it are not.
Infinite whileThe cell never finishesMake sure the loop body changes the variable the condition tests.

The second one is subtle enough to show. Both loops below look reasonable; only one gives the right total for 1 to 5, which is 15:

where does total = 0 belong?
# WRONG: total is wiped clean on every pass
for number in range(1, 6):
    total = 0
    total = total + number
print("wrong version:", total)      # prints 5, not 15

# RIGHT: total starts at 0 once, then grows
total = 0
for number in range(1, 6):
    total = total + number
print("right version:", total)      # prints 15
wrong version: 5 right version: 15

In the wrong version, every pass throws the running total away and rebuilds it from zero, so at the end it only holds the last number. The lesson is the one-line habit: initialise accumulators above the loop. When a total or a counter comes out wrong, this is the first place to look.

Reading a loop like Python

When a loop confuses you, trace it for a tiny input — say n = 3 — writing down the value of every variable after each pass. Three rows of a table almost always reveal the bug, and it is far faster than staring at the whole thing.

3.8Try it yourself

Task 1 — countdown

Print 10, 9, 8 … 1, then "Lift off". Use a loop, not ten print lines.

Solution
countdown.py
for i in range(10, 0, -1):
    print(i)
print("Lift off")

Ten steps for a countdown from 10, a hundred from 100: the work is proportional to where you start.

Task 2 — count the evens, with a counter

Count how many numbers between 1 and n are even, and also count how many steps your program takes. Try n = 100, 200, 400 and note both numbers.

Solution
evens.py
n = 100
evens = 0
steps = 0

for number in range(1, n + 1):
    steps = steps + 1
    if number % 2 == 0:
        evens = evens + 1

print(f"n = {n}: {evens} evens, {steps} steps")

Steps: 100, 200, 400 — exactly n. Answer: 50, 100, 200. Both grow in a straight line, and doubling n doubles both.

Task 3 — predict before you run

Write down, on paper, the step count you expect from the nested loop for n = 300. Then run it. Then explain any surprise in one sentence.

Answer

90 000. If you guessed 600 you added when you should have multiplied — a nested loop multiplies, and that single mistake is behind most "why is my program frozen?" questions.

Task 4 — halving, generalised

Modify the while loop from §3.3 so the starting size is a variable. Record the "looks" for 1 000, 10 000, 100 000, 1 000 000. What pattern do you see?

What you should see

10, 14, 17, 20. Multiplying the input by ten adds only three or four steps. Compare that with the nested-loop table above — these two behaviours are as different as walking and teleporting.

Task 5 — first multiple, counted

Using break, find the first number from 1 upward that is a multiple of both 7 and 9, and report how many numbers you had to check. Predict the count before you run it.

Solution
first_multiple.py
steps = 0
answer = None

for k in range(1, 1000):
    steps = steps + 1
    if k % 7 == 0 and k % 9 == 0:
        answer = k
        break

print(f"answer {answer}, checked {steps}")
answer 63, checked 63

63 is 7 × 9, the first number both divide, so the loop stops after checking 63. Without break it would have run all the way to 999.

Task 6 — FizzBuzz, the classic

For numbers 1 to 20, print "Fizz" if the number is a multiple of 3, "Buzz" if a multiple of 5, "FizzBuzz" if both, and the number itself otherwise.

Solution
fizzbuzz.py
for n in range(1, 21):
    if n % 3 == 0 and n % 5 == 0:
        print("FizzBuzz")
    elif n % 3 == 0:
        print("Fizz")
    elif n % 5 == 0:
        print("Buzz")
    else:
        print(n)

The order of the branches is the whole trick: test "both" first, because a multiple of 15 is also a multiple of 3, and Python takes the first true branch. Put the "Fizz" test first and 15 would wrongly print "Fizz".

3.9Self-check

How many numbers does range(1, 11) produce?

It starts at 1 and stops before 11, giving 1…10 — ten values.

A single loop over n items takes 1 000 steps when n = 1 000. A nested double loop over the same data takes:

n × n = 1 000 × 1 000. The inner loop runs completely for every single pass of the outer one.

You double n from 500 to 1 000. Which program's step count grows the least?

Halving is the star: each doubling of the input costs exactly one extra step. Week 12 gives it a name.

What does break do inside a loop?

break exits the loop then and there. The skip-this-pass keyword is continue.

A loop that should total 1 to 5 keeps printing 5. The most likely cause is:

Resetting the accumulator each pass leaves only the last value. Initialise it once, above the loop.

3.10Homework

Due before week 4
  1. Create AA_Week03.ipynb and solve Tasks 1–6.
  2. Write a program that prints the multiplication table from 1×1 to n×n using nested loops, with a step counter. Run it for n = 5, 10, 20 and record the counts.
  3. Write a program that finds the first number above 1 that is divisible by 2, 3, 4, 5 and 6, using break, and reports how many numbers it checked. Explain the result in one sentence.
  4. Make a table in a text cell: n, single-loop steps, nested-loop steps, halving-loop steps, for n = 10, 100, 1 000, 10 000.
  5. Write five sentences on your table: which column worries you most as n grows, and why? Which would you be happy to run on a million rows?

3.11Words from this week

TermMeaning in plain words
loop / iterationRepeating a block of instructions; one pass through it.
range(a, b)The numbers from a up to but not including b.
accumulatorA variable that collects a running total or count.
step counterAn accumulator whose only job is to measure the work done.
break / continueLeave the loop now / skip to the next pass.
nested loopA loop inside a loop — the counts multiply, not add.
indentationThe four spaces that tell Python which lines belong inside a block.

3.12Chapter problem set — Skiena 2.10

These are exercises from Skiena's Algorithm Design Manual, Chapter 2, matched to this week and solved step by step. They are all about the same skill you practised above: counting how many times a loop runs, and turning that count into a tidy formula.

Problem 2-35 · how many times does the inner loop run?

Consider this fragment. The outer loop runs i from 1 to n; for each i the inner loop runs j from i up to 2*i, printing "foobar" each time. Let T(n) be the total number of "foobar"s printed. (a) Write T(n) as a summation. (b) Simplify it to a closed form.

the fragment
for i in range(1, n + 1):          # i = 1, 2, ..., n
    for j in range(i, 2 * i + 1):  # j = i, i+1, ..., 2i
        print("foobar")
Worked solution

Start with one pass of the outer loop. Fix a value of i. The inner loop runs j from i up to and including 2*i. How many whole numbers are there from i to 2*i? Count them: it is (2i − i) + 1 = i + 1. The "+1" is the same off-by-one care from §3.2 — both ends are included, so you add one. Quick check with i = 3: j takes 3, 4, 5, 6 — that is four values, and i + 1 = 4. Good.

(a) Add up every pass of the outer loop. The outer loop does this for i = 1, 2, …, n, so we add i + 1 for each of those:

T(n) = ∑i=1n (i + 1)

(b) Simplify. Split the sum into two easier sums:

T(n) = (1 + 2 + … + n) + (1 + 1 + … + 1)

The first bracket is the famous "add 1 up to n" from §3.2, which equals n(n+1)/2. The second bracket is just the number 1 added n times, which is n. So:

T(n) = n(n+1)/2 + n = (n² + n + 2n)/2 = (n² + 3n)/2

The biggest piece is the n², so for large n this grows like a nested loop should — it is Θ(n²), the two-loops-multiply behaviour from §3.5.

A step counter confirms the formula for small n:

confirm_2_35.py
for n in range(1, 6):
    steps = 0
    for i in range(1, n + 1):
        for j in range(i, 2 * i + 1):
            steps = steps + 1              # one "foobar"
    formula = (n * n + 3 * n) // 2
    print(f"n = {n}: counted {steps}, formula {formula}")
n = 1: counted 2, formula 2 n = 2: counted 5, formula 5 n = 3: counted 9, formula 9 n = 4: counted 14, formula 14 n = 5: counted 20, formula 20

Answer: (a) T(n) = ∑i=1n (i + 1); (b) T(n) = n(n+1)/2 + n = (n² + 3n)/2, which is Θ(n²).

Problem 2-32 · an alternating sum of squares

Show that the alternating sum 1² − 2² + 3² − 4² + … + (−1)k−1 k² equals (−1)k−1 · k(k+1)/2. (We will convince ourselves it is true rather than write a formal proof.)

Worked solution

What the statement says. On the left we add the squares 1, 4, 9, 16, … but flip the sign on every second one: plus, minus, plus, minus. The claim is that this messy-looking total is always just k(k+1)/2 — the plain "add 1 up to k" number — carrying a plus sign when k is odd and a minus sign when k is even (that is all (−1)k−1 does: it is +1 for odd k, −1 for even k).

Check the pattern for the first few k. The quickest way to trust an identity is to compute both sides on small cases and watch them line up:

kLeft side (the alternating sum)Right side (−1)k−1·k(k+1)/2
11 = 1+ 1·2/2 = 1
21 − 4 = −3− 2·3/2 = −3
31 − 4 + 9 = 6+ 3·4/2 = 6
41 − 4 + 9 − 16 = −10− 4·5/2 = −10
5… + 25 = 15+ 5·6/2 = 15

Why it works — pair the terms. Group each minus with the plus just before it: (1² − 2²), then (3² − 4²), and so on. Each pair is a difference of squares, and there is a neat fact hiding in it:

(2m−1)² − (2m)² = −(4m − 1) = −(2m−1) − (2m)

In plain words, each plus-minus pair collapses to just the negative sum of the two numbers involved. So after pairing, the whole alternating sum of squares turns into a plain running total of 1, 2, 3, … (with a sign) — and a plain running total of 1 up to k is exactly k(k+1)/2. The sign at the end is whatever the last term carried: plus for odd k, minus for even k. That is the right-hand side. (If k is odd there is one unpaired plus term left over at the end, and the arithmetic still lands on the same formula — worth checking yourself as an optional challenge.)

A four-line check confirms both sides agree all the way to k = 20:

confirm_2_32.py
for k in range(1, 21):
    left = sum((-1) ** (i - 1) * i * i for i in range(1, k + 1))
    right = (-1) ** (k - 1) * k * (k + 1) // 2
    print(k, left, right, left == right)
1 1 1 True 2 -3 -3 True 3 6 6 True ... 20 -210 -210 True (every row prints True)

Answer: the identity holds: 1² − 2² + … + (−1)k−1 k² = (−1)k−1 · k(k+1)/2, verified for k = 1…20 and explained by pairing consecutive terms.

Problem 2-34 · the Twelve Days of Christmas

In the song "The Twelve Days of Christmas", on the first day my true love sends 1 gift, on the second day 2 new gifts plus the 1 from before, and so on — each day you receive that day's number of gifts on top of everything from the earlier days. If Christmas lasts n days, exactly how many presents arrive in total?

Worked solution

How many arrive on a single day. On day d the true love delivers 1 + 2 + … + d presents (the first gift, plus the second, …, up to the d-th). That is the "add 1 up to d" number again, so a single day d brings d(d+1)/2 presents.

Add up all the days. The grand total over n days is the sum of each day's haul:

Total(n) = ∑d=1n d(d+1)/2

A small table makes the running total concrete:

Day nPresents that day = n(n+1)/2Total so far
111
234
3610
41020

The closed form. Those running totals 1, 4, 10, 20, … are the tetrahedral numbers (imagine stacking triangular layers of cannonballs into a pyramid). The tidy formula for the sum above is:

Total(n) = n(n+1)(n+2)/6

Check it against the table: n = 4 gives 4·5·6/6 = 20. For the real song, n = 12: 12·13·14/6 = 2184/6 = 364 — one present for (almost) every day of the year. A tiny program agrees:

confirm_2_34.py
for n in (1, 2, 3, 4, 12):
    total = 0
    for d in range(1, n + 1):
        total = total + d * (d + 1) // 2   # presents delivered on day d
    formula = n * (n + 1) * (n + 2) // 6
    print(f"n = {n}: total {total}, formula {formula}")
n = 1: total 1, formula 1 n = 2: total 4, formula 4 n = 3: total 10, formula 10 n = 4: total 20, formula 20 n = 12: total 364, formula 364

Notice the highest power is n³, so the present count grows like Θ(n³) — even faster than the nested-loop Θ(n²) tables of §3.5.

Answer: Total(n) = ∑d=1n d(d+1)/2 = n(n+1)(n+2)/6, which for n = 12 is 364 presents. The growth is Θ(n³).

Where this leads

You can repeat work and count the repetitions. Week 4 gives you something worth repeating over — lists — and you will watch the count grow with the data.