Unit 1: Beat the Clock
Right is not enough — it has to be fast.
Unit 1 of 25 in Competitive programming for kids. Its 4 lessons are One Second, That Is All, Naming the Shape, The Limits Tell You the Plan and The Same Answer, Three Speeds — below is everything each one explains, and a question or two from it to try.
Every sample on this page was run through real Python before it shipped, and prints exactly what it says it prints.
This unit is free for ever, because the first two units of every track are. Try it in the app.
⏱️ One Second, That Is All
A contest gives your program a time limit
You send in a program. A machine runs it on secret data and gives it about one second. Correct but slow scores exactly the same as wrong.
So every problem here has two answers: the one that works, and the one that works in time.
Python
total = 0
for i in range(1, 1001):
total += i
print(total)
print(1000 * 1001 // 2)
It prints
500500 500500
Same answer, a thousand times fewer steps
The loop added a thousand numbers. The formula did one multiply and one divide.
For a thousand nobody notices. Watch what happens when the number gets big: the formula answers instantly for a number the loop could not finish counting to in your lifetime.
Python
def slow(n):
total = 0
for i in range(1, n + 1):
total += i
return total
def fast(n):
return n * (n + 1) // 2
print(slow(100000))
print(fast(100000))
print(fast(1000000000000))
It prints
5000050000 5000050000 500000000000500000000000
Count steps, not seconds
Seconds measure the computer. Steps measure the plan, and the plan is the part you choose.
So count how many times the innermost line runs. A loop inside a loop runs its inside line n times for each of n rounds — and that multiplies.
Python
def steps(n):
count = 0
for i in range(n):
for j in range(n):
count += 1
return count
for n in [10, 100, 500]:
print(n, steps(n))
It prints
10 100 100 10000 500 250000
The number to keep in your head
A rough rule that has never let anyone down: a computer manages roughly 100 million simple steps in a second.
So before writing anything, work out how many steps your plan takes. If the answer has more than nine digits, it is the wrong plan — and no amount of tidying the code will save it.
Try it yourself
There are 100000 items and your plan checks every pair. Roughly how many steps is that?
- About 200 thousand
- About 1 million
- About 5 billion
- About 100
How many pairs does this count?
Python
count = 0
n = 5
for i in range(n):
for j in range(i + 1, n):
count += 1
print(count)
Answer them in the app
📈 Naming the Shape
Nobody counts the exact steps
Two loops of n is 2n steps, and a tidier version might be n + 3. Nobody cares: for big n both are "about n", and doubling the machine beats both.
What matters is the shape — what happens to the time when the input doubles. That shape is written with a big O.
The five shapes you will meet
O(1) — same time whatever the size. The formula in lesson 1.
O(log n) — halve the problem each round.
O(n) — look at everything once.
O(n log n) — sorting, and almost everything built on sorting.
O(n * n) — every pair. Fine for 5000 things, hopeless for a million.
Halving is the cheapest thing there is
Keep halving a thousand and you reach 1 in nine steps. Keep halving a billion and you reach 1 in thirty.
That is O(log n), and it is why the next few units keep looking for something to cut in half.
Python
n = 1000
steps = 0
while n > 1:
n //= 2
steps += 1
print(steps)
It prints
9
Reading the shape off the code
Loops side by side add — and adding a smaller shape to a bigger one changes nothing, so O(n + n) is just O(n).
Loops inside each other multiply. That is the whole rule.
Python
import math
for n in [10, 100, 1000, 1000000]:
print(n, math.floor(math.log2(n)), n * n)
It prints
10 3 100 100 6 10000 1000 9 1000000 1000000 19 1000000000000
Try it yourself
A function runs one loop over n items, then a second loop over n items. What shape is it?
- O(n * n)
- O(n)
- O(log n)
- O(1)
What does this print?
Python
def f(n):
total = 0
for i in range(n):
total += 1
for i in range(n):
total += 1
return total
print(f(10), f(100))
Answer them in the app
📏 The Limits Tell You the Plan
Every problem tells you how big it gets
Near the bottom of a contest problem, under the story, is a line like 1 <= n <= 200000.
That line is not paperwork. It is the setter telling you which plans are allowed, and reading it first saves writing the wrong program.
The table worth memorising
n up to 10 — anything, even trying every order.
n up to 20 — you may try every subset.
n up to 500 — three nested loops.
n up to 5000 — two nested loops.
n up to 1000000 — sorting, or one pass, and nothing slower.
Why 20 is the subset line
Each of n things is either in your chosen group or out of it, so there are 2 * 2 * 2 * ... groups — two to the power of n.
At 20 that is a million: fine. At 40 it is a million million: not fine. Trying every order instead of every subset runs out far sooner.
Python
import math
print(2 ** 20)
print(2 ** 40)
print(math.factorial(10))
print(math.factorial(20))
It prints
1048576 1099511627776 3628800 2432902008176640000
Try it yourself
A problem says n can be 200000. Which plan fits in a second?
- Check every pair of items
- Sort the items, then make one pass over them
- Try every subset of the items
- Try every order of the items
What does this print?
Python
n = 100000
print(n * n)
Answer them in the app
🏆 The Same Answer, Three Speeds
The best run of numbers
A list of numbers, some negative. Pick a run of neighbours — as long or as short as you like — with the biggest total.
Here the winner starts at the 2 and ends at the second 2, and adds up to 10. The trick is finding that without trying every run.
Python
numbers = [-1, 2, 4, -3, 5, 2, -5, 2]
print(numbers[1:6])
print(sum(numbers[1:6]))
It prints
[2, 4, -3, 5, 2] 10
Speed one: try every run
Start somewhere, keep adding neighbours, remember the best total seen. Two loops, so O(n * n) — for eight numbers that is 36 additions.
For 100000 numbers it is five billion. That is the wrong plan, and it is worth watching the count to feel why.
Python
numbers = [-1, 2, 4, -3, 5, 2, -5, 2]
best = numbers[0]
steps = 0
for i in range(len(numbers)):
total = 0
for j in range(i, len(numbers)):
total += numbers[j]
steps += 1
if total > best:
best = total
print(best, steps)
It prints
10 36
Speed two: one question per number
Walk along once, holding the best run that ends here. At each new number there are only two choices: glue it onto the run behind it, or start again from this number.
Whichever is bigger wins. That is one pass — O(n) — and it is called Kadane's algorithm.
Python
numbers = [-1, 2, 4, -3, 5, 2, -5, 2]
best = numbers[0]
here = numbers[0]
for x in numbers[1:]:
here = max(x, here + x)
if here > best:
best = here
print(best)
It prints
10
Why "start again" is the whole idea
If the run behind you adds up to something negative, dragging it along can only make your total smaller. Drop it.
That single decision is what turns five billion steps into a hundred thousand — and the fast program is shorter than the slow one.
Try it yourself
Every number in the list is negative. What does the best run look like?
- Empty, with a total of 0
- The whole list
- Just the single largest number
- The first two numbers
What does this print?
Python
numbers = [3, -4, 5, -1, 2]
best = numbers[0]
here = numbers[0]
for x in numbers[1:]:
here = max(x, here + x)
if here > best:
best = here
print(best)
Answer them in the app