🚀 Alguni Start learning

Unit 17: Chance

Counting what has not happened yet.

Unit 17 of 25 in Competitive programming for kids. Its 4 lessons are Count the Good Ones, What It Is Worth on Average, Where the Chance Flows and How Long Until It Happens — 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 opens with a fortnight’s trial of everything — no card needed — or with a family plan, bought in the iPhone app. The first two units of every track are free for ever. Try it in the app.

🎲 Count the Good Ones

A probability is a count divided by a count

Two dice have 36 outcomes, all equally likely. Six of them add up to 7.

So the chance of 7 is 6 out of 36, which is one in six. When the outcomes are equally likely, probability is nothing but counting — which is the whole of unit 15 again.

Python

from fractions import Fraction

outcomes = [(a, b) for a in range(1, 7) for b in range(1, 7)]
sevens = [p for p in outcomes if p[0] + p[1] == 7]

print(len(outcomes), len(sevens))
print(Fraction(len(sevens), len(outcomes)))

It prints

36 6
1/6

The whole shape of two dice

Seven is the commonest because it has the most ways to happen. Two and twelve have one each.

Adding a third die makes the middle even more crowded, which is the reason a total of 10 or 11 is a good bet in a lot of board games.

Python

counts = [0] * 13
for a in range(1, 7):
    for b in range(1, 7):
        counts[a + b] += 1

for total in range(2, 13):
    print(total, counts[total])

It prints

2 1
3 2
4 3
5 4
6 5
7 6
8 5
9 4
10 3
11 2
12 1

Keep it as a fraction while you can

Contest answers are usually asked for as a fraction, or as a fraction under a modulus using unit 14's inverse.

Decimals lose exactness and a judge comparing to six places will notice. Fraction is exact and slow; use it to check, and integers to submit.

Try it yourself

What is the chance that two dice show the same number?

  • 1 in 6
  • 1 in 12
  • 1 in 36
  • 1 in 3

What does this print?

Python

from fractions import Fraction

good = 0
for a in range(1, 7):
    for b in range(1, 7):
        if a + b >= 10:
            good += 1

print(Fraction(good, 36))

Answer them in the app

⚖️ What It Is Worth on Average

Every outcome, weighted by its chance

The expected value is what you would average over a very long run. Add up each outcome times its probability.

One die averages 3.5 — a number it can never actually show, which is the first surprise about expectations.

Python

expected = 0
for face in range(1, 7):
    expected += face * (1 / 6)

print(round(expected, 4))

It prints

3.5

Expectations simply add up

The expected total of two dice is 7 — and you can get that by adding 3.5 and 3.5 rather than by working through all 36 outcomes.

That is linearity of expectation, and it holds even when the things are not independent. It is the most useful single fact in contest probability.

Python

slow = 0
for a in range(1, 7):
    for b in range(1, 7):
        slow += (a + b) / 36

print(round(slow, 4))
print(round(3.5 + 3.5, 4))

It prints

7.0
7.0

The trick it makes possible

"How many of the 36 outcomes have a double somewhere" is a counting job. "What is the expected number of dice showing a six" is not: each die contributes 1/6, so two dice give 1/3. No outcomes are listed at all.

Break the thing you are counting into a sum of tiny yes/no questions, and add up their chances.

Python

print(round(2 * (1 / 6), 4))

slow = 0
for a in range(1, 7):
    for b in range(1, 7):
        sixes = (1 if a == 6 else 0) + (1 if b == 6 else 0)
        slow += sixes / 36

print(round(slow, 4))

It prints

0.3333
0.3333

Try it yourself

Twenty dice are thrown. What is the expected number of sixes?

  • 20
  • About 3.33
  • 6
  • 1

What does this print?

Python

expected = 0
for face in range(1, 5):
    expected += face * (1 / 4)

print(round(expected, 4))

Answer them in the app

🌀 Where the Chance Flows

A row of five squares, and a coin

You start on square 2. Each turn you step left or right, half and half. Squares 0 and 4 are traps: land there and you stop.

Instead of following one journey, follow the probability: keep a number for each square saying how likely you are to be standing on it.

Each step spreads it out

Every square hands half its probability to each neighbour — except the traps, which keep theirs.

After one step you are on 1 or 3. After two, back on 2 half the time and caught a quarter of the time each side. This is a Markov chain.

Python

probs = [0.0] * 5
probs[2] = 1.0

for step in range(3):
    nxt = [0.0] * 5
    nxt[0] = probs[0]
    nxt[4] = probs[4]
    for i in range(1, 4):
        nxt[i - 1] += probs[i] / 2
        nxt[i + 1] += probs[i] / 2
    probs = nxt
    print([round(x, 4) for x in probs])

It prints

[0.0, 0.5, 0.0, 0.5, 0.0]
[0.25, 0.0, 0.5, 0.0, 0.25]
[0.25, 0.25, 0.0, 0.25, 0.25]

Run it long enough and it settles

After a hundred steps almost everything has fallen into a trap: half at each end, because you started exactly in the middle.

The probabilities always add to 1, which is the check to make after every step. If they drift, a rule is handing out chance it does not have.

Python

probs = [0.0] * 5
probs[2] = 1.0

for step in range(100):
    nxt = [0.0] * 5
    nxt[0] = probs[0]
    nxt[4] = probs[4]
    for i in range(1, 4):
        nxt[i - 1] += probs[i] / 2
        nxt[i + 1] += probs[i] / 2
    probs = nxt

print([round(x, 4) for x in probs])
print(round(sum(probs), 4))

It prints

[0.5, 0.0, 0.0, 0.0, 0.5]
1.0

And it is a matrix, if you want it to be

One step is a fixed rule applied to a list of numbers — which is exactly unit 16's matrix multiplication.

So "where am I after a thousand million steps" is a matrix power, in about thirty multiplications. That is the join between the last unit and this one.

Try it yourself

Why do the probabilities always add up to 1?

  • Because there are five squares
  • Every square gives away exactly what it has, so nothing is lost or invented
  • Because of the traps
  • They do not — they shrink

What does this print?

Python

probs = [0.0, 1.0, 0.0]
for step in range(1):
    nxt = [0.0] * 3
    nxt[0] = probs[0]
    nxt[2] = probs[2]
    nxt[0] += probs[1] / 2
    nxt[2] += probs[1] / 2
    probs = nxt

print([round(x, 4) for x in probs])

Answer them in the app

🏆 How Long Until It Happens

Throwing until a six

How many throws, on average? The answer is a sum with no end: one throw times the chance it works, two throws times the chance the first missed and the second worked, and so on for ever.

Add two hundred terms and it has clearly stopped moving: 6.

Python

p = 1 / 6
total = 0.0
for k in range(1, 200):
    total += k * p * (1 - p) ** (k - 1)

print(round(total, 6))

It prints

6.0

The one-line way to get the same 6

Throw once. Either it is a six — done — or it is not, and you are back exactly where you started with the same expected wait ahead of you.

So E = 1 + (5/6) E, which rearranges to E = 6. A self-referential equation, and no series at all. Expected waits are almost always solved this way.

All six faces, not just one

Waiting for every face is the same idea six times. When you have seen k faces, the chance a throw shows a new one is (6 - k) / 6, so the wait for it is 6 / (6 - k).

Add those six waits: 1, then 1.2, then 1.5, 2, 3, 6. It comes to 14.7 — twice as long as most people guess.

Python

total = 0.0
for k in range(6):
    wait = 6 / (6 - k)
    total += wait
    print(k, round(wait, 4), round(total, 4))

It prints

0 1.0 1.0
1 1.2 2.2
2 1.5 3.7
3 2.0 5.7
4 3.0 8.7
5 6.0 14.7

And a check by playing it out

Ten thousand pretend throws of two dice, counting the sevens — using the seeded generator from the AI track rather than random, so the number below is the same every time anyone runs it.

0.1614 against a true 0.1667. Simulation is a way to check an answer, never to produce one: it is out by 3% here, and a judge wants six decimal places.

Python

seed = 12345
hits = 0
trials = 10000

for i in range(trials):
    seed = (seed * 75 + 74) % 65537
    a = seed % 6 + 1
    seed = (seed * 75 + 74) % 65537
    b = seed % 6 + 1
    if a + b == 7:
        hits += 1

print(hits, round(hits / trials, 4), round(1 / 6, 4))

It prints

1614 0.1614 0.1667

Try it yourself

The chance of a new face is (6 - k) / 6. Why is the wait 6 / (6 - k)?

  • It is the same number upside down, by chance
  • If something happens with chance p each go, the expected wait is 1/p
  • Because there are six faces
  • It is an approximation

What does this print?

Python

total = 0.0
for k in range(2):
    total += 2 / (2 - k)

print(round(total, 4))

Answer them in the app