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.

How do I make the computer repeat work, and how do I count that work?

Lesson

Follow one decision at a time

Run in Colab · predict the result first
x = 7
if x % 2 == 0:
    print("even")
else:
    print("odd")
Trace x = 7
  1. 7 % 2 = 1
  2. 1 == 0 → False
  3. take else
  4. print odd

== compares values. if / elif / else chooses the first matching branch. Indentation shows which instructions belong to it.

A loop is a repeated trace

Run in Colab · predict the result first
total = 0
for i in range(1, 5):
    total += i
print(total)
range(1, 5): include 1, stop before 5
i
1234
total after adding i
13610

total += i means total = total + i. range(n) gives 0 through n − 1. break leaves a loop; continue skips to its next turn.

Two loops: add or multiply?

Each cell is one inner-loop action
i = 0
(0, 0)(0, 1)(0, 2)
i = 1
(1, 0)(1, 1)(1, 2)
i = 2
(2, 0)(2, 1)(2, 2)
Run in Colab · predict the result first
n = 3
for i in range(n):
    for j in range(n):
        print(i, j)
Nested loopsn × n = n² actions

Two separate loops of n turns do n + n = 2n actions. Nesting makes every outer turn repeat the whole inner loop.

Halve until the stopping condition fails

Run in Colab · predict the result first
n = 20
steps = 0
while n > 1:
    n //= 2
    steps += 1
print(steps)
Four updates, then n > 1 becomes false
n
2010521
For a positive integer starting value nsteps = ⌊log₂ n⌋

For powers of two: 2ᵏ → … → 1 takes exactly k updates.

A while loop must move toward a stopping condition. Using n += 1 here would never reach it.

Practice

Practice questions

10 test questions · 4 written questions · 14 total

Each question is followed by its answer and explanation. Hard questions also include a starting hint and smaller reasoning steps.

Test questions

Choose one option. Cost questions state their model and whether the bound is tight, expected or worst-case. Here lg means log₂, and heap positions start at 1.

Question 1 · easy

Two nested loops each run n times around a constant-time body. Which is the tightest listed upper bound as n grows?

  1. O(n)
  2. O(n log n)
  3. O(n²)
  4. O(2ⁿ)

Answer: option C. Nested loops multiply.

Question 2 · medium

The running time of i = 1; while i < n: i = 3i is:

  1. Θ(log n)
  2. Θ(n)
  3. Θ(n/3)
  4. Θ(√n)

Answer: option A. Tripling reaches n after about log₃ n steps, and the base does not matter.

Question 3 · medium · course question

How many values does range(4) produce?

  1. 4
  2. 3
  3. 5
  4. 0

Answer: option A. They are 0, 1, 2 and 3. The stop value 4 is excluded.

Question 4 · medium · course question

What is the final total after total = 0 and then, for each i in range(1, 4), total = total + i?

  1. 3
  2. 4
  3. 10
  4. 6

Answer: option D. The loop visits 1, 2 and 3, so the total becomes 1, then 3, then 6.

Question 5 · medium · course question

How many times does the body run in: x = 1; while x < 8: x = 2*x?

  1. 2
  2. 3
  3. 4
  4. 8

Answer: option B. The body changes x from 1 to 2, then 4, then 8. At 8 the condition is false, so there are three executions.

Question 6 · medium · course question

An outer loop runs 3 times and its inner loop runs 4 times on every outer iteration. How often does the inner body run?

  1. 7
  2. 4
  3. 12
  4. 64

Answer: option C. Each of the three outer iterations contributes four inner executions: 3 × 4 = 12.

Question 7 · medium · course question

A loop visits i = 0, 1, 2 and runs its inner body i times. What is the total inner-body count?

  1. 3
  2. 6
  3. 9
  4. 2

Answer: option A. Add the row lengths: 0 + 1 + 2 = 3. The bounds start at zero, so this differs from summing 1 + 2 + 3.

Question 8 · medium · course question

What happens in x = 0; while x < 3: print(x), if the body contains no other statement?

  1. it prints 0, 1, 2
  2. it prints 0 once
  3. it prints 3 once
  4. it keeps printing 0 unless interrupted

Answer: option D. The body never changes x. The condition 0 < 3 remains true after each iteration.

Question 9 · medium · course question

How many values does range(2, 9, 2) produce?

  1. 3
  2. 4
  3. 5
  4. 7

Answer: option B. The values are 2, 4, 6 and 8. The step is 2 and the stop value 9 is excluded.

Question 10 · medium · course question

A loop visits [4, 7, 2, 9] and stops immediately when it finds 2. How many items does it inspect?

  1. 1
  2. 2
  3. 3
  4. 4

Answer: option C. It inspects 4, then 7, then 2. Stopping at the match means 9 is not inspected.

Written questions

Read each question together with its explanation, trace or proof. Numbering continues from the test questions.

Question 11 · easy

Two nested loops each run n times around a constant-time body. Running time? What if there are three nested loops?

Answer & reasoning

Θ(n²); Θ(n³). Nested loops multiply.

Question 12 · medium

What is the running time of this loop nest? for i = 1 to n: for j = 1 to i: (constant work)

Answer & reasoning

the inner loop runs 1 + 2 + … + n = n(n + 1)/2 times in total, so Θ(n²). Notice that “the inner loop depends on i” does not change the order compared with a full n × n nest.

Question 13 · medium

What is the running time of i = 1; while i < n: i = 2i?

Answer & reasoning

Θ(log n). The counter doubles each time, and doubling 1 until it reaches n takes ⌈lg n⌉ steps.

Question 14 · medium

What is the running time of for i = 1 to n: (j = 1; while j < n: j = 2j)?

Answer & reasoning

Θ(n log n): the outer loop runs n times and each inner loop runs about lg n times.

Three core tasks

Predict on paper, run the code, then explain any difference. Use the animations below to inspect individual steps.

1. Trace

For the sum loop, replace range(1, 5) with range(1, 6). List each total.

Check your reasoning

1, 3, 6, 10, 15: five additions.

2. Calculate

Count body actions for two nested loops, each range(8). Compare two separate loops.

Check your reasoning

Nested: 8 × 8 = 64. Separate: 8 + 8 = 16.

3. Change one thing

Start the halving loop at 32, then 31. Predict the update counts.

Check your reasoning

32 → 16 → 8 → 4 → 2 → 1 takes 5. 31 → 15 → 7 → 3 → 1 takes 4.

Explore the animations & more worked tasks

3.8Try it yourself

Task 1 — countdown

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

Animate it — see which numbers range really produces
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.

Animate it — count the evens and the steps for 100, 200, 400
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.

Animate it — after your paper prediction, watch 300 × 300 fill
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 number of integer size reductions for 1 000, 10 000, 100 000, 1 000 000. What pattern do you see?

Animate it — halve 1 000 up to 1 000 000 and compare
What you should see

9, 13, 16, 19. Multiplying the input by ten adds only three or four reductions. These are the counts for floor division until the size is at most one, not target comparisons. 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.

Animate it — watch break stop the search (and what happens without 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.

Animate it — follow the branches, or be Python yourself
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".

Check your understanding

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.
Extra material & reference
Optional depth · full technical reference

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.

  • range(5) → 0 1 2 3 4
  • range(1, 6) → 1 2 3 4 5
  • range(0, 10, 2) → 0 2 4 6 8 (step of two)

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
reductions = 0

while remaining > 1:
    remaining = remaining // 2   # halve the size, rounding down
    reductions = reductions + 1

print(f"Size left: {remaining}, reductions: {reductions}")
Size left: 1, reductions: 9

This captures the repeated-halving idea from week 1, but counts integer size reductions, not a complete search. The sizes are 1000, 500, 250, 125, 62, 31, 15, 7, 3, 1: nine reductions. Change 1000 to 1 000 000 and the count is 19. The loop does not inspect a target or count a final candidate comparison, so do not equate these exact counts with the guessing game.

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.

From the beginner notes · Lectures 2, 3

Count a loop by drawing its work

Do not count indentation levels alone. Draw one mark for each execution of the operation you care about. If the outer counter runs from 1 to n and the inner counter runs from 1 to that outer counter, the rows contain 1, 2, …, n marks. At n = 4 there are 10 marks, not 16.

The total is n(n + 1)/2. Pair the first and last rows, then the second and second-last: each pair has n + 1 marks. A full rectangular pair of loops has n² marks instead. Both eventually grow quadratically, but their exact counts differ.

A doubling counter follows a different pattern: 1, 2, 4, 8, … . Count the doublings rather than the size of the final value. We will attach the formal growth notation in Week 8.

Engineering use. For a collision check between parts, distinguish pairs counted in both orders, pairs counted only once, and a part paired with itself.

Learning goals & class plan
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.
Three-hour interactive studio

00:00–01:00: launch, predict–run–explain cycle, questions  ·  01:00–01:10: break  ·  01:10–02:00: worked variation, peer instruction, questions  ·  02:00–02:10: break  ·  02:10–03:00: core mechatronics practice, exam bridge, and exit ticket.

Ask at any point. Weekly self-checks stay private; optional extensions are not collected.

Need a slower explanation? Open the English + Türkçe reference guide.

Optional extra practice

3.10Optional Studio Extension

Optional practice · no submission or deadline
  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?
Optional reference · Words from this week

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.
Chapter problem set — Skiena 2.10

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.