This condensed study guide connects definitions to the mechanisms that use them. Read the short examples to understand why a rule works, then use the reference tables to recall it. The topic list follows the catalog description below; it is not a verified checklist of the current objective assessment. Your current course materials take precedence.
WGU’s June 2026 catalog defines C959 around logic and proofs, Boolean algebra and functions, set theory, finite and infinite sequences and series, relations, graphs, and trees. C960 covers searching and sorting, Big-O, number theory and cryptography, recursion and induction, counting, probability, and models of computation. (wgu.edu)
For each topic, ask: what object am I working with, what does its definition allow, and which operation answers the question? A set of choices, an ordered list of choices, and a probability distribution may describe the same situation, but call for different calculations.
Reading the examples. Each worked explanation moves between words, notation, a computation, and a picture or trace. Blocks labeled Python are code; blocks labeled pseudocode describe an algorithm without promising a particular programming language. Diagrams and traces show structure or intermediate values and are not programs. Finite examples illustrate a rule; a claim about infinitely many objects still needs a mathematical argument. Follow the stated domain and conditions before reusing any formula.
Part IIC960 — Discrete Mathematics II#
How to read the notation. A letter is a named quantity, not a new operation. Unless a section says otherwise, n is an input size or integer index; i, j, and k count positions or steps. A subscript, as in a[i] or ai, selects one entry. A superscript, as in 23, is a power: 2 × 2 × 2. Juxtaposition such as ab, a centered dot such as a·b, and × usually mean multiplication; f(n) means “the value of function f at input n,” not f multiplied by n. The sign ≠ means “not equal”; ≤ and ≥ mean “less than or equal” and “greater than or equal”; ≈ means “approximately equal.” An arrow → in a trace shows the next state or step; ⇒ in a proof means “implies.” Each chapter below gives its letters a local job.
A code-reading key for this course. Instruction snippets use Python or language-neutral pseudocode; monospaced trace tables and diagrams show the resulting states rather than executable statements. You do not need another guide open to read them. A variable is a name holding a value; x = 3 stores 3 under the name x. In ordinary mathematics, = states equality, but in Python it is assignment: the right-hand side is evaluated first, then stored in the left-hand name. Thus x = x + 1 increases x rather than asserting an impossible equation. == tests equality without changing either value.
| Python mark | Read it as | Concrete example |
|---|---|---|
==, != | equal to; not equal to | 3 == 3 is True; 3 != 3 is False |
<, >, <=, >= | less; greater; less or equal; greater or equal | 3 < 3 is False; 3 <= 3 is True |
True, False | the two Boolean truth values | A test evaluates to one of these, with capital initials in Python |
and, or, not | both tests; at least one test; reverse a test's truth | 3 > 0 and 3 < 5 is True; not True is False |
+, -, *, ** | add; subtract; multiply; raise to a power | 2**3 is 8, not 6; 2*3 is 6 |
/, //, % | divide; floor quotient; remainder | 7/2 is 3.5; 7//2 is 3; 7%2 is 1 |
+=, *=, //= | update the name using that operation | x += 2 means x = x + 2 |
# | the rest of this line is a comment, not an instruction | x = 3 # starting value executes only the assignment |
Lists and positions. Square brackets make a list, an ordered collection. With a = [3,7,2], a[0] selects its first entry, 3; a[1] selects 7; and len(a) gives the number of entries, 3. Python counts positions from zero, so the last position is len(a)-1. a.append(9) adds 9 at the end. sum(a) adds the entries; min(a) and max(a) select a smallest and largest entry of a nonempty list; abs(x) gives the magnitude of x without its sign. Parentheses call a function, as in len(a); they also group arithmetic. Two values such as (2,5) form an ordered pair (a tuple).
list a: [3, 7, 2]
Python index: 0 1 2
mathematical
1-based index: 1 2 3
# An index selects an entry; len(a)=3 counts entries.
Blocks and functions. def defines a named function; names in its parentheses are its inputs, called parameters. return gives the caller an answer and immediately ends that call. A colon starts a block; indentation shows which statements belong to it. if runs its indented block when the test is True, and else supplies the alternative.
def larger(x, y): # define a function with two inputs
if x > y: # this test belongs to the function
return x # this answer belongs to the if branch
else:
return y
answer = larger(3, 7) # caller passes x=3, y=7; stores answer 7
Loops and collections of tests. for x in items takes one item at a time, assigning it to x, and runs the indented body once for each item. range(n) supplies the integers 0 through n−1; range(start, stop) includes start but excludes stop; a third argument sets the step. A while loop repeats while its test remains True. break leaves the nearest loop. Unlike return, it does not by itself leave the function.
total = 0
for x in [2, 4, 6]:
total += x
# x: 2 4 6
# total: 0 → 2 → 6 → 12
# range(1,4) supplies 1,2,3; range(0,7,2) supplies 0,2,4,6.
# Collecting results in brackets is a list comprehension:
squares = [x*x for x in [2, 4, 6]] # [4,16,36]
all(...) asks whether every supplied test is True; any(...) asks whether at least one is True. The phrase test for x in items generates one test per item. In all(x > 0 for x in [2,4,6]), the three tests are True, True, True, so all returns True. In any(x > 5 for x in [2,4,6]), the tests are False, False, True, so any returns True. This mirrors the mathematical “for every” and “there exists.” With no tests, all returns True and any returns False.
in also tests membership outside a loop: 3 in [1,3,5] is True. A set such as {1,3,5} stores distinct members without an index order; set() makes an empty set. A dictionary such as {'A': 2, 'B': 5} maps labels to values; indexing by ['A'] retrieves 2. More specialized syntax is explained where used. Code demonstrates a mechanism; running finite examples does not establish a theorem for every input.
Reading route. C960 reuses C959's logic, sets, functions, and finite sums. Follow these chains rather than treating every formula as an isolated fact:
- Trace an algorithm → count its work → compare searches and sorts. Recurrences count recursive work; induction proves claims about it.
- Divisibility and remainders → gcd → modular inverse → RSA. Binary place value makes repeated squaring efficient.
- Count outcomes → assign probabilities → measure outcomes with a random variable → choose a distribution.
- Strings and languages → state machines → regular expressions and limits of finite memory.
11Algorithms and pseudocode#
Concrete starting point. A recipe for finding the largest list entry must work for every allowed nonempty list, not merely today's example. Its input is the supplied list; its state is the changing collection of variable values; its output is the returned largest value. A precondition says what must be true before it starts (the list is nonempty); a postcondition says what it guarantees when it ends (the returned value is a largest entry).
input [3, 7, 2] → initialize best=3 → compare 7 → best=7
↓
output 7 ← return best ← compare 2, keep best=7
The mathematical objects are now inputs to procedures. First follow the changing state to find what a procedure does. Only then ask whether it is correct and how its work grows; output, correctness, and cost are different questions.
An algorithm is a finite, unambiguous, effective procedure that transforms inputs into outputs.
A proper algorithm has:
- specified inputs;
- specified outputs;
- precise steps;
- termination after finitely many steps;
- executable operations;
- correctness for all allowed inputs.
11.1Common algorithmic structures#
Sequence#
One statement's result becomes the next statement's input. With x = 2, then x = x + 3, then y = 2 * x, the state changes (x,y): (2,unset) → (5,unset) → (5,10). Reversing the last two statements would give a different y.
Execute statements in order.
Selection#
A condition is a true-or-false question. Only the chosen branch runs; the other branch contributes no actions on that execution.
def absolute(x):
if x < 0:
return -x
else:
return x
# x=-3 → question (-3 < 0)? yes → return 3
# x= 4 → question ( 4 < 0)? no → return 4
Choose a branch:
if condition
action A
else
action B
Iteration#
A loop repeats a body while its condition permits it. The loop counter i names the current repetition, not a fixed value.
total = 0
for i in range(1, 4): # Python includes 1,2,3; excludes 4
total = total + i
# i: 1 2 3
# total: 0→ 1→ 3→ 6
Repeat using a loop.
Recursion#
A recursive call starts a new invocation with its own input. A base case returns without another call; a recursive case reduces the problem. Here n must be a nonnegative integer.
def factorial(n):
if n == 0:
return 1
return n * factorial(n - 1)
# factorial(3) → 3 * factorial(2) → 3 * 2 * factorial(1)
# → 3 * 2 * 1 * factorial(0) → 6
A procedure calls itself on a smaller or simpler input.
11.2Reading pseudocode#
Translate every mark into an action. x ← x + 1 reads “replace the old value of x by the old value plus one”; ← is assignment. a[i] reads “the entry at index i of array a”; n is the number of entries. > means strictly greater, ≥ includes equality, and … means continue the displayed pattern. Pseudocode's for i ← 2 to n below includes both endpoints. Its indices start at 1; the equivalent Python indices start at 0.
def maximum(a):
best = a[0] # requires a nonempty list
for i in range(1, len(a)): # indices 1 through len(a)-1
if a[i] > best:
best = a[i]
return best
# mathematical a[1], a[2], a[3], a[4]
# Python a[0], a[1], a[2], a[3]
# values 3 7 2 9
Key distinctions:
- Assignment changes a variable:
- Equality compares values: would be false as an ordinary mathematical equation.
returnusually ends the current procedure immediately.- A loop endpoint may be inclusive or exclusive.
- Array indexing may begin at or .
- Updating a variable inside a loop affects subsequent iterations.
When tracing, maintain a small table containing:
- current iteration;
- relevant variable values;
- output produced so far.
Ignore variables that never change. This procedure returns the largest entry of a list a[1], …, a[n]:
m ← a[1]
for i ← 2 to n
if a[i] > m
m ← a[i]
return m
Traced on a = [3, 7, 2, 9], one row per iteration:
i a[i] m
start - 3
2 7 7 7 > 3, so m changes
3 2 7 2 is not > 7
4 9 9 returns 9
11.3Correctness#
Why a trace is not a proof. The trace [3,7,2,9] → 9 checks four values. The invariant must cover an arbitrary number of arbitrary values. For the maximum algorithm, “best is largest among entries inspected so far” is preserved because the next entry either replaces best when larger or leaves it alone.
already inspected next entry new inspected prefix
[3, 7] with best=7 + [2] → [3, 7, 2], best=7
[3, 7, 2] with best=7 + [9] → [3, 7, 2, 9], best=9
The loop also terminates: the number of uninspected entries drops by one each time and cannot become negative. An invariant plus a stopping argument proves total correctness; an invariant alone need not rule out an infinite loop.
An algorithm is correct if it produces the specified output for every permitted input. A trace checks one input; a proof must cover them all. Look at the trace in 11.2 again. Whatever the list, after the iteration for index i, m holds the largest of a[1], …, a[i]. That sentence is true before the loop starts (m = a[1] is the largest of one item); one more comparison keeps it true; and when the loop ends at i = n, it says m is the largest of the whole list, which is the specified output.
Such a sentence is a loop invariant: a statement that remains true:
- before the first iteration;
- after each iteration;
- when the loop ends.
A correctness proof typically shows:
- initialization: invariant starts true;
- maintenance: one iteration preserves it;
- termination: invariant plus stopping condition implies the result.
This is induction expressed in algorithmic form.
12Complexity and Big-O#
Let n be the number of list entries, and let f(n) count a chosen operation. For the maximum algorithm, the first entry initializes best and each of the remaining entries needs one comparison, so f(n) = n − 1. This is an operation count, not a number of seconds. We normally treat one comparison of fixed-size values as constant cost; operations on enormous integers can require a different cost model.
n (entries) 1 2 4 8
comparisons n-1 0 1 3 7
# Doubling the input nearly doubles the comparisons.
Use the trace from the previous chapter, but replace a particular input size with n. Count how often the main operation runs. A bound summarizes that count; it does not change what the algorithm computes.
Complexity measures resource growth as input size increases.
Common resources are:
- time;
- memory;
- comparisons;
- arithmetic operations;
- recursive calls.
The focus is asymptotic growth, not exact wall-clock time.
12.1Best, worst, and average case#
These are three different functions of the same size n. For linear search in [A,B,C,D], finding A costs 1 comparison, finding D costs 4, and missing entirely costs 4. If each of the four present targets is equally likely, average cost is (1+2+3+4)/4 = 2.5. If absent targets are allowed, the distribution must also say how often absence happens.
# Explicitly average the costs under a uniform present-target model.
costs = [1, 2, 3, 4]
average = sum(costs) / len(costs) # 2.5
# best = min(costs)=1; worst = max(costs)=4
- Best case: minimum operations among size- inputs.
- Worst case: maximum operations.
- Average case: expected operations under an assumed input distribution.
Unless specified, complexity questions often ask for worst-case behavior.
Case and bound are separate choices. First choose which inputs to describe, then bound their operation count. Linear search over n items makes 1 comparison when the target is first (best case) and n when it is last or absent (worst case). If a present target is equally likely to sit at any position, it makes (n + 1)/2 comparisons on average, still linear.
12.2Big-O, Omega, and Theta#
Read a bound as a comparison of curves. f(n) is the measured work, g(n) is a simpler comparison function, c is a fixed positive scale factor, and n0 (“n sub zero”) is the size beyond which the bound must hold. ∀ n ≥ n₀ reads “for every n at least n-zero”; ∃ c > 0 reads “there exists a positive constant c.” Constants must stay fixed as n grows.
| Notation | Read aloud | What must eventually hold |
|---|---|---|
| f(n) = O(g(n)) | f is big-oh of g | f stays at or below some fixed multiple of g |
| f(n) = Ω(g(n)) | f is big-omega of g | f stays at or above some positive fixed multiple of g |
| f(n) = Θ(g(n)) | f is big-theta of g | f is trapped between two such multiples |
The equals sign here is conventional shorthand for belonging to a class of functions; O(g(n)) is not one exact numerical value.
# A few points illustrate, but do not prove, the bound.
# f(n)=3*n**2+5*n; lower=3*n**2; upper=4*n**2
n 1 5 10 100
lower 3 75 300 30000
f(n) 8 100 350 30500
upper 4 100 400 40000
# From n=5 on: lower ≤ f ≤ upper.
# For all n≥5, 5*n≤n**2 is the algebraic proof.
Big-O#
means is an asymptotic upper bound. Formally, there are constants and such that:
for all .
The two constants are a certificate you can check. Take f(n) = 3n2 + 5n and g(n) = n2. Choose c = 4 and n0 = 5: once n ≥ 5, 5n ≤ n2, so 3n2 + 5n ≤ 4n2. Smaller n can break the inequality (at n = 1, f is 8 and 4n2 is 4), and n0 exists to say those early values do not count. The question is about growth.
Big-Omega#
means is an asymptotic lower bound: the inequality turns around, with f(n) ≥ cg(n) ≥ 0 for some c > 0 and all n ≥ n0. The same 3n2 + 5n is Ω(n2) with c = 3 and n0 = 1.
Big-Theta#
means is both an upper and lower bound—a tight growth classification. Being both O(n2) and Ω(n2), 3n2 + 5n is Θ(n2): it is trapped between 3n2 and 4n2 from n = 5 on.
Big-O is not necessarily exact. A linear function is also technically , though is the informative tight classification.
O does not mean “worst,” and Ω does not mean “best”; either notation can bound a best-case, worst-case, or average-case function.
12.3Simplifying complexity expressions#
“Drop constants” applies to fixed multipliers and lower-order additions, not to the exponent. 7*n**3 and n**3 have the same growth class; 2**n and 3**n do not. A term is lower-order when its share of the dominant term tends to zero.
def f(n):
return 7*n**3 + 20*n**2 + 900
# n: 10 100
# dominant 7*n**3: 7000 7000000
# remaining terms: 2900 200900
# remaining / dominant: 0.414 0.0287
Discard:
- constant factors;
- lower-order terms.
As grows, the dominant term determines behavior. At n = 100, 7n3 is 7,000,000 while 20n2 + 900 is 200,900, under 3% of it, and that share keeps shrinking as n grows. Constant factors go for a different reason: they change the height of the curve but not its shape, and doubling n multiplies 7n3 by 8 exactly as it does n3.
12.4Growth hierarchy#
What each shape means. log₂ n asks “how many doublings of 1 reach n?” Thus log₂8 = 3 because 2³ = 8. n log n means n times log n; n² means n times n; cⁿ means a fixed base c multiplied by itself n times. In logₐ n, a is the base and must be positive and not 1. Complexity comparisons here use fixed bases greater than 1. The change-of-base fraction below divides two logarithms; it does not change the input n.
n 4 8 16
1 1 1 1
log₂ n 2 3 4
n 4 8 16
n log₂ n 8 24 64
n² 16 64 256
2ⁿ 16 256 65536
n! 24 40320 20922789888000
The square-root shape √n asks for a number whose square is n: √16=4. A polynomial term nᵏ keeps its exponent k fixed while n grows; an exponential term cⁿ keeps its base c>1 fixed while n grows. These are different growth mechanisms. At the fast end, nⁿ multiplies n by itself n times, whereas n! multiplies the differing factors 1 through n. Thus 4⁴=256 while 4!=24. The ordering describes eventual growth, not strict ordering at every small input. For example 2⁴ = 4², and constants can let a quadratic implementation beat a linear one on small lists.
n! (“n factorial”) is the product of the integers from 1 through n; by convention, 0! = 1.
From slower to faster:
for fixed .
Logarithm bases differ only by a constant factor:
so all fixed bases represent:
Polynomial growth is generally considered scalable relative to exponential or factorial growth. Even at n = 20 the gap is enormous: n3 is 8,000, 2n is about a million, and n! is about 2.4 × 1018.
12.5Recognizing loop complexity#
Count how many times the innermost body actually runs. Python's range(n) supplies n values, 0 through n−1. A body that costs more than constant work must have that cost included too.
# Compare three traversal shapes when n=4.
# square triangle doubling
# (i,j) visits: (i,j) visits: i values:
# 1: xxxx 1: x 1 → 2 → 4(stop)
# 2: xxxx 2: xx 2 body executions
# 3: xxxx 3: xxx
# 4: xxxx 4: xxxx
# 16 total 10 total
Fixed work#
A fixed number of fixed-size arithmetic operations does not grow with n. return a + b performs one addition whether a list elsewhere contains 10 or 10,000 entries.
x = a + b
Linear loop#
for i in range(n):
inspect(i) # assume inspect costs one unit
# n=4: inspect(0), inspect(1), inspect(2), inspect(3): 4 units
for i = 1 to n
Step size other than one#
for i in range(0, n, 10):
inspect(i)
# n=31: i=0,10,20,30 → 4 units = ceiling(n/10)
# Growing n by a factor of 10 still grows the count about tenfold.
for i = 1 to n step 10
Still:
Dividing the iteration count by a fixed constant does not change asymptotic class.
Doubling or halving#
The exponent k counts completed doublings; it is not the current value i. Each extra bit of n allows approximately one more doubling.
i = 1
while i < n:
inspect(i)
i *= 2
# n=16: inspect 1 → 2 → 4 → 8; i=16 stops.
# Four repetitions, log₂16=4. For other n, round upward.
i = 1
while i < n
i = 2i
After iterations:
Stop near , so:
Complexity:
Independent nested loops#
for i in range(n):
for j in range(n):
inspect(i, j)
# n=3 visits a 3-by-3 grid: (0,0),(0,1),(0,2),
# (1,0),(1,1),(1,2),
# (2,0),(2,1),(2,2).
for i = 1 to n
for j = 1 to n
so:
Triangular loop#
The inner count now depends on the outer counter. Add its actual lengths instead of blindly multiplying n by n.
for i in range(1, n + 1):
for j in range(i):
inspect(i, j)
# n=4: lengths 1 + 2 + 3 + 4 = 10 = 4*5/2.
# Two triangles fit a 4-by-5 rectangle, hence division by 2.
for i = 1 to n
for j = 1 to i
Total iterations:
therefore:
Linear outer, logarithmic inner#
for i in range(n):
j = 1 # reset for EACH outer repetition
while j < n:
inspect(i, j)
j *= 2
# n=8: 8 rows; each visits j=1,2,4 → 8*3=24 visits.
Without resetting j, only the first outer repetition would run the inner loop. The program's actual state matters more than indentation alone.
for i = 1 to n
j = 1
while j < n
j = 2j
Sequential blocks#
for i in range(n):
inspect(i)
for i in range(n):
for j in range(n):
inspect(i, j)
# n=4: 4 visits THEN 16 visits = 20, not 4*16.
For one block followed by another, add costs and retain the dominant term:
Nested blocks#
“Multiply” means each outer repetition pays the full inner cost: 4 outer repetitions × 3 inner visits each = 12 visits. If the inner length changes with i, add those lengths as in the triangle example instead.
For genuinely nested independent repetitions, multiply counts.
12.6Common operation sums#
Read the sum sign. Σ (capital sigma) means “add the listed values.” In Σᵢ₌₁ⁿ i², i is a temporary counter, its first value is 1, its last is n, and i² is the item being added: 1² + 2² + … + n². It does not mean square the total sum.
constant_work = sum(1 for i in range(1, n + 1))
triangle_work = sum(i for i in range(1, n + 1))
square_work = sum(i*i for i in range(1, n + 1))
level_work = sum(2**i for i in range(k + 1))
# k=3: level widths 1 | 2 | 4 | 8, total=15.
# Number of levels=4; total work=15. These are different quantities.
If , the final sum is , even though there are only levels. Each term is one more than all the earlier terms combined (1 + 2 + 4 = 7, one less than 8), so the last term carries about half the total. A procedure whose rounds do 1, 2, 4, …, n units of work does less than 2n in all.
13Searching and sorting#
In the code below, a is a list, x is the search target, and index −1 signals “not found.” Sorting examples compare numeric keys. A key is the field used for ordering, such as a surname; different records can share the same key. Auxiliary memory is extra storage beyond the input itself, such as a temporary list or a recursive call stack.
Searching asks where an item is; sorting arranges items so later operations can exploit order. Linear search works without order. Binary search earns its speed by using sorted order to rule out half the candidates at each step.
13.1Linear search#
Start at the first entry and compare each value with the target. A match ends the search immediately; reaching the end proves the target is absent.
def linear_search(a, x):
for i in range(len(a)):
if a[i] == x:
return i
return -1
# a=[8,3,9,2], x=9:
# index 0 1 2
# compare 8 3 9 → equal, return index 2
# A missing target would inspect the final 2 as well.
Inspect elements one by one until:
- the target is found, or
- the collection ends.
No sorting is required.
- best case: ;
- worst case: ;
- average case: .
13.2Binary search#
The candidate interval has endpoints lo (“low”) and hi (“high”), both included. mid is their integer midpoint. Sorted order justifies every discarded half; without it, the target might be in the half thrown away.
def binary_search(a, x):
lo, hi = 0, len(a) - 1
while lo <= hi:
mid = (lo + hi) // 2
if a[mid] == x:
return mid
if a[mid] < x:
lo = mid + 1
else:
hi = mid - 1
return -1
# a=[1,3,5,7,9,11,13], x=11
# [1 3 5 7 9 11 13] midpoint=7; x is larger
# [9 11 13] midpoint=11; found at index 5
# Stopping lo>hi means the candidate interval is empty.
Binary search requires sorted data.
- inspect the middle element;
- if equal, stop;
- otherwise discard the half that cannot contain the target;
- repeat.
After divisions, the remaining size is roughly:
Set this near :
so:
Worst-case complexity:
In numbers: 1,000 sorted items shrink to 500, 250, 125, and so on, reaching a single candidate after about 10 halvings, since 210 = 1,024. A million items need only about 20.
Binary search on a linked list may not provide the same practical benefit because accessing the middle is not constant-time.
Two sorting terms recur below. Stable means equal-key records keep their original relative order. In-place means constant auxiliary storage for rearranging the items. Quicksort is often called in-place while its recursion stack is counted separately; check the space convention in the question.
13.3Bubble sort#
Compare neighbors and swap any pair in the wrong order. One sweep carries a largest remaining value to the right edge; repeat on the unfinished part.
def bubble_sort(a):
for end in range(len(a) - 1, 0, -1):
swapped = False
for j in range(end):
if a[j] > a[j + 1]:
a[j], a[j + 1] = a[j + 1], a[j]
swapped = True
if not swapped:
break
# [3,1,2] → [1,3,2] → [1,2,3] | next pass: no swap, stop
# compare neighbors fixed suffix [3]
The comma assignment swaps two entries simultaneously. The strict > test does not swap equal keys, which preserves stability.
Repeatedly compare adjacent elements and swap inverted pairs.
After a full pass, a largest remaining element “bubbles” toward the end. One pass over [4, 3, 1, 2] carries the 4 past each smaller neighbor: [3, 4, 1, 2], then [3, 1, 4, 2], then [3, 1, 2, 4]. Each pass fixes one more item at the end, so up to n − 1 passes of up to n − 1 comparisons are needed. A pass with no swaps proves the list is sorted, which gives the optimized best case: one pass over already sorted input. Only adjacent unequal items ever swap, so equal items never pass each other.
- worst case: ;
- average: ;
- optimized best case: ;
- stable;
- in-place.
13.4Selection sort#
Reserve the next output position, scan the unfinished entries to find their minimum, and swap that minimum into the reserved position. The sorted prefix grows one item per pass.
def selection_sort(a):
for i in range(len(a) - 1):
smallest = i
for j in range(i + 1, len(a)):
if a[j] < a[smallest]:
smallest = j
a[i], a[smallest] = a[smallest], a[i]
# [3,1,2] → find minimum 1, swap → [1 | 3,2]
# find remaining minimum 2 → [1,2 | 3]
# Left of | is the completed sorted prefix.
Repeatedly find the smallest remaining item and place it next.
It performs roughly:
comparisons:
It performs relatively few swaps but remains quadratic. Sorted input does not help: the only way to know which remaining item is smallest is to look at all of them.
The long-distance swap is what breaks stability. In [2a, 2b, 1], the first pass swaps 1 with 2a, giving [1, 2b, 2a]: the two equal keys have changed order.
Usually:
- not stable in its basic form;
- in-place.
13.5Insertion sort#
Treat the left prefix as already sorted. Pick up its next neighbor, shift larger prefix entries right to make a space, and put the picked-up item into that space.
def insertion_sort(a):
for i in range(1, len(a)):
item = a[i]
j = i - 1
while j >= 0 and a[j] > item:
a[j + 1] = a[j] # shift larger entry right
j -= 1
a[j + 1] = item
# [3 | 1,2] → [1,3 | 2] → [1,2,3 |]
# Insert 1 before 3; then insert 2 between 1 and 3.
Maintain a sorted prefix and insert each next item into its proper position, the way most people sort a hand of cards. The cost depends on how far each item must travel. On sorted input each new item is compared once with its left neighbor and stays put: n − 1 comparisons. On reversed input the kth item moves past all k − 1 items before it: 1 + 2 + ⋯ + (n − 1) = Θ(n2). An item stops at the first item not larger than it, so equal items keep their order.
- best case on already sorted data: ;
- average and worst: ;
- stable;
- in-place;
- efficient for small or nearly sorted inputs.
13.6Merge sort#
Two sorted lists are easy to combine: keep taking the smaller front item. Merge sort obtains those sorted lists by splitting until each piece has at most one item, then combines them on the way back up.
MERGE(left, right):
output = empty list
while neither list is empty:
move the smaller front item to output
on a tie, choose left
append the unconsumed items from either list
return output
MERGE_SORT(items):
if length(items) <= 1: return items
split items into left and right halves
return MERGE(MERGE_SORT(left), MERGE_SORT(right))
[4,1,3,2] split
/ \
[4,1] [3,2] split
/ \ / \
[4] [1] [3] [2] singleton lists already sorted
\ / \ /
[1,4] [2,3] merge
\ /
[1,2,3,4] merge
- divide the list into halves;
- recursively sort each half;
- merge the sorted halves.
Merging is the step that does the work. To merge [2, 5, 9] with [3, 4], repeatedly compare the two front items and output the smaller: 2, 3, 4, 5, and then 9 is all that is left. Each comparison outputs one item, so merging m items costs Θ(m). Taking from the left half on ties keeps equal items in order, which makes merge sort stable.
There are approximately:
levels, and each level performs merge work:
Merge sort is:
- stable;
- reliably ;
- usually requires auxiliary memory for arrays.
13.7Quicksort#
A pivot is a chosen key used to divide the other keys. Partitioning groups keys by their relation to it; it does not by itself sort the groups.
def quicksort(a):
if len(a) <= 1:
return a
pivot = a[(len(a) - 1) // 2]
less = [x for x in a if x < pivot]
same = [x for x in a if x == pivot]
more = [x for x in a if x > pivot]
return quicksort(less) + same + quicksort(more)
# [3,8,5,1,9,2], pivot at index (6-1)//2=2, value 5:
# [3,1,2] | [5] | [8,9]
# sort→ [1,2,3] | [5] | [8,9] → concatenate
# Balanced partitions: widths n, n/2, n/4... along each path.
# Bad partitions: n, n-1, n-2... along the long path.
This clear three-list version uses extra lists and preserves order among equal keys. The usual “in-place, unstable” description below refers to swap-based partition implementations, not this particular teaching version.
- choose a pivot;
- partition elements around it;
- recursively sort partitions.
With pivot 5, the list [3, 8, 5, 1, 9, 2] partitions into [3, 1, 2], then 5, then [8, 9]. No merge is needed afterward: everything on the left is already smaller than everything on the right. Merge sort splits blindly and works hard to combine; quicksort works hard to split and combines for free. The pivot decides the split. One near the median halves the list, as merge sort does. One that is always the smallest item, such as the first element of already sorted input, peels off a single item per level, leaving n levels.
If partitions are reasonably balanced:
If pivots repeatedly create highly unbalanced partitions:
Typical properties:
- average ;
- worst ;
- often in-place;
- typically not stable.
13.8Algorithm categories#
These categories describe a strategy, not a guaranteed runtime or correctness result. A strategy can combine with another; memoized recursion is both recursive and dynamic programming.
Brute force#
# Find a pair summing to target by trying every pair.
for i in range(len(a)):
for j in range(i + 1, len(a)):
if a[i] + a[j] == target:
return (i, j)
# [2,5,7], target=9: try (2,5) → (2,7), success.
# All-pairs enumeration is quadratic here, not exponential.
Enumerate many or all possibilities.
Simple and general, but often exponential or factorial.
Greedy#
# Greedy coin choice: repeatedly take the largest coin that fits.
# coins=[4,3,1], amount=6
# greedy path: 6 → take 4 → remainder 2 → take 1 → take 1
# result [4,1,1]: three coins
# better [3,3]: two coins
The locally largest coin did not give the global minimum number of coins. A proof of a greedy-choice property is needed before assuming this strategy solves a minimization problem.
Kruskal and Prim, named below, are examples where that property can be proved. For a connected undirected network, they find a minimum spanning tree: connect every point (vertex) of the network using connections (edges) of minimum total cost, with no cycles. Kruskal takes the cheapest available edge that does not form a cycle; Prim takes the cheapest edge connecting the growing tree to a new vertex.
triangle edge costs: A--B costs 1; B--C costs 2; A--C costs 4
KRUSKAL: sort edges by cost → take AB(1) → take BC(2)
skip AC(4): it would close the cycle A-B-C-A
PRIM from A: take AB(1) → tree contains A,B
among edges reaching C, choose BC(2) over AC(4)
chosen tree: A --1-- B --2-- C total cost 3
Choose the locally best option at every step.
A greedy method is correct only when the problem has a structure proving local choices lead to a global optimum.
Kruskal and Prim are greedy.
Divide and conquer#
problem size 8
↓ divide
4 4
/ \ / \
2 2 2 2 solve smaller problems
↑ combine their answers
# Merge sort combines by merging; binary search keeps only one child.
Divide into subproblems, solve them, and combine.
Merge sort and binary search use this structure.
Dynamic programming#
def fib(n): # nonnegative integer n
values = [0, 1]
for k in range(2, n + 1):
values.append(values[k - 1] + values[k - 2])
return values[n]
# index k: 0 1 2 3 4 5
# values[k]: 0 1 1 2 3 5
# Each earlier answer is stored and reused, not recomputed.
Memoization stores answers on demand during recursion; tabulation fills a table in dependency order as above. Both exploit overlapping subproblems.
Solve overlapping subproblems once and store their answers.
It converts many exponential recursive procedures into polynomial algorithms.
Naive Fibonacci recomputes the same values repeatedly; memoization removes that duplication.
14Number theory#
| Number theory studies integers, divisibility, primes, and remainders. Start with exact division, then allow a remainder. Euclid finds a gcd; back-substitution expresses it as a combination of the inputs. When that gcd is 1, the combination gives a modular inverse. Each step prepares the next. |
14.1Divisibility#
Number-theory notation before the formulas. An integer is a whole number that may be negative, zero, or positive. ℤ (“the integers”) denotes that set. The statement
In the divisibility rules below, m and n are arbitrary integer coefficients; they are not required to be positive. “a divides mb+nc” means every integer linear combination of two multiples of a remains a multiple of a. A negative coefficient subtracts copies rather than adding them. |
| means: |
|
For example, 3 ∣ 12 because 12 = 3 · 4, while 3 ∤ 14 (“3 does not divide 14”) because no integer k gives 3k = 14. Read a ∣ b as a sentence that is true or false, not as the number a/b. |
| If and , then: |
| and: |
| for integers . The reason is one line of substitution: if b = ak and c = al, then mb + nc = a(mk + nl), again a multiple of a. Since 3 ∣ 12 and 3 ∣ 21, 3 also divides 12 + 21 = 33 and 2 · 12 − 21 = 3. |
| If: |
| and: |
| then: |
|
Substitution again: if b = ak and c = bl, then c = a(kl). From 3 ∣ 6 and 6 ∣ 42 follows 3 ∣ 42. |
14.2Division algorithm#
In a = qn + r, a is the dividend, n the positive divisor, q the quotient (whole groups), and r the remainder (leftovers). div names the quotient operation; mod names the remainder operation. The constraint 0 ≤ r < n reads “r is at least zero and strictly less than n.”
q, r = divmod(17, 5) # q=3, r=2
# [*****] [*****] [*****] **
# 3 full groups of 5 2 left over
q, r = divmod(-21, 33) # q=-1, r=12
# -21 = (-1)*33 + 12; use the floor quotient, not truncation toward 0.
Most pairs do not divide evenly, so allow a leftover. Sharing 17 cookies among 5 people gives each person 3, with 2 left over: 17 = 3 · 5 + 2. The leftover is always smaller than the number of people, or everyone could take one more. The division algorithm says this split always exists and is unique.
For integer and positive integer , there exist unique integers such that:
with:
Then:
and:
So 17 div 5 = 3 and 17 mod 5 = 2. The nonnegative remainder requirement matters with negative dividends.
For example:
so:
not .
14.3Modular congruence#
a ≡ b (mod n) reads “a is congruent to b modulo n.” The triple bar means equal remainders, not equal integers. n ∣ (a−b) says the difference is a whole multiple of n. A residue class is all integers having one specified remainder.
def congruent(a, b, n):
return (a - b) % n == 0 # n must be positive
# Modulo 5, the class of 2 is:
# ... -8 -- -3 -- 2 -- 7 -- 12 -- 17 ...
# every step adds 5
# 12 ≡ 2 (mod 5) is true; 12 = 2 is false.
# Clock: 10 → 11 → 0 → 1 → 2 → 3 (five hours mod 12).
A 12-hour clock already does this arithmetic. Five hours after 10 o’clock it is 3 o’clock, because 15 and 3 leave the same remainder when divided by 12; the clock ignores whole turns. Congruence names that sameness:
means:
Equivalent interpretations:
- have the same remainder modulo ;
- they represent the same residue class.
Congruence behaves well under addition and multiplication:
If:
and:
then:
and:
You may reduce values modulo after every step. To find 38 · 47 mod 5, reduce first: 38 ≡ 3 and 47 ≡ 2, so the product is ≡ 3 · 2 = 6 ≡ 1. Multiplying out gives 1,786, which also leaves remainder 1, with much bigger numbers on the way.
Ordinary division is not generally valid modulo . It requires an invertible divisor. Modulo 6, 2 · 3 ≡ 2 · 0, since both sides are multiples of 6, yet cancelling the 2 would claim 3 ≡ 0, which is false. Cancelling is safe only for a factor that has a modular inverse (14.8).
14.4Prime numbers and factorization#
A factor divides a number exactly; a prime factor is a factor that is prime. The symbol √N reads “square root of N,” the nonnegative number whose square is N. Neither 0 nor 1 is prime; 1 is also not composite.
def is_prime(N):
if N < 2:
return False
d = 2
while d*d <= N:
if N % d == 0:
return False
d += 1
return True
# This checks all integers up to √N, including some redundant composites.
# 60 = 2 * 30 = 2 * 2 * 15 = 2² * 3 * 5
# Prime-factor leaves: 2, 2, 3, 5. Order can change; the multiset cannot.
A prime is an integer greater than with exactly two positive divisors:
A composite number has a nontrivial factorization.
To test whether is prime, it is sufficient to test prime divisors no larger than:
If is composite, at least one of must be at most , since two factors both larger than √N would multiply to more than N. To test 91, try the primes up to 9 (√91 ≈ 9.5): 2, 3 and 5 fail, but 7 divides it, and 91 = 7 · 13. For 97, none of 2, 3, 5 and 7 divides it, so 97 is prime without any further checks.
The fundamental theorem of arithmetic says every integer greater than has a unique prime factorization apart from order.
14.5Greatest common divisor and least common multiple#
gcd(a,b) reads “greatest common divisor of a and b”; lcm(a,b) reads “least common multiple.” A multiple is what you obtain by multiplying by an integer. |ab| means the absolute value of their product: remove its sign. For the product identity below, take positive a and b (it extends to nonzero integers with absolute values).
from math import gcd
def lcm(a, b): # positive inputs here
return a // gcd(a, b) * b
# 12 = 2² * 3¹
# 18 = 2¹ * 3²
# min exponents → gcd = 2¹ * 3¹ = 6
# max exponents → lcm = 2² * 3² = 36
# multiples of 12: 12,24,36,48,...
# multiples of 18: 18,36,54,... → first shared positive one is 36.
is the greatest positive integer dividing both.
Two integers are coprime when:
The least common multiple satisfies:
Prime factorizations show why. Write 12 = 22 · 3 and 18 = 2 · 32. A common divisor can use each prime at most as often as the smaller exponent allows, so the gcd is 2 · 3 = 6. A common multiple must use each prime at least as often as the larger exponent, so the lcm is 22 · 32 = 36. For each prime, one exponent goes to the gcd and the other to the lcm, so together they use every factor of 12 · 18 exactly once: 6 · 36 = 216 = 12 · 18.
14.6Euclidean algorithm#
Each step replaces the pair “old dividend, old divisor” by “old divisor, new remainder.” The quotient q records how many copies were removed; only the remainder affects the next pair.
def euclid(a, b):
a, b = abs(a), abs(b)
while b != 0:
a, b = b, a % b
return a
# pair (a,b) quotient remainder next pair
# (252,105) 2 42 (105,42)
# (105,42) 2 21 (42,21)
# (42,21) 2 0 (21,0) → return 21
Use repeated division:
and the identity:
The identity holds because r = a − bq. Anything that divides both a and b also divides r (14.1), and anything that divides both b and r also divides a = bq + r. The pairs (a, b) and (b, r) have exactly the same common divisors, so the same greatest one, and the second pair is smaller. Continue until the remainder becomes zero. The last nonzero remainder is the gcd, because gcd(d, 0) = d.
For example:
Therefore:
The algorithm is efficient because remainders decrease rapidly.
14.7Bézout’s identity and extended Euclid#
In gcd(a,b) = sa + tb, s and t are integer coefficients: the multipliers telling how many signed copies of a and b to combine. They can be negative. Extended Euclid tracks a remainder together with the two coefficients expressing that remainder in terms of the original inputs.
def extended_gcd(a, b): # nonnegative a, positive b
if b == 0:
return a, 1, 0 # a = 1*a + 0*b
g, u, v = extended_gcd(b, a % b)
return g, v, u - (a // b) * v
# remainder coefficient of 252 coefficient of 105
# 252 1 0
# 105 0 1
# 42=252-2*105 1 -2
# 21=105-2*42 -2 5
# Thus 21=(-2)*252 + 5*105.
Bézout’s identity states that there exist integers such that:
The extended Euclidean algorithm finds these coefficients by back-substituting through Euclid’s equations.
For the preceding gcd:
and:
so:
Thus:
14.8Modular inverses#
a⁻¹ reads “a inverse” in this modular setting. The exponent −1 names an undo operation; it does not ask for a negative residue or an ordinary decimal reciprocal. In ax ≡ 1 (mod n), a and n are known and x is the inverse to find.
def inverse(a, n):
g, s, t = extended_gcd(a, n)
if g != 1:
raise ValueError('no modular inverse')
return s % n
# multiplication by 3 modulo 7:
# input x: 0 1 2 3 4 5 6
# output: 0 3 6 2 5 1 4
# x=5 lands at 1, so inverse(3,7)=5.
# multiplication by 2 modulo 8:
# output: 0 2 4 6 0 2 4 6 → 1 never appears.
raise ValueError(...) stops the calculation with an error message when the precondition for an inverse fails; it must not silently return an invented inverse.
Write a−1 for the modular inverse: a residue that undoes multiplication by a, not the ordinary fraction 1/a.
An inverse of modulo is a number satisfying:
It exists exactly when:
If extended Euclid produces:
then reducing modulo gives:
so:
Reduce a negative coefficient to the standard range by adding multiples of .
Undoing multiplication. Modulo 7, the inverse of 3 is 5 because 3×5 = 15 ≡ 1. Thus 3x ≡ 6 gives x ≡ 5×6 ≡ 2. Modulo 8, however, 2 has no inverse: 2 times any integer stays even and cannot be congruent to 1. The gcd condition tells you when this method is legal.
14.9Linear congruences#
A linear congruence asks which integer x makes a*x leave the same remainder as b when divided by n. The gcd d measures a shared obstruction. “d incongruent solutions modulo n” means d distinct representatives among 0,1,…,n−1, with infinitely many integer solutions obtained by adding multiples of n.
def solutions(a, b, n):
return [x for x in range(n) if (a*x - b) % n == 0]
# Small enumeration illustrates 4*x ≡ 6 (mod 10):
# x: 0 1 2 3 4 5 6 7 8 9
# 4*x %10: 0 4 8 2 6 0 4 8 2 6
# ↑ ↑
# solutions x=4,9. Reduce all three a,b,n by d=2:
# 2*x ≡ 3 (mod 5) → x≡4 (mod 5) → 4 and 9 modulo 10.
Consider:
Let:
A solution exists exactly when:
If , multiply by the inverse of :
If divides , the congruence has incongruent solutions modulo .
The existence test comes from 14.1. A solution means ax − b = kn for some integer k, so b = ax − kn. Since d divides both a and n, it must divide b. For example, 4x ≡ 5 (mod 10) has no solution: 4x − 5 is always odd, so 10 never divides it, matching gcd(4, 10) = 2 ∤ 5. But 4x ≡ 6 (mod 10) passes the test, since 2 ∣ 6. Divide everything by d = 2 to get 2x ≡ 3 (mod 5), solved by x ≡ 4 (mod 5) because 2 · 4 = 8 ≡ 3. Modulo 10 that is x ≡ 4 or x ≡ 9: two solutions, one for each of the d = 2 ways to lift a residue mod 5 to mod 10. Check: 16 and 36 both end in 6.
14.10Chinese remainder theorem#
Each subscript i selects one equation: nᵢ is its modulus and aᵢ its required remainder. “Pairwise coprime” means every pair of distinct moduli has gcd 1. N is the product of all moduli; Nᵢ = N/nᵢ omits just the ith one; yᵢ is the inverse of Nᵢ modulo nᵢ. The sum Σᵢ aᵢNᵢyᵢ adds one carefully isolated contribution per equation.
from math import prod
def crt(residues, moduli): # positive, pairwise coprime moduli
N = prod(moduli)
answer = 0
for ai, ni in zip(residues, moduli):
Ni = N // ni
yi = inverse(Ni, ni)
answer += ai * Ni * yi
return answer % N
# equation Ni yi contribution mod 3 mod 5
# x≡2 (mod 3) 5 2 2*5*2=20 2 0
# x≡3 (mod 5) 3 2 3*3*2=18 0 3
# sum=38, reduce modulo N=15 → 8 2 3
In the code, prod(moduli) multiplies the moduli, and zip(residues,moduli) pairs each remainder with its corresponding modulus. The two lists must have the same length. The zeroes in the other equation's column are why adding contributions does not undo earlier requirements. With non-coprime moduli, this formula is not directly available; a system can be incompatible or can have a different combined period.
For pairwise coprime moduli:
a system:
has a unique solution modulo:
A constructive method is:
- compute ;
- set:
- find satisfying:
- calculate:
Each term is built to do one job. Since Ni contains every modulus except ni, the term aiNiyi is ≡ 0 modulo every other modulus, and ≡ ai · 1 modulo ni. Adding the terms answers every congruence at once.
Example: solve x ≡ 2 (mod 3) and x ≡ 3 (mod 5). Here N = 15, N1 = 5 and N2 = 3. For y1, 5y ≡ 1 (mod 3) gives y1 = 2; for y2, 3y ≡ 1 (mod 5) gives y2 = 2. Then x ≡ 2 · 5 · 2 + 3 · 3 · 2 = 38 ≡ 8 (mod 15). Check: 8 leaves remainder 2 on division by 3 and 3 on division by 5.
For small systems, substitution is often faster. Here the numbers ≡ 3 (mod 5) are 3, 8, 13, …, and the first one that is ≡ 2 (mod 3) is 8.
15Number bases#
In the numeral (dₖ dₖ₋₁ … d₀)ᵦ, b is the base; dᵢ is the digit in position i; the rightmost position is 0. The subscript b labels the representation, not multiplication by b. A digit's value is dᵢ × bⁱ, and Σ adds those contributions. The most significant digit is the leftmost nonzero one.
numeral (2301)₄ digit place value contribution
position 3 2 4³=64 128
position 2 3 4²=16 48
position 1 0 4¹=4 0
position 0 1 4⁰=1 1
---
integer represented 177
The division algorithm already splits a number into quotient and remainder. Base conversion repeats that operation: the remainder is the last digit, and the quotient contains the digits still to recover.
A base- numeral:
represents:
Allowed digits are:
15.1Converting to decimal#
You can compute place values explicitly or accumulate from the left. Multiplying the current partial value by b shifts its digits one place left; adding the next digit fills the empty place.
def value_of(digits, base):
value = 0
for digit in digits:
value = value * base + digit
return value
# digits=[2,3,0,1], base=4:
# 0 → 0*4+2=2 → 2*4+3=11 → 11*4+0=44 → 44*4+1=177
Expand by place value:
That is 128 + 48 + 0 + 1 = 177. Each place is worth b times the place to its right, just as a base-10 tens digit is worth ten ones.
15.2Converting from decimal#
def base_digits(N, base): # N≥0, integer base≥2
if N == 0:
return [0]
reversed_digits = []
while N > 0:
N, remainder = divmod(N, base)
reversed_digits.append(remainder)
return reversed_digits[::-1]
# 177 → quotient 44, remainder 1 → quotient 11, remainder 0
# → quotient 2, remainder 3 → quotient 0, remainder 2
# collected [1,0,3,2]; reversed [2,3,0,1].
The slice [::-1] reads a list backward: the −1 is the step. divmod(N,base) returns the quotient and remainder together, assigned to N and remainder using their old input values.
Repeatedly divide by the target base:
- divide by ;
- record the remainder;
- divide the quotient again;
- read remainders from last to first.
The first remainder produced is the least significant digit.
Converting 177 back to base 4 reverses 15.1:
177 = 44 · 4 + 1 last digit 1
44 = 11 · 4 + 0 next digit 0
11 = 2 · 4 + 3 next digit 3
2 = 0 · 4 + 2 first digit 2 read upward: (2301)₄
Dividing by 4 shifts every base-4 digit one place right, and the digit that falls off the end is the remainder. That is why the remainders come out last digit first.
15.3Binary, octal, and hexadecimal#
A bit is one binary digit, 0 or 1. Hexadecimal needs six extra digit symbols: A=10, B=11, C=12, D=13, E=14, F=15. Groups are made from the right, adding leading zeroes if needed; leading zeroes do not change the value.
binary bits: 1 0 1 1 0 1 1 1
hex groups (4 bits): [1 0 1 1] [0 1 1 1] → B7
8+2+1=11 4+2+1=7
octal groups (3 bits): [0 1 0] [1 1 0] [1 1 1] → 267
2 6 7
# In Python, int('B7',16)=183 and format(183,'b')='10110111'.
⌊x⌋ (“floor x”) rounds down to an integer; ⌈x⌉ (“ceiling x”) rounds up. For example ⌊2.8⌋=2 and ⌈2.8⌉=3. A width of w bits provides 2ʷ different patterns. The interval of integers 0 through N has N+1 values. If N=7, 3 bits suffice (000 through 111); N=8 needs 4 bits (0000 through 1000). The formula for positive N's own bit length excludes N=0; the numeral 0 is conventionally written with one digit, even though a singleton set needs zero bits of distinguishing information.
Binary uses base .
Octal uses base , so each octal digit corresponds to 3 bits.
Hexadecimal uses base , so each hex digit corresponds to 4 bits.
| Hex | Binary |
|---|---|
| 0 | 0000 |
| 1 | 0001 |
| 8 | 1000 |
| A | 1010 |
| F | 1111 |
The grouping works because 16 = 24: each hex digit stands for exactly four bits, with no carrying between groups. To convert (10110111)2, split it from the right into 1011 0111, giving B7 in hex; split it into 10 110 111, giving 267 in octal. All three name 183.
The number of bits needed to represent positive is:
The number of bits needed to represent all integers from through is:
Check the first formula on 7 = (111)2 and 8 = (1000)2: ⌊log27⌋ + 1 = 2 + 1 = 3 bits and ⌊log28⌋ + 1 = 3 + 1 = 4 bits. A new bit is needed exactly at each power of 2. The second formula counts values instead: 0 through N is N + 1 values, and b bits give 2b patterns, so b must satisfy 2b ≥ N + 1.
16Modular exponentiation and cryptography#
Keep three quantities separate: the base a is multiplied repeatedly; the exponent k says how many factors; the modulus n sets the remainder range. Thus 3¹³ mod 7 uses base 3, exponent 13, and modulus 7. Cryptographic keys will assign new names to some of these roles.
You can now reduce remainders, find gcds, and undo multiplication with a modular inverse. Cryptography combines those operations. Binary exponents keep the arithmetic small; Euler’s theorem explains when powers repeat.
16.1Fast modular exponentiation#
def mod_power(a, k, n): # k≥0, n>0
result = 1 % n
a %= n
while k > 0:
if k % 2 == 1: # rightmost exponent bit is 1
result = result * a % n
a = a * a % n # next power: a, a², a⁴, a⁸,...
k //= 2 # remove rightmost binary digit
return result
# 13 = (1101)₂ = 8+4+1; process bits from right to left.
# remaining k current power mod 7 use it? result after use
# 13 3 yes 3
# 6 2 no 3
# 3 4 yes 5
# 1 2 yes 3
# 0 - stop 3
# Python's built-in pow(3,13,7) performs modular exponentiation.
The result column reduces after each multiplication: 3×4 mod 7=5, then 5×2 mod 7=3. The O(log k) count concerns modular multiplications; it does not claim each multiplication of arbitrarily large integers costs constant bit operations.
To calculate:
do not construct directly.
Write in binary and repeatedly square:
Reduce modulo after every multiplication.
If:
then:
Use only powers corresponding to 1-bits in the exponent.
This requires only:
squaring/multiplication steps.
Compute 313 mod 7. Square and reduce each time: 31 ≡ 3, 32 = 9 ≡ 2, 34 ≡ 22 = 4, and 38 ≡ 42 = 16 ≡ 2. Since 13 = 8 + 4 + 1, 313 ≡ 2 · 4 · 3 = 24 ≡ 3 (mod 7). No number larger than 24 ever appeared, while 313 itself is 1,594,323. The exponent k has about log2k bits, and each bit costs one squaring and at most one multiplication, which is where O(log k) comes from.
16.2Euler’s totient function#
The Greek letter φ (“phi”) names the totient function here: φ(n) counts invertible residue classes modulo n. In the product formula, Π (capital pi) means multiply rather than add. The expression Πₚ∣ₙ(1−1/p) uses each distinct prime divisor p of n once, even if p occurs repeatedly in n's factorization. An exponent eᵢ in pᵢeᵢ counts repeated copies of that prime.
from math import gcd
def totient(n): # simple counting version, n>0
return sum(1 for k in range(1, n + 1) if gcd(k, n) == 1)
# candidate k: 1 2 3 4 5 6 7 8 9 10 11 12
# coprime 12: ✓ × × × ✓ × ✓ × × × ✓ ×
# φ(12)=4. Product version: 12*(1-1/2)*(1-1/3)=4.
# Prime 2 appears twice in 12=2²*3, but removes even numbers only once.
counts positive integers no greater than that are coprime to . For example, φ(10) = 4, counting 1, 3, 7 and 9; the other six numbers share a factor 2 or 5 with 10. These are exactly the residues that have inverses modulo n (14.8), which is why φ matters for cryptography.
For prime :
For distinct primes :
The product formula is a count of what is left after removing multiples. Among 1, …, pq, the numbers sharing a factor with pq are the q multiples of p and the p multiples of q, with pq itself counted in both. That leaves pq − q − p + 1 = (p − 1)(q − 1). For 10 = 2 · 5 it gives 1 · 4 = 4, as counted above.
More generally, if n has prime factorization (distinct primes pi with exponents ei ≥ 1):
then:
Each factor 1 − 1/p removes the fraction of numbers divisible by p. For 12 = 22 · 3, φ(12) = 12 · ½ · ⅔ = 4, counting 1, 5, 7 and 11.
16.3Fermat’s little theorem#
The notation p ∤ a reads “p does not divide a.” With prime p this is equivalent to gcd(a,p)=1. The theorem says a block of p−1 factors of a has remainder 1, so full blocks can be removed from an exponent when that condition holds.
# 3**100 modulo 7; p-1=6
q, r = divmod(100, 6) # q=16, r=4
# factors: [3⁶][3⁶] ... [3⁶] [3⁴]
# 16 blocks leftover
# mod 7: 1*1*...*1 * 4 = 4
answer = pow(3, r, 7) # 4
# Do not reduce 7**6's exponent to 0 modulo 6 and claim 1 mod 7;
# base 7 is divisible by p and its positive powers are 0 modulo 7.
If is prime and (p does not divide a), then:
Check it with p = 7 and a = 3: 36 = 729 = 104 · 7 + 1, so 36 ≡ 1 (mod 7).
Equivalent general form:
This can reduce large exponents modulo when is not divisible by . Since 36 ≡ 1 (mod 7), every block of six factors of 3 collapses to 1. For 3100, write 100 = 16 · 6 + 4, so 3100 ≡ 34 = 81 ≡ 4 (mod 7). The same reasoning confirms 16.1: 313 = (36)2 · 3 ≡ 3.
16.4Euler’s theorem#
Euler replaces prime p by any positive modulus n, provided the base a is coprime to n. The exponent cycle length φ(n) is a valid period, not necessarily the shortest one.
# a=3, n=10, φ(10)=4
# exponent: 0 1 2 3 4 5 6 7 8
# 3**exponent
# modulo 10: 1 3 9 7 1 3 9 7 1
# |----------|----------| blocks of length 4
# Thus 3**123 mod 10 = 3**(123 % 4) mod 10 = 7.
If:
then:
Fermat’s theorem is the prime-modulus special case, because φ(p) = p − 1. With n = 10 and φ(10) = 4: 34 = 81 ≡ 1 (mod 10). The gcd condition is essential. Powers of 2 modulo 10 run 2, 4, 8, 6, 2, … and never reach 1, because every power of 2 is even.
16.5Cryptographic terminology#
shared-secret scheme:
plaintext → encrypt with secret K → ciphertext → decrypt with K → plaintext
public-key encryption:
sender uses recipient's PUBLIC key → ciphertext
recipient uses their PRIVATE key → plaintext
digital signature (different purpose):
signer uses PRIVATE key → signature; verifier uses PUBLIC key → valid/invalid
Encryption protects confidentiality. A signature establishes authenticity and detects changes; signing is not simply “encrypting the message backward.” Encoding, such as converting letters to numbers, is reversible without a secret. A cryptographic hash is a fixed-length fingerprint, not an encryption format from which the original can ordinarily be recovered.
- Plaintext: original information.
- Ciphertext: encrypted information.
- Encryption: transforms plaintext into ciphertext using a key.
- Decryption: reverses the transformation with the appropriate key.
- Symmetric encryption: the same shared secret, or directly related secrets, perform encryption and decryption.
- Public-key encryption: a public key encrypts or verifies; a private key decrypts or signs.
- Encoding: changes representation but does not provide secrecy.
- Hashing: maps input to a fixed-size digest and is intended to be one-way.
16.6Shift cipher#
Let x be a plaintext letter number, y its ciphertext number, and k the secret shift. E names the encryption function; D names decryption. Both results are reduced to the alphabet's residue range.
def shift_letter(letter, k):
x = ord(letter) - ord('A')
y = (x + k) % 26
return chr(ord('A') + y)
# k=3: A B C ... X Y Z
# D E F ... A B C (wrap around)
# Encrypt C(2) → F(5); decrypt F with k=-3 → C(2).
ord(letter) gets a character's numeric code; subtracting ord('A') maps A through Z to 0 through 25. chr(...) converts a numeric character code back to a letter. This example accepts uppercase English letters, not arbitrary text.
Represent letters as residues, often:
Encryption shifts each letter by the key k:
Decryption:
With k = 3, CAT is (2, 0, 19), which shifts to (5, 3, 22), or FDW; decrypting subtracts 3 again. A Caesar cipher is insecure because there are only 26 possible shifts: an attacker can simply try them all.
16.7Affine cipher#
The two-part key (a,b) first multiplies by a, then adds b. To undo a composition, reverse the order: subtract b first, then multiply by a's modular inverse. Here m is the alphabet size, not a message.
def affine_encrypt(x, a, b, m=26):
return (a*x + b) % m
def affine_decrypt(y, a, b, m=26):
return inverse(a, m) * (y - b) % m
# forward: C=2 --multiply 5→ 10 --add 8→ 18=S
# reverse: S=18 --subtract 8→ 10 --multiply 21 mod 26→ 2=C
# 21 is the inverse of 5 modulo 26, since 5*21=4*26+1.
Encryption with key (a, b) over an alphabet of m letters (m = 26 for English):
The multiplier must be invertible modulo :
Decryption:
The modular inverse is what makes the transformation reversible. See what fails without it: with a = 2, A (0) and N (13) both encrypt to b, since 2 · 13 = 26 ≡ 0, and no decryption can tell them apart. With a = 5 and b = 8, C (2) encrypts to 5 · 2 + 8 = 18, or S. Since 5 · 21 = 105 ≡ 1 (mod 26), 5−1 ≡ 21, and decryption gives 21(18 − 8) = 210 ≡ 2, back to C.
16.8RSA#
Keep an RSA role card beside the arithmetic. p and q are distinct secret primes; n is their public product; φ(n) counts the invertible residues; e is the public exponent; d is the private inverse of e modulo φ(n); m is the numeric message representative; c is its ciphertext. The same letter m denoted alphabet size in the affine cipher, but here it denotes a message.
secret p,q → n=p*q and φ(n)=(p-1)*(q-1)
↓ choose coprime e, find inverse d
public (n,e): m -- raise to e mod n → c
private d: m ← raise to d mod n -- c
# The modulus for MESSAGE arithmetic is n.
# The modulus for finding exponent d is φ(n). Do not exchange them.
RSA connects nearly every number-theory mechanism in the course. The idea is short. Anyone may encrypt with the public pair (n, e). Undoing that needs the private exponent d, and computing d needs φ(n) = (p − 1)(q − 1), which in practice means factoring n. Multiplying two primes is easy; recovering them from a product hundreds of digits long is not.
Key generation#
p, q = 3, 11 # deliberately tiny teaching primes
n = p * q # 33
phi = (p - 1) * (q - 1) # 20
e = 3 # 1<e<20 and gcd(3,20)=1
d = inverse(e, phi) # 7
# exponent check: e*d=21=1+1*20 → e*d≡1 (mod 20)
Choose distinct primes:
Compute:
Compute:
Choose a public exponent satisfying:
and:
Find the private exponent:
The public key is:
The private information includes , and in real implementations usually as well.
Encryption#
m = 4 # must satisfy 0≤m<n
c = pow(m, e, n) # 4³ mod 33=31
# message 4 --public exponent 3→ integer 64 --mod 33→ ciphertext 31
For message representative with:
calculate:
Decryption#
recovered = pow(c, d, n) # 31⁷ mod 33=4
# 31≡-2 (mod 33); (-2)⁷=-128; -128+4*33=4.
# Use pow with a modulus rather than constructing the enormous power.
Use fast modular exponentiation for both.
A toy run. Take p = 3 and q = 11, so n = 33 and φ(n) = 2 · 10 = 20. Choose e = 3, since gcd(3, 20) = 1. Then d = 7, because 3 · 7 = 21 ≡ 1 (mod 20). Encrypt m = 4: c = 43 = 64 ≡ 31 (mod 33). Decrypt: 31 ≡ −2 (mod 33), so 317 ≡ (−2)7 = −128 ≡ −128 + 4 · 33 = 4. The message comes back.
Why it works#
The exponent product ed means e×d, not a separate function. The inverse relation gives ed = 1 + kφ(n) for an integer k. Raising m to that exponent means one leftover factor m times k blocks of m^φ(n). When gcd(m,n)=1, each block reduces to 1.
m --encrypt→ mᵉ --decrypt→ (mᵉ)ᵈ = mᵉᵈ
= m¹⁺ᵏφ⁽ⁿ⁾
= m * (m^φ(n))^k
≡ m * 1^k ≡ m (mod n)
# Toy exponent chain: 4³ then seventh power → 4²¹ = 4*(4²⁰).
# Since φ(33)=20 and gcd(4,33)=1, 4²⁰≡1 (mod 33).
Because:
there is an integer such that:
When gcd(m, n) = 1, Euler’s theorem gives:
The Euler argument above requires the message to be coprime to n. RSA also recovers other message representatives: work modulo each prime separately. If p divides the message, both the original and recovered value are 0 modulo p; otherwise Fermat’s theorem applies. Do the same modulo q, then use the Chinese remainder theorem to obtain the same message modulo pq. Textbook RSA here illustrates the arithmetic; real encryption also needs a secure padding scheme.
RSA traps#
- Find modulo , not modulo .
- must be coprime to .
- The message block must be smaller than .
- Reduce after every multiplication.
- In toy problems, factorization of is intentionally feasible; real RSA uses enormous primes.
17Recursion and recurrence relations#
Two different mathematical descriptions. A recursive function is executable instructions that call themselves. A recurrence is an equation relating values at different indices; it can describe a function's answer, a population, or a runtime. aₙ (“a sub n”) is the value at index n; aₙ₋₁ is the preceding value, not aₙ minus 1. A closed form, such as aₙ=2ⁿ−1, computes a value directly from its index without needing earlier sequence values. T(n) will instead name work on input size n.
answer sequence: factorial(n) = n * factorial(n-1)
work sequence: T(n) = T(n-1) + constant work
# The factorial answer grows as n!, but this procedure makes n calls
# below the initial call and has linear call count.
Recall recursive sequences from C959: an initial value plus a rule determines later values. A recursive program uses the same idea to solve smaller inputs. Distinguish the value it returns from the number of operations it performs; each may obey a different recurrence.
17.1Recursive definitions#
The base case anchors the definition; the rule alone does not determine a unique sequence. For example aₙ = aₙ₋₁ + 2 gives 0,2,4,… if a₀=0, but 5,7,9,… if a₀=5.
factorial(3)
waits for factorial(2)
waits for factorial(1)
factorial(0) returns 1 ← base case
# Inputs 3→2→1→0 strictly approach the base.
# Calling factorial(n+1) instead would move away from it.
A recursive definition contains:
- one or more base cases;
- a rule defining larger objects from smaller ones.
For factorial:
A recursive algorithm must move toward a base case. Otherwise it fails to terminate.
17.2Call stack and unwinding#
A stack frame remembers a call's local n and the operation still waiting for its child result. A stack is last-in, first-out: the deepest unfinished call must return first.
def f(n): # nonnegative integer n
if n == 0:
return 0
child = f(n - 1)
return n + child
call downward return upward
f(3): wait for 3 + ? 3 + 3 = 6
f(2): wait for 2 + ? 2 + 1 = 3
f(1): wait for 1 + ? 1 + 0 = 1
f(0): base 0
# f(3) does not return when f(0) returns; it still has additions to do.
When a recursive function calls itself, the current operation waits on the call stack.
For:
the calls expand:
Then return values unwind:
For subtraction, division, or multiple recursive calls, the return order matters.
17.3Recurrence relations#
“Order” measures the furthest index lag, not the number of terms written. A rule involving aₙ₋₃ is third-order even if it skips aₙ₋₁ and aₙ₋₂. The first-order rule below needs a₀; the second-order example usually needs a₀ and a₁.
a = [0] # a₀=0
for n in range(1, 5):
a.append(3*a[n - 1] + 2)
# index n: 0 1 2 3 4
# aₙ: 0 2 8 26 80
# dependency: a₀ → a₁ → a₂ → a₃ → a₄
A recurrence defines sequence terms from preceding terms.
The order is the maximum number of positions referenced backward.
is first-order.
is second-order.
Initial conditions are required to select one particular solution.
17.4Arithmetic and geometric recurrences#
In an arithmetic recurrence, d is the fixed difference added each step. In a geometric recurrence, r is the fixed ratio multiplied each step. a₀ is the starting value, so reaching aₙ requires n steps, not n+1.
# arithmetic: start a₀=5, add d=3
# index n: 0 1 2 3
# aₙ=5+3*n: 5 8 11 14
# +3 +3 +3
# geometric: start a₀=5, multiply r=2
# aₙ=5*2**n: 5 10 20 40
# ×2 ×2 ×2
Arithmetic:
Solution:
Geometric:
Solution:
17.5First-order linear recurrence#
In aₙ = r aₙ₋₁ + c, r scales the previous value and c is the fixed new addition. The letter c here is not the Big-O bound constant from chapter 12. Every added c is multiplied by r on later steps; an older deposit has more time to grow.
def growth(start, r, c, steps):
value = start
for _ in range(steps):
value = r*value + c
return value
# Track three steps without hiding the deposits:
# a₁ = r*a₀ + c
# a₂ = r²*a₀ + r*c + c
# a₃ = r³*a₀ + r²*c + r*c + c
# initial old deposit ... newest deposit
# r=2,c=1,a₀=0: a₃ = 0 + 4 + 2 + 1 = 7.
The underscore _ is a loop variable whose value is deliberately unused; only the number of repetitions matters. The geometric-sum fraction (rⁿ−1)/(r−1) replaces 1+r+…+rⁿ⁻¹; it is not valid when r=1 because that would divide by zero. At r=1 there are simply n copies of c.
For:
repeated substitution gives:
When :
When :
This form models repeated growth plus a fixed deposit or input. Take an = 2an−1 + 1 with a0 = 0, the move count for the Towers of Hanoi: the terms run 0, 1, 3, 7, 15, 31. The formula gives 2n · 0 + 1 · (2n − 1)/(2 − 1) = 2n − 1, matching every term. The pattern comes from substituting once more: an = r(ran−2 + c) + c = r2an−2 + c(1 + r); each further step multiplies the old terms by r and adds one more c.
The timing matters. “Add the deposit, then apply interest” and “apply interest, then add the deposit” produce different recurrences.
17.6Solving by telescoping#
Here f(k) names the increment at step k, not necessarily a recursive function. Sum one equation for each step. Every intermediate value appears once positively and once negatively, so only the endpoints remain.
a₁ - a₀ = f(1)
a₂ - a₁ = f(2)
a₃ - a₂ = f(3)
------------------ add vertically
a₃ - a₀ = f(1)+f(2)+f(3)
# Odd increments build square layers:
# 1²: # 2²: ## 3²: ###
# ## ###
# ###
# new cells: 1, then 3, then 5. Total after n layers = n².
a = 0
for k in range(1, n + 1):
a += 2*k - 1 # same as sum(2*k-1 for k in ...)
If:
sum both sides:
so:
This converts the recurrence into a known finite sum. The name comes from the middle terms: in (a1 − a0) + (a2 − a1) + ⋯ + (an − an−1) every term except an and −a0 cancels, the way a telescope collapses. For an = an−1 + (2n − 1) with a0 = 0, the sum of the first n odd numbers gives an = n2: 1, 4, 9, 16, ….
17.7Second-order homogeneous recurrences#
In aₙ = c₁aₙ₋₁ + c₂aₙ₋₂, c₁ and c₂ are fixed coefficients. “Linear” means the earlier sequence values are multiplied by constants and added, not squared or multiplied together. “Homogeneous” means no extra forcing term such as +7 or +n. Try a geometric component rⁿ: substituting gives rⁿ = c₁rⁿ⁻¹ + c₂rⁿ⁻², which leads to r² = c₁r + c₂. Move everything to the left to obtain the characteristic equation.
# recurrence: aₙ=5*aₙ₋₁ - 6*aₙ₋₂
# coefficient card: c₁=5, c₂=-6
# polynomial: r² - c₁*r - c₂ = r² - 5*r + 6
# factor: (r-2)*(r-3)=0 → allowed ratios r=2 or r=3
# The minus sign before c₂ turns -(-6) into +6.
A root is a value making that polynomial zero. Distinct roots give two different geometric components. The standard repeated-root formulas below assume the nondegenerate second-order setting; if the coefficient of aₙ₋₂ is zero, the rule reduces to lower order and should be treated directly.
Suppose:
A first-order rule an = ran−1 produces the geometric sequence rn (17.4). Guess the same shape here and ask which ratios r work. Try a solution:
Substitution yields:
This is the characteristic equation.
Distinct roots#
A and B are constants chosen to fit the starting values; they are not new sequences. For the example below, each equation comes from a specific index.
general: aₙ = A*2ⁿ + B*3ⁿ
n=0: a₀=0 → A+B=0
n=1: a₁=1 → 2A+3B=1
subtract 2*(first equation) from second → B=1, then A=-1.
# index n 0 1 2 3
# 3**n - 2**n 0 1 5 19
# recurrence: next = 5*current - 6*previous
# 5*1-6*0=5; 5*5-6*1=19
If roots are :
Use two initial conditions to determine .
Example: an = 5an−1 − 6an−2 with a0 = 0 and a1 = 1. The characteristic equation r2 − 5r + 6 = 0 has roots 2 and 3, so an = A · 2n + B · 3n. The initial values give A + B = 0 and 2A + 3B = 1, so B = 1, A = −1, and an = 3n − 2n. Check against the recurrence: a2 = 5 · 1 − 6 · 0 = 5 = 9 − 4, and a3 = 5 · 5 − 6 · 1 = 19 = 27 − 8.
Repeated root#
If both roots coincide, writing A rⁿ + B rⁿ would collapse to just one adjustable constant, (A+B). Multiplying the second component by n provides the missing independent shape.
# aₙ=4*aₙ₋₁-4*aₙ₋₂; a₀=1, a₁=4
# characteristic: r²-4r+4=(r-2)², repeated root r=2.
# aₙ=(A+B*n)*2**n
# n=0: 1=A. n=1: 4=(1+B)*2 → B=1.
# index n: 0 1 2 3
# (1+n)*2**n: 1 4 12 32
# check: 4*4-4*1=12; 4*12-4*4=32.
If one root has multiplicity two:
The factor supplies the second independent solution.
17.8Fibonacci sequence#
Fₙ is the nth Fibonacci number. The rule adds the two preceding values, so two initial values are supplied. The Greek letters φ (“phi”) and ψ (“psi”) below are the two roots of a quadratic; φ without an argument here means the golden ratio, not the totient function φ(n).
def fibonacci(n): # nonnegative integer n
previous, current = 0, 1
for _ in range(n):
previous, current = current, previous + current
return previous
# n: 0 1 2 3 4 5 6 7 8 9 10
# Fₙ: 0 1 1 2 3 5 8 13 21 34 55
# ↖ 21+34=55
# The closed form combines two geometric components φⁿ and ψⁿ.
In the closed form below, √5 is the positive square root of 5; |ψ| is ψ's absolute value, ignoring its negative sign. Since ψ is about −0.618, its magnitude is less than 1 and repeated powers get smaller. Although the formula uses irrational square roots, their combination is exactly an integer. Floating-point code can lose this exactness for large n; the addition-based recurrence avoids that issue. In the Python assignment, both right-hand values are computed from the old pair before either variable changes.
Its characteristic equation is:
with roots:
Here φ is the golden ratio, unrelated to Euler’s totient φ(n) from 16.2. The other root is:
leading to:
Because |ψ| < 1, the ψn term shrinks toward 0, so Fn is the nearest integer to φn/√5. For n = 10 that is about 55.004, and F10 = 55. Fibonacci numbers therefore grow exponentially, each about 1.618 times the one before.
For exam purposes, the recurrence mechanism and growth behavior are more important than reproducing this closed form.
17.9Runtime recurrences#
The plus sign separates the work done in recursive children from the local work of the current call. A term Θ(1) is a bounded positive constant amount of local work. Base cases, usually T(1)=Θ(1), stop the expansion.
example code shape runtime equation
one call on n-1, one addition T(n)=T(n-1)+Θ(1)
one call on n-1, loop of n items T(n)=T(n-1)+Θ(n)
one call on half, one comparison T(n)=T(n/2)+Θ(1)
two calls on halves, merge n items T(n)=2T(n/2)+Θ(n)
A runtime recurrence counts calls and local work, not the returned value. Write T(n) for the work on an input of size n. In T(n) = 2T(n/2) + Θ(n), the 2 counts recursive calls, n/2 is their input size, and Θ(n) is the work outside them. For merge sort, each level processes n items in total, not n per call; about log₂n levels give Θ(n log n).
Linear recursion#
# def count(n):
# if n == 0: return 0
# return 1 + count(n-1)
# work per call: 1 → 1 → 1 → 1 → base
# input sizes: 4 → 3 → 2 → 1 → 0
# Add n constant contributions, not n*n.
Unrolling produces constant contributions:
Linear work at every decrement#
WORK(n):
if n == 0: return
inspect each of n items
WORK(n-1)
# n=4 local-work rows:
# ****
# ***
# **
# * total 4+3+2+1=10; triangle growth.
This sums:
so:
Repeated halving#
HALVE(n):
if n <= 1: return
do one fixed-cost action
HALVE(floor(n/2))
# n=16 → 8 → 4 → 2 → 1(base)
# Four constant-work non-base calls, not 16.
There are levels:
Merge-sort form#
size at each call calls at level work each level total
n 1 n n
n/2 2 n/2 n
n/4 4 n/4 n
...
1 n 1 n
# For n=8: level totals 8,8,8,8. Number of levels grows as log₂n.
There are levels and work per level:
Naive Fibonacci#
fib(5)
├─ fib(4)
│ ├─ fib(3)
│ └─ fib(2) ← computed again below
└─ fib(3) ← computed again, including its own fib(2)
# Memoization turns repeated subtrees into one stored answer per index.
# Naive call count grows exponentially even though this table-free
# algorithm has only O(n) simultaneous frames on its deepest stack.
The recursion tree branches and repeats subproblems, giving exponential growth.
17.10Simplified Master theorem#
Translate the recurrence before selecting a case. In T(n)=aT(n/b)+f(n), a counts recursive children, b is the factor shrinking each child's input, and f(n) is the total local work at that call. Neither a nor b may grow with n. The comparison n^(log_b a) estimates the number of leaf calls. “Polynomially smaller” means smaller by a factor of nε for some fixed ε>0, not merely by a logarithm.
# Example: T(n)=4*T(n/2)+n, with n=8
level calls size per call local work total
0 1 8 8
1 4 4 16
2 16 2 32
3 64 1 64
# Work grows toward the leaves.
# log_b(a)=log₂4=2 → leaf count n² → Θ(n²).
For the larger-local-work case, the usual regularity requirement is a f(n/b) ≤ c f(n) for some fixed c<1 beyond a threshold. It ensures successive levels really get cheaper. With f(n)=n², a=2, b=2, the child-level total is 2*(n/2)²=n²/2, so c=1/2 works. If f(n) differs from the leaf term by only a log factor, or regularity is unknown, this simplified three-case statement may not apply; unroll the recurrence rather than forcing a case.
recognition guide:
smaller local work: levels n → 2n → 4n ... → n² leaves dominate
matching work: levels n → n → n ... add log n levels
larger local work: levels n² → n²/2 → n²/4 ... top dominates
# These sketches illustrate appropriate recurrences, not one equation.
This form assumes a fixed number a ≥ 1 of subproblems, a fixed shrink factor b > 1, and constant-size base cases. It does not apply to recurrences such as T(n−1), where the input shrinks by subtraction rather than division.
For:
compare with:
- If is polynomially smaller, recursive leaves dominate:
- If they are the same order:
- If is polynomially larger and regularity conditions hold:
The comparison has a picture behind it. In the recursion tree, the top call does f(n) work, and the tree has alogbn = nlogba leaves. Whichever end is heavier dominates the total; when they match, every one of the logbn levels does about the same work, which adds the log factor.
- Merge sort, T(n) = 2T(n/2) + n: nlog22 = n matches f(n) = n, so Θ(n log n).
- Binary search, T(n) = T(n/2) + 1: nlog21 = n0 = 1 matches f(n) = 1, so Θ(log n).
- T(n) = 4T(n/2) + n: nlog24 = n2 beats n, so the leaves dominate: Θ(n2).
- T(n) = 2T(n/2) + n2: n2 beats nlog22 = n, so the top dominates: Θ(n2).
This is a structured way to read divide-and-conquer recurrences.
18Mathematical induction#
P(n) names a proposition about integer n, such as “the first n positive integers sum to n(n+1)/2.” n₀ is the first index covered; k is an arbitrary index at least n₀. P(k) ⇒ P(k+1) reads “if the claim holds at k, then it holds at the next index.” An arrow is a logical implication, not numerical addition.
base proven step works for EVERY allowable k
P(n₀) ─────────→ P(n₀+1) ─────────→ P(n₀+2) ─────────→ ...
↑
one starting domino the same argument links each neighbor
# Proof recipe (not an executable finite test):
# 1. Prove P(n₀).
# 2. Choose arbitrary k≥n₀ and assume P(k).
# 3. Derive P(k+1) using that assumption.
# 4. Conclude P(n) for every integer n≥n₀.
A loop checking P(n) for the first thousand values is evidence, not induction. Induction succeeds because the step is valid for an arbitrary k, including indices beyond any finite check.
Induction proves statements indexed by integers. Picture a long row of dominoes. You do not need to watch each one fall; it is enough to know that the first one falls and that any falling domino knocks over the next. Induction proves a claim P(n) for every n the same way, in three parts.
To prove for every :
Base case#
Prove:
Inductive hypothesis#
Assume:
for an arbitrary .
Inductive step#
Use the hypothesis to prove:
This establishes the chain:
The inductive hypothesis is a temporary assumption about , not permission to assume the case.
18.1Induction for sums#
claim P(n): sum of 1..n = n*(n+1)/2
base n=1: 1 = 1*2/2
hypothesis: 1+...+k = k*(k+1)/2
next case:
1+...+k+(k+1)
= k*(k+1)/2 + (k+1) replace ONLY the old sum
= (k+1)*(k/2+1) factor (k+1)
= (k+1)*(k+2)/2 desired formula with n replaced by k+1
# Code correspondence: extending a running total by one term.
total_next = total_through_k + (k + 1)
# Hypothesis supplies total_through_k, not total_next.
To prove a formula for:
the case begins by separating the new term:
Then replace the first portion using the inductive hypothesis.
That is the standard mechanism for identities involving cumulative sums.
For the sum identity 1 + … + n = n(n+1)/2, the base n = 1 gives 1 = 1. Assume the identity for k. Then the next sum is k(k+1)/2 + (k+1) = (k+1)(k+2)/2, exactly the required formula for k+1. The proof works because the new case contains the old case plus one new term.
18.2Induction for divisibility#
In d ∣ f(n), d is the fixed proposed divisor and f(n) is an integer expression. To use the hypothesis, turn divisibility into an equality with an integer multiplier m. This m is a witness, not necessarily a constant independent of k.
claim: 6 divides 7ⁿ−1, for integer n≥1
hypothesis: 7ᵏ−1 = 6*m for some integer m
next expression: 7ᵏ⁺¹−1
= 7*(7ᵏ−1)+6
= 7*(6*m)+6
= 6*(7*m+1)
# New witness = 7*m+1, still an integer.
# If k=1, m=1; k=2 gives witness 8 and 7²−1=6*8=48.
# Computational view: remainder stays zero under this update.
To prove:
show:
- ;
- assuming: for some integer , rewrite as:
and factor out .
Example: 6 ∣ 7n − 1 for all n ≥ 1. Base: 7 − 1 = 6. Step: assume 7k − 1 = 6m. Then 7k+1 − 1 = 7 · 7k − 1 = 7(7k − 1) + 6 = 7 · 6m + 6 = 6(7m + 1). The trick is always to make the old expression appear inside the new one, so the hypothesis can replace it.
18.3Induction for inequalities#
Write the target for k+1 before manipulating the hypothesis. For n! > 2ⁿ, the next target is (k+1)! > 2^(k+1), not a statement involving k alone. A positive multiplier preserves the direction of an inequality; a negative multiplier reverses it.
k! > 2ᵏ hypothesis
(k+1)*k! > (k+1)*2ᵏ multiply by positive k+1
(k+1)*2ᵏ > 2*2ᵏ because k+1>2
therefore (k+1)! > 2ᵏ⁺¹
# ratio picture: factorial gains a factor k+1;
# powers of two gain only a factor 2.
# Once ahead at n=4, factorial stays ahead under each next step.
The inductive hypothesis gives a bound on the -case. Manipulate it in a direction that produces the required bound.
Example: n! > 2n for all n ≥ 4. Base: 4! = 24 > 16. Step: assume k! > 2k for some k ≥ 4. Then (k + 1)! = (k + 1) · k! > (k + 1) · 2k > 2 · 2k = 2k+1, using k + 1 > 2. The base must start at 4, because the claim is false for n = 1, 2, 3.
Be careful:
- multiplying by a negative reverses inequalities;
- taking reciprocals of positive values reverses inequalities;
- a true base case does not repair an invalid inductive step.
18.4Strong induction#
Ordinary induction gives one earlier case P(k); strong induction permits all earlier cases P(n₀) through P(k). It still requires a base case and may require several bases if the step needs several prior values.
PROVE_FACTORABLE(n): # describes proof structure
if n is prime: use n itself
otherwise write n=a*b with 2≤a,b<n
use already-established claims for a and b
concatenate their prime factorizations
84
├─ 7 → prime
└─ 12
├─ 3 → prime
└─ 4
├─ 2 → prime
└─ 2 → prime
# Every child is smaller than its parent, but not necessarily one smaller.
Strong induction assumes:
and proves .
Use it when the next case depends on several earlier cases. The standard example is that every integer n ≥ 2 is a product of primes. If n is prime, done. Otherwise n = ab with both factors between 2 and n − 1, and the claim is needed for a and b, not for n − 1. For 84 = 7 · 12, knowing the claim for 83 is no help; knowing it for 7 and 12 is.
Typical uses:
- prime factorization;
- recursively reducing by varying amounts;
- Fibonacci-like recurrences;
- tiling or construction problems.
Strong induction is not logically stronger than ordinary induction; it gives a more convenient hypothesis.
18.5Structural induction#
The induction variable is the way an object was built. First list every permitted constructor; then prove the property survives each constructor. For balanced-parenthesis strings, ε (“epsilon”) is the empty string, wrapping adds ( and ), and concatenation joins two strings without adding a separator.
constructors: EMPTY | WRAP(s) | JOIN(s,t)
EMPTY produces ε, with counts (left,right)=(0,0)
WRAP(EMPTY) produces (), with counts (1,1)
JOIN((),()) produces ()(), with counts (2,2)
WRAP(()()) produces (()()), with counts (3,3)
# Property expressed as code over a constructed string s:
# s.count('(') == s.count(')')
# Proof: EMPTY establishes equality; WRAP adds one to each side;
# JOIN adds equal pairs of counts, keeping the totals equal.
Equal counts alone do not characterize balanced parentheses: )( has equal counts but cannot be built by these rules. The claim is that every generated string has equal counts, not that every equal-count string is generated.
Structural induction applies to recursively defined objects.
For a recursively defined collection:
- prove the property for every base object;
- assume it holds for component objects;
- prove each construction rule preserves it.
Applications include:
- strings;
- logical formulas;
- rooted trees;
- recursively generated expressions.
Example: build strings of parentheses by three rules: the empty string is allowed; if s is allowed, so is (s); if s and t are allowed, so is st. Claim: every such string has as many ( as ). The empty string has 0 of each. Wrapping s adds one of each. Joining s and t adds their equal counts. Every rule preserves the property, so every string the rules can build has it.
It is induction over construction depth rather than an explicit integer variable.
19Counting#
Suppose you have three people, A, B and C. “Choose two people” is incomplete: choosing a chair and a secretary gives six outcomes, but choosing a committee gives three. Counting starts by defining when two outcomes are different, not by looking for numbers to put in a formula.
| Question | Different outcomes? | Small picture |
|---|---|---|
| Do roles or positions matter? | AB and BA differ for ordered roles. | AB, AC, BA, BC, CA, CB |
| Is order irrelevant? | AB and BA name the same committee. | {A,B}, {A,C}, {B,C} |
| Can an object be reused? | AA is allowed only with repetition. | A repeated choice is not another distinct person. |
Notation used throughout this chapter: n usually counts available objects, r counts selected objects, and k often counts categories or a specified number of successes. A subscript labels an item: n2 means “the number for stage 2,” not n squared. Superscripts such as 25 are powers: multiply five copies of 2. Juxtaposition means multiplication, so nr means n times r. A factorial, n!, multiplies the positive integers from 1 through n; by definition, 0! = 1. Vertical bars around a set, |A|, mean its number of elements. Braces { } describe a set; parentheses (A,B) can describe an ordered pair. The snippets below use Python-like pseudocode; “append,” “add,” and “return” show the counting process, not a requirement to run software. In these snippets, = stores a value, == compares two values, != means “not equal,” and += adds to a stored value. len counts a collection’s entries, and sum adds numerical entries. A loop written “1 through n” includes both endpoints; it has no iterations when the upper endpoint is below the lower one. An example computed for one small input illustrates a general rule; it does not by itself prove the rule for every input.
factorial(n):
total = 1
for value in 1 through n:
total = total * value
return total
# 4! = 1 * 2 * 3 * 4 = 24; 0! uses no loop, so returns 1
Counting problems are largely classification problems.
Before choosing a formula, determine:
- Is this a sequence of decisions or a choice among cases?
- Does order matter?
- Is repetition permitted?
- Are objects distinguishable?
- Are there restrictions?
- Can the complement be counted more easily?
19.1Sum rule#
m and n are the sizes of two separate lists. Disjoint means no outcome occurs in both. Add the lengths when choosing exactly one item from either list. You are joining alternatives, not pairing every item in one list with every item in the other.
math = [M1, M2, M3, M4, M5]
art = [A1, A2, A3]
choices = math + art # join the lists
count = len(choices) # 5 + 3 = 8
| First list | Second list | Combined outcomes |
|---|---|---|
| M1 M2 M3 M4 M5 | A1 A2 A3 | Eight distinct choices; no duplicate course. |
If one outcome can occur through disjoint cases containing and possibilities:
A student choosing one elective from 5 math courses or 3 art courses has 5 + 3 = 8 choices, because no course is in both lists.
The cases must not overlap. If they do, use inclusion-exclusion.
Recognition cue: OR between mutually exclusive alternatives.
19.2Product rule#
Here k is the number of stages, and ni is the number of choices at stage i. The dots in n1n2⋯nk mean “continue multiplying through the last stage.” Two shirts and three trousers make six outfits because each shirt has three extensions.
outfits = []
for shirt in [A, B]:
for trousers in [1, 2, 3]:
outfits.append((shirt, trousers))
# A1 A2 A3 B1 B2 B3
shirt A ── trousers 1, 2, 3 ── 3 leaves
shirt B ── trousers 1, 2, 3 ── 3 leaves
6 outfits
If a process has successive stages with:
choices, then the total is:
A plate of three letters followed by three digits allows 26 · 26 · 26 · 10 · 10 · 10 = 17,576,000 plates. Picture a decision tree: every first letter branches into 26 second letters, each of those into 26 third letters, and so on, so the leaves multiply.
Multiply when each partial choice has the stated number of extensions. The available objects may change: choosing two distinct people for ordered roles gives n(n−1). If branches have different numbers of extensions, count each branch and add. The word “and” alone does not justify multiplication.
19.3Bijection principle#
A bijection is a reversible pairing: every object on the left has exactly one partner on the right, with nobody left over. A bit is a 0-or-1 position. For subsets, write 1 when the corresponding item is included and 0 when it is excluded. There are two independent choices per position.
items = [1, 2, 3]
encode(subset): return [1 if x in subset else 0 for x in items]
decode(bits): return {items[i] for i in positions if bits[i] == 1}
# decode(encode(subset)) gives the original subset
| Subset | Bits, in item order 1,2,3 |
|---|---|
| {} | 000 |
| {2} | 010 |
| {1,3} | 101 |
| {1,2,3} | 111 |
If a difficult collection can be paired one-to-one with an easier collection, they have the same size.
Much of combinatorics works by representing an object as:
- a bit string;
- a subset;
- a permutation;
- a tuple;
- a path.
For example, how many subsets does {1, 2, 3, 4, 5} have? Write each subset as five bits, bit i being 1 when i is in the subset: {1, 4} becomes 10010. Every subset gives one bit string and every bit string gives one subset, so there are 25 = 32 subsets, a count the product rule makes easy.
19.4Division rule#
If your list has M entries but each real outcome appears d times, the true count is M/d. The “exactly” matters: division by a common number is justified only when every outcome has that many representations. For two-person committees, the two orders of each pair create a duplication factor of 2! = 2.
| Real committee | Representations in an ordered list |
|---|---|
| {A,B} | AB, BA |
| {A,C} | AC, CA |
| {B,C} | BC, CB |
ordered = [AB, BA, AC, CA, BC, CB]
committee_count = len(ordered) / factorial(2) # 6 / 2 = 3
If a counting procedure counts every actual object exactly times, divide by .
This is why combinations divide permutations by : every unordered -element choice appears in orders.
19.5Permutations#
P(n,r) reads “permutations of n objects taken r at a time.” Here 0 ≤ r ≤ n; objects are distinct and cannot be reused. Filling ordered positions leaves one fewer choice each time. The denominator (n−r)! cancels the unused tail of n!. A fraction bar means division; n!/(n−r)! is not n! divided by (n−r) followed by another factorial.
permutations_count(n, r):
count = 1
for filled in 0 through r-1:
count = count * (n - filled)
return count
# n=4, r=2: first role 4 choices, second role 3; count=12
| Position | Available people after earlier selections | Factor |
|---|---|---|
| Chair | A, B, C, D | 4 |
| Secretary, if A is chair | B, C, D | 3 |
| Total role assignments | AB differs from BA | 4 × 3 = 12 |
When selecting and ordering distinct objects from :
For all objects:
Order matters.
Examples of ordered roles:
- president, vice president, secretary;
- first, second, third place;
- password positions.
Gold, silver and bronze among 8 runners: 8 choices for gold, then 7, then 6, so P(8, 3) = 8 · 7 · 6 = 336. The formula 8!/5! is the same product: the 5! cancels the factors for the runners left unplaced.
19.6Combinations#
The stacked notation n choose r, also written C(n,r) or choose(n,r), counts unordered subsets of size r. It is not the fraction n/r. First count ordered selections, then divide by r!, the number of orders of each selected group. The symbol = says both expressions have the same value.
choose(n, r) = factorial(n) / (factorial(r) * factorial(n-r))
# Choose 2 of A,B,C,D: 4! / (2! * 2!) = 6
committees = [{A,B}, {A,C}, {A,D}, {B,C}, {B,D}, {C,D}]
| Chosen pair | Excluded pair |
|---|---|
| {A,B} | {C,D} |
| {A,C} | {B,D} |
| {A,D} | {B,C} |
Each chosen subset has one complementary excluded subset. That pairing explains C(n,r) = C(n,n−r). The table shows three such pairs; reversing the two columns gives the other three size-two choices.
When selecting objects from and order does not matter:
Symmetry:
Choosing which items are included is equivalent to choosing which are excluded.
19.7The four basic selection models#
Read the following table as four different experiments using the same n available types and r selections. “Repetition” means the same type may be selected again; it does not mean counting the same outcome twice. For a small concrete comparison, choose two letters from {A,B,C}.
| Model | Outcomes for n=3, r=2 | Count |
|---|---|---|
| Ordered, no reuse | AB AC BA BC CA CB | 3 × 2 = 6 |
| Ordered, reuse allowed | AA AB AC BA BB BC CA CB CC | 3² = 9 |
| Unordered, no reuse | AB AC BC | C(3,2) = 3 |
| Unordered, reuse allowed | AA AB AC BB BC CC | C(4,2) = 6 |
for first in [A,B,C]:
for second in [A,B,C]:
if no_reuse and first == second: continue
outcome = (first, second) if ordered else sorted_pair(first, second)
distinct_outcomes.add(outcome)
| Order matters? | Repetition? | Count |
|---|---|---|
| Yes | No | |
| Yes | Yes | |
| No | No | |
| No | Yes |
This table should be automatic. The last row, combinations with repetition, is derived in 19.12 by stars and bars.
Count the same people in two ways. From five people, choose a chair and a secretary: 5×4 = 20 assignments because swapping the roles changes the result. Choose a two-person committee instead: divide by 2! to get 10, since each pair was counted in both orders. The formula follows from what counts as a different outcome.
19.8Repeated identical objects#
Now all n objects are arranged, but some copies are indistinguishable. ni is the number of copies of category i; k is the number of categories. A multinomial coefficient is the many-category version of a binomial coefficient. The counts must add to n, because every position receives exactly one object.
| Temporarily labeled letters | Visible word after labels are erased |
|---|---|
| A₁ A₂ B | AAB |
| A₂ A₁ B | AAB |
| A₁ B A₂ and A₂ B A₁ | ABA |
| B A₁ A₂ and B A₂ A₁ | BAA |
distinct_orders = factorial(3) / (factorial(2) * factorial(1))
# AAB: six labeled orders collapse to three visible words.
# BANANA: counts A=3, N=2, B=1; 6!/(3!*2!*1!) = 60
For total objects where identical categories occur times:
The denominator removes rearrangements among indistinguishable copies. BANANA has 6 letters, so pretend its letters are all different and get 6! = 720 orders. Each real word was counted once for every way to shuffle the three A’s (3!) and the two N’s (2!) among themselves, so there are 720/(3! · 2!) = 60 distinct words.
This is a multinomial coefficient.
More generally:
where:
19.9Circular permutations#
A circle has no distinguished first seat in this model. Rotating everybody together produces the same arrangement, but reversing clockwise order usually does not. Fix one named person as an anchor, then arrange the remaining n−1 people. If seats are numbered, rotations are different and the answer returns to n!.
| Same circle, written with different starting points | One anchored representative |
|---|---|
| ABCD, BCDA, CDAB, DABC | A B C D |
| ADCB, DCBA, CBAD, BADC | A D C B (a different clockwise order) |
anchor = A
for order in permutations([B,C,D]):
circle = [anchor] + order
# 3! = 6. Do not divide by 2 unless reflection is also equivalent.
When only rotation is considered equivalent, arrange distinct objects around a circle:
Fix one object to remove rotational duplication. Equivalently, the n! line-ups each describe some circle, and each circle appears n times, once per rotation, so the division rule gives n!/n = (n − 1)!. Four people around a table can sit in 3! = 6 distinct ways.
If reflections are also considered identical, division by may be appropriate, provided the arrangements do not have special symmetry.
19.10Complementary counting#
The complement is everything in your chosen universe that fails your condition. |desired| means the number of desired outcomes, not an absolute value of a number here. The desired and undesired groups do not overlap and together exhaust the universe. Count a hard event by subtracting its easier opposite.
total_pins = 10**4 # ** means exponentiation
all_distinct = 10 * 9 * 8 * 7
repeated_digit = total_pins - all_distinct # 4960
| All 10,000 four-position PINs | Partition |
|---|---|
| 5,040 PINs | Every digit different, e.g. 0123 |
| 4,960 PINs | Some digit repeats, e.g. 0102 |
Leading 0 is allowed in a PIN. It would not normally be allowed in a four-digit integer; changing the universe changes the count.
Use:
This is particularly effective for phrases such as:
- at least one;
- none;
- not all;
- contains a repeated item;
- violates no restriction.
How many 4-digit PINs repeat some digit? Counting the repeats directly splits into many cases. The complement is one product: PINs with all digits different number 10 · 9 · 8 · 7 = 5,040, so 10,000 − 5,040 = 4,960 PINs repeat a digit.
The same structural idea works in counting and probability.
19.11Stars and bars#
Here n is the total being distributed and k is the number of labeled recipients. Each variable xi is the integer share for recipient i. “Nonnegative” means 0 is allowed. Identical items and no capacity limits are essential. The k−1 bars divide the stars into k possibly empty runs. Of n+k−1 total positions, choose the positions for the bars; the stars fill the rest.
| Stars and bars | Shares (child 1, child 2, child 3) |
|---|---|
| **|*|** | (2,1,2) |
| |*****| | (0,5,0) |
| ***||** | (3,0,2) |
ways = 0
for x1 in 0 through 5:
for x2 in 0 through 5-x1:
x3 = 5 - x1 - x2
ways = ways + 1
# 21 solutions, matching choose(5+3-1, 3-1).
The inequality xi ≥ Li means share i must be at least its lower bound Li. Set yi = xi−Li; then the new variables count only the extra items after the minimum shares have been supplied. Their total is n−(L1+⋯+Lk). If that is negative, there are zero solutions. For positive shares, all lower bounds are 1 and n must be at least k.
Hand out 5 identical cookies to 3 children. Line up the 5 cookies as stars and drop 2 bars among them to mark where one child’s share ends and the next begins: ★★|★|★★ means (2, 1, 2), and |★★★★★| means (0, 5, 0). Every distribution is one arrangement of 5 stars and 2 bars, so the count is the number of ways to choose which 2 of the 7 positions hold bars: C(7, 2) = 21. The same idea in general:
Count nonnegative integer solutions to:
Imagine identical stars separated into categories by bars.
There are:
positions containing stars and bars, so:
solutions.
Equivalent form:
For positive solutions:
give one item to every category first. The remaining total is , giving:
If every child must get at least one cookie, hand out one each first and distribute the remaining 2 freely: C(4, 2) = 6.
For lower bounds:
substitute:
so .
Upper bounds generally require inclusion-exclusion.
19.12Multisets#
A multiset records how many of each type you choose; it ignores selection order but preserves multiplicity. In this section n counts types and r counts chosen items. This reverses the roles of the total and category letters used in the preceding section: the method is the same, but always read each variable’s definition locally.
| Three scoops from flavors V,C,S,M | Count vector in order V,C,S,M | Separator representation |
|---|---|---|
| V,V,C | (2,1,0,0) | **|*|| |
| C,V,V | (2,1,0,0), the same cup | **|*|| |
| M,M,M | (0,0,0,3) | |||*** |
cup = {V:2, C:1, S:0, M:0}
# 3 stars + 3 bars = 6 positions. Choose 3 star positions.
possible_cups = choose(4 + 3 - 1, 3) # 20
Choosing items from categories with repetition allowed and order irrelevant is equivalent to:
with .
Thus:
This is combinations with repetition. A cup of 3 scoops from 4 flavors, with repeats allowed and order irrelevant, is a solution of x1 + x2 + x3 + x4 = 3, where xi counts scoops of flavor i. The flavors are the categories and the scoops the stars: C(6, 3) = 20 cups.
19.13Pigeonhole principle#
The “pigeons” can be people, files or strings; the “holes” are categories. N is the number of objects and k the number of categories, with k positive. The ceiling symbols ⌈ ⌉ mean round up to the next integer: ⌈13/12⌉ = 2. This is a guarantee about some category, not every category.
guaranteed_maximum_at_least = ceil(N / k)
# To avoid r in any one box, each box can hold at most r-1.
maximum_without_r = k * (r-1)
first_forced_total = maximum_without_r + 1
Three boxes; avoid having 3 in one box:
box A: ** box B: ** box C: ** total 6, still possible
next object must join one of those boxes: total 7 forces a group of 3.
If objects are placed into boxes, at least one box contains at least:
objects. If every box held fewer, the total would fall short of N. Among 13 people, some two share a birth month, since ⌈13/12⌉ = 2.
To guarantee at least objects in one box, the maximum avoiding that outcome is:
so the guarantee threshold is:
To guarantee 3 people with the same birth month, 12 · 2 = 24 people can still spread out two per month, so 25 are needed.
The principle proves existence without identifying which box has the concentration.
19.14Inclusion-exclusion#
A ∪ B (“A union B”) contains objects in A or B, including both. A ∩ B (“A intersection B”) contains objects in both. |A| is the count of A. Adding the separate counts counts a shared object twice, so subtract the overlap once. With three sets, objects in all three were added three times then subtracted three times, leaving zero; add the triple overlap once to restore their intended count of one.
| Membership region | Added by |A|+|B|+|C| | Subtracted by pair overlaps | Final after triple term |
|---|---|---|---|
| A only | 1 | 0 | 1 |
| A and B only | 2 | 1 | 1 |
| All three | 3 | 3 | 1 (add it back) |
A = {x for x in 1 through 100 if x % 2 == 0}
B = {x for x in 1 through 100 if x % 5 == 0}
at_least_one = len(A) + len(B) - len(A.intersection(B))
neither = 100 - at_least_one # 40; % means remainder
For two properties:
For three:
To count objects having none of several properties:
and evaluate “at least one” using inclusion-exclusion.
Among 1 to 100, 50 numbers are divisible by 2 and 20 by 5. Adding gives 70, but the 10 multiples of 10 were counted twice, so 50 + 20 − 10 = 60 are divisible by 2 or 5, and 100 − 60 = 40 by neither. With three sets the triple overlap is added back because subtracting the three pairs removed it once too often.
19.15Binomial theorem#
A binomial has two terms, such as x+y. The exponent n is a nonnegative integer number of identical factors. The summation sign ∑ means add one term for every integer k from the lower limit 0 to the upper limit n. In each term, k factors supply y, and the other n−k supply x. A coefficient tells how many selection patterns produce the same powers.
| Picks from three factors | Resulting term | Number of patterns |
|---|---|---|
| xxx | x³ | 1 |
| xxy, xyx, yxx | x²y | 3 |
| xyy, yxy, yyx | xy² | 3 |
| yyy | y³ | 1 |
expanded = 0
for k in 0 through n:
expanded += choose(n,k) * x**(n-k) * y**k
# n=3: x**3 + 3*x**2*y + 3*x*y**2 + y**3
In Pascal’s identity, designate one of the n objects. A size-k subset either includes it (choose k−1 others from n−1) or excludes it (choose all k from n−1). These disjoint cases explain the addition. For endpoint cases, use C(n,0) = C(n,n) = 1; combinations outside the allowed size range count as 0.
Expand (x + y)3 = (x + y)(x + y)(x + y) by picking one letter from each factor. A term x3−kyk arises once for each choice of which k factors supply y, giving coefficients 1, 3, 3, 1. That is why the coefficients are binomial coefficients.
The coefficient of:
is:
Pascal’s identity:
Interpretation: count -element subsets according to whether they contain one designated element.
19.16Bit strings#
A bit string has ordered positions, each holding 0 or 1. In these formulas n is its length, k the requested number of 1s, and i the running count of 1s in a sum. “Exactly k” chooses which positions hold 1. “At most k” adds the disjoint cases 0,1,…,k. “At least one” excludes just the all-zero string. Assume 0 ≤ k ≤ n.
| Three-bit strings by number of 1s | Strings | Count |
|---|---|---|
| 0 | 000 | 1 |
| 1 | 001,010,100 | 3 |
| 2 | 011,101,110 | 3 |
| 3 | 111 | 1 |
total = 2**n
exactly_k = choose(n,k)
at_most_k = sum(choose(n,i) for i in 0 through k)
at_least_one = 2**n - 1
# No adjacent 1s: a[n] counts valid length-n strings.
a[0] = 1 # the empty string
a[1] = 2 # 0, 1
for n in 2 through target:
a[n] = a[n-1] + a[n-2]
Valid length 3, separated by final construction:
append 0 to valid length 2: 00→000, 01→010, 10→100
append 01 to valid length 1: 0→001, 1→101
Total: 3 + 2 = 5. No string appears in both groups.
Number of bit strings of length :
Number containing exactly ones:
Number containing at most ones:
Number containing at least one :
Number with no consecutive ones is often modeled recursively: strings ending in may follow any valid shorter string, while strings ending in must have a preceding . So a valid string of length n is a valid string of length n − 1 followed by 0, or one of length n − 2 followed by 01: an = an−1 + an−2. Starting from 2 strings of length 1 (0, 1) and 3 of length 2 (00, 01, 10), the counts run 2, 3, 5, 8, 13: the Fibonacci recurrence of 17.8.
19.17Block method#
If designated objects must be consecutive, compress them into a temporary single object. Count outer placements, then inner orders. Multiplication applies because every outer placement permits the same inner arrangements. “Together in any order” differs from “in the fixed order ABC.”
outer = factorial(3) # [block], D, E
inner = factorial(3) # A,B,C within the block
answer = outer * inner # 36
# If the block must read ABC, inner = 1 instead.
[ABC] D E ── expand block ── ABCDE, ACBDE, BACDE, BCADE, CABDE, CBADE
D [ABC] E ── another outer arrangement, with the same 6 inner choices
When designated objects must remain together, treat them as one block.
If must appear consecutively among other distinct objects:
- treat as one object;
- arrange that block with the others;
- multiply by the internal arrangements of .
If the internal order is fixed, omit the internal factorial.
With A, B, C kept together among D and E, the block, D and E are 3 objects with 3! = 6 orders, and the block’s inside has 3! = 6 orders: 36 arrangements of the five letters.
19.18Gap method#
Place the objects allowed to touch each other first. They create one gap at each end and one between each neighboring pair, so four placed people create five gaps. Putting at most one restricted person in each selected gap enforces nonadjacency. The people in the example are all distinct; if only category patterns matter, the person-order factorials would not be present.
girls_order = factorial(4)
boy_positions_and_order = choose(5,3) * factorial(3)
total = girls_order * boy_positions_and_order # 24 * 10 * 6 = 1440
gaps: _ G1 _ G2 _ G3 _ G4 _
choose: B G1 _ G2 B G3 _ G4 B
Distinct gaps separate every pair of boys.
To prevent certain objects from being adjacent:
- arrange the unrestricted objects;
- identify gaps around and between them;
- place restricted objects into distinct gaps.
This is useful for “no two identical/category- objects adjacent.”
Seat 4 girls and 3 boys in a row with no two boys adjacent. Seat the girls first: 4! = 24 ways, leaving 5 gaps, _G_G_G_G_. Each boy takes a different gap, so no two boys touch: 5 · 4 · 3 = 60 ways. In all, 24 · 60 = 1,440 seatings.
19.19Lexicographic order#
Think dictionary order using a fixed ordering of the symbols. Compare the leftmost position where two sequences differ; whichever has the smaller symbol comes first. If one sequence is a proper prefix of the other, the shorter comes first. Generating the next arrangement means making the smallest possible increase, then making the rest as small as possible.
| Comparison | First difference | Conclusion |
|---|---|---|
| 13542 versus 14235 | Second position: 3 < 4 | 13542 comes first. |
| 12 versus 123 | No differing symbol before 12 ends | 12 comes first. |
lex_less(a,b):
for i in shared_positions(a,b):
if a[i] != b[i]: return a[i] < b[i]
return len(a) < len(b)
Lexicographic order compares sequences at their first differing position.
Next permutation#
In the steps below, ai is the entry at position i; use positions numbered from 1. “Largest i” means the rightmost suitable position, not the largest entry. The suffix is everything to its right. A decreasing suffix is already at its last possible order, so the next increase must change the entry immediately before it.
next_permutation(a):
i = rightmost position with a[i] < a[i+1]
if no such i: return NONE # already last
j = rightmost position with a[j] > a[i]
swap(a[i], a[j])
reverse(a[i+1 ... end])
return a
| Step | Array | Reason |
|---|---|---|
| Find pivot | 1 [3] 5 4 2 | 3 < 5 is the rightmost rise. |
| Find replacement | 1 [3] 5 [4] 2 | 4 is the smallest larger value in the decreasing suffix. |
| Swap | 1 4 5 3 2 | Prefix increased as little as possible. |
| Reverse suffix | 1 4 2 3 5 | Remaining positions now as small as possible. |
Given an ordering:
- find the largest such that:
- find the largest such that:
- swap ;
- reverse the suffix after position .
If no such exists, the permutation is the last one.
Example: the permutation after 13542. The rightmost rise is 3 < 5, so i points at the 3; everything after it, 542, is decreasing and already in its last order. The rightmost entry larger than 3 is 4, so swap them: 14532. Reversing the suffix 532 gives its smallest order, and the answer is 14235.
Next -combination#
Represent an unordered combination by an increasing list, so every subset has one representation. With positions 1 through r, the largest possible entry at position i is n−r+i: enough larger values must remain for the later positions. Increase the rightmost entry that is below its bound, then pack its successors tightly.
next_combination(a, n, r):
i = rightmost position where a[i] < n-r+i
if no such i: return NONE
a[i] += 1
for j in i+1 through r:
a[j] = a[j-1] + 1
return a
| n=5, r=3 | Position 1 | Position 2 | Position 3 |
|---|---|---|---|
| Bounds n−r+i | 3 | 4 | 5 |
| Current | 1 | 2 | 5 |
| Next | 1 | 3 (increased) | 4 (reset) |
For:
from :
- find the rightmost with:
- increment ;
- reset every later entry to the smallest consecutive values.
Example with n = 5 and r = 3: after {1, 2, 5}, the last entry is already at its maximum 5 = n − r + 3, but a2 = 2 can still grow (its maximum is 4). Increment it to 3 and reset the last entry to 4: the next combination is {1, 3, 4}.
20Discrete probability#
Counting says how many outcomes there are; probability says how much weight they carry. A sample space, named S, is your complete list of elementary outcomes. An event, named E, is a subset of that list. P(E) reads “probability of event E” and is a number from 0 (impossible) through 1 (certain). It is not the permutation count P(n,r) from chapter 19: the argument tells you which meaning is intended. For equally likely outcomes, |E|/|S| means favorable count divided by total count.
| Two fair coin tosses: S = {HH,HT,TH,TT} | Event: exactly one head |
|---|---|
| HH, weight 1/4 | Not in E |
| HT, weight 1/4 | In E |
| TH, weight 1/4 | In E |
| TT, weight 1/4 | Not in E |
weights = {HH:1/4, HT:1/4, TH:1/4, TT:1/4}
E = {HT, TH}
probability = sum(weights[outcome] for outcome in E) # 1/2
Probability assigns numerical weights to outcomes.
- Experiment: process producing an outcome.
- Outcome: one possible result.
- Sample space : set of all outcomes.
- Event: subset of .
For an event E, P(E) means “the probability of E.” For equally likely finite outcomes:
This ratio is invalid when outcomes are not equally likely.
Counting from the previous chapter supplies the numerator and denominator only when the individual outcomes are equally likely. For two fair dice, the 36 ordered pairs are equally likely; the possible sums 2 through 12 are not. Always identify the elementary outcomes before dividing counts.
20.1Probability axioms#
An axiom is a rule the probability model must obey. Every event has nonnegative weight; all of S has weight 1; disjoint events add. In ∪iEi, the indexed union joins all the events in a list. In ∑iP(Ei), the indexed sum adds their probabilities. Pairwise disjoint means no two different events share an outcome. The empty-set symbol ∅ means no outcomes at all; it is different from the set containing the number zero, {0}.
probability(event):
return sum(weight[x] for x in event)
# probability(empty_set) = 0: there are no weights to add.
# probability(sample_space) = 1: every weight is included.
| Event for one fair die | Outcomes | Probability |
|---|---|---|
| Impossible | ∅ | 0 |
| Odd | {1,3,5} | 1/2 |
| Even | {2,4,6} | 1/2 |
| Odd or even | {1,2,3,4,5,6} | 1/2 + 1/2 = 1 |
For pairwise disjoint events Ei, whose union is written ⋃i Ei:
Consequences:
20.2Complement and unions#
The superscript in Ec means the complement of E, not a power: every outcome in S that is not in E. Union A ∪ B means A or B; intersection A ∩ B means both. For non-disjoint events, subtract the shared weight once, exactly as in inclusion-exclusion. “Mutually exclusive” is another name for disjoint.
p_no_six = (5/6)**4
p_at_least_one_six = 1 - p_no_six
p_heart_or_king = 13/52 + 4/52 - 1/52
| Card category | Count | Contribution to heart-or-king |
|---|---|---|
| Hearts other than king | 12 | 12/52 |
| King of hearts | 1 | 1/52, counted only once |
| Other kings | 3 | 3/52 |
| Neither | 36 | 0 |
Complement:
“At least one success” is usually easier as:
The chance of at least one six in four rolls of a die has many success patterns but one failure pattern: no six at all, with probability (5/6)4 = 625/1296. So the answer is 1 − 625/1296 ≈ 0.518.
General addition rule:
If are mutually exclusive:
and:
Draw one card from a standard deck. It is a heart with probability 13/52 and a king with probability 4/52, but the king of hearts is counted in both, so P(heart or king) = 13/52 + 4/52 − 1/52 = 16/52. This is inclusion-exclusion (19.14) divided by the size of the sample space.
20.3Conditional probability#
The vertical bar in P(A | B) reads “given B.” It restricts attention to outcomes where B happened; it is not division or the union symbol. A ∩ B supplies the weight where both happen. Dividing by P(B) rescales the restricted population to a total weight of 1. P(B) must be positive; conditioning on an impossible event is not defined by this ratio. In the chain formula, Ai names event number i, and ∩ means every listed event has happened.
| 100 devices | Patched (A) | Not patched | Total |
|---|---|---|---|
| Laptops (B) | 12 | 18 | 30 |
| Other devices | 28 | 42 | 70 |
| Total | 40 | 60 | 100 |
p_A_and_B = 12/100
p_B = 30/100
p_A_given_B = p_A_and_B / p_B # 12/30 = 0.4
p_both = p_B * p_A_given_B # 0.30 * 0.40 = 0.12
all devices ── laptop: 0.30 ── patched given laptop: 0.40
joint path weight: 0.30 * 0.40 = 0.12
Suppose 30 of 100 devices are laptops, and 12 of those laptops are patched. Given that a chosen device is a laptop, only those 30 devices remain possible: the patched fraction is 12/30. The original population size 100 is no longer the denominator.
provided .
The condition becomes the new restricted sample space.
Rearranging gives the multiplication rule:
Also:
For a sequence of events:
On a probability tree:
- multiply along a path;
- add probabilities of disjoint successful paths.
20.4Independence#
Independence says the conditional weight equals the original weight: learning B does not change P(A). That is a property to establish from the experiment or given probabilities, not a shortcut you may assume because the events have different names. Multiply unconditional probabilities only after establishing independence; otherwise multiply a conditional probability by its conditioning event’s probability.
| Two fair dice | Second die = 6 | Second die ≠ 6 |
|---|---|---|
| First die = 6 | 1/36 | 5/36 |
| First die ≠ 6 | 5/36 | 25/36 |
p_first6 = 1/6
p_second6 = 1/6
p_both6 = p_first6 * p_second6 # independent dice
# Without replacement, use the changed second probability.
p_two_aces = (4/52) * (3/51)
Events are independent when:
Equivalent, when denominators are nonzero:
and:
Learning one event does not change the probability of the other.
Roll two dice. The first shows 6 with probability 1/6, and the second shows 6 with probability 1/6 whatever the first did, so both show 6 with probability 1/6 · 1/6 = 1/36, which matches 1 of the 36 equally likely pairs. Draw two cards without putting the first back, and the second draw depends on the first: both are aces with probability 4/52 · 3/51, not (4/52)2.
Independence versus mutual exclusivity#
These answer different questions: can they both occur? versus does learning one change the chance of the other? If A and B are disjoint and both have positive probability, learning A makes B impossible, which changes its probability. They are therefore dependent.
| One die | A = roll 1 | B = roll 2 |
|---|---|---|
| P(A), P(B) | 1/6 | 1/6 |
| Both A and B | Impossible | P(A ∩ B) = 0 |
| If independent | Product would be 1/36 | But 0 ≠ 1/36 |
| After learning A | A happened | P(B | A) = 0 |
mutually_exclusive = (p_both == 0)
independent = (p_both == p_A * p_B)
# Different conditions; neither is a substitute for the other.
Mutually exclusive means:
If both events have positive probability, mutually exclusive events cannot be independent:
but:
Sampling with replacement is often independent. Sampling without replacement is usually dependent because probabilities change.
A single die shows why disjoint events are dependent. “Roll a 1” and “roll a 2” cannot both happen, so learning that the roll was a 1 drops the probability of a 2 from 1/6 to 0. That is the opposite of independence, where learning one event changes nothing.
20.5Law of total probability#
A partition breaks S into non-overlapping cases A1 through Ak that cover all possibilities. Each term P(B | Ai)P(Ai) is one route to B: weight of a case times B’s conditional chance inside it. ∑i=1k adds the k routes. Cases with zero weight contribute zero and can be omitted rather than conditioning on them.
p_flagged = 0
for case in [infected, clean]:
p_flagged += p_case[case] * p_flag_given_case[case]
# 0.01*0.90 + 0.99*0.10 = 0.108
1,000 devices
├─ infected: 10 ── flagged: 9 (0.01 * 0.90 of all devices)
└─ clean: 990 ── flagged: 99 (0.99 * 0.10 of all devices)
total flagged: 108
If:
partition the sample space, then:
This adds every mutually exclusive route by which can occur.
The device numbers in 20.6 show it at work. A device is infected with probability 0.01 and clean with probability 0.99; the scanner flags 90% of infected devices and 10% of clean ones. Then P(flagged) = 0.9 · 0.01 + 0.1 · 0.99 = 0.009 + 0.099 = 0.108: one route through each kind of device, which is 108 of every 1,000 devices.
20.6Bayes’ theorem#
Bayes’ theorem asks which original case is likely after observing evidence. Ai is the particular case you want; B is the evidence. The prior P(Ai) is the case’s probability before that evidence. The likelihood P(B | Ai) asks how often that case would produce the evidence. The posterior P(Ai | B) is the updated probability after seeing it. The denominator adds over all possible cases Aj, not just the chosen i; j is a running index. Cases form a partition, and the evidence must have positive probability.
| Actual status among 1,000 devices | Flagged B | Not flagged | Total |
|---|---|---|---|
| Infected A | 9 | 1 | 10 |
| Clean Ac | 99 | 891 | 990 |
| Total | 108 | 892 | 1,000 |
infected_and_flagged = 0.01 * 0.90
clean_and_flagged = 0.99 * 0.10
evidence = infected_and_flagged + clean_and_flagged
posterior = infected_and_flagged / evidence # 0.009/0.108 = 1/12
Read the table in opposite directions to see the distinction: 9/10 follows the infected row and asks for a flag; 9/108 follows the flagged column and asks for infection. Reversing the conditional changes its denominator.
Conceptually:
Bayes reverses a conditional.
Given:
- base rates ;
- likelihoods ;
- observed evidence ;
it finds:
The denominator is calculated using total probability.
Base rates matter. A highly accurate test can still produce many false positives when the condition tested is rare.
Bayes as a count. Suppose 10 of 1,000 devices are infected. A scanner flags 9 of those 10 and also flags 99 of the 990 clean devices. Among the 108 flagged devices, only 9 are infected, so P(infected | flagged) = 9/108 ≈ 8.3%. This is not the 90% detection rate: that rate answers P(flagged | infected), the reverse question. The total probability denominator counts all ways to get flagged.
Bayesian networks: factoring a joint probability
A Bayesian network combines the conditional-probability ideas into a graph. Each node is a random variable, a named rule that assigns a value to each experimental outcome (formalized in chapter 21); an arrow identifies a parent whose value appears in the child’s probability table. The graph is directed (arrows have a direction) and acyclic (following arrows can never return to the starting node). A table lists a node’s probability for each combination of its parents’ values. A node without parents has an ordinary prior table. The graph describes probability dependence; arrows alone do not establish causation.
Consider an illustrative device model. I means infection status, F means scanner flag status, and D means slowdown status; each variable is either true or false. Draw I → F and I → D. This model assumes that once infection status is known, the flag and slowdown are conditionally independent: learning one gives no additional information about the other. That assumption is stronger than merely drawing a plausible picture, and it must be justified for a real application.
I (infected?)
/ \
v v
F (flag?) D (slow?)
P(I=i, F=f, D=d) = P(I=i) * P(F=f | I=i) * P(D=d | I=i)
In the last line, lowercase i, f, and d are particular true/false values. Commas in P(I=i,F=f,D=d) mean all three conditions occur together. Multiply one local table entry per node, using the parent values of the same assignment. This factorization stores the joint model through smaller conditional tables.
| I value | P(I) | P(F=true | I) | P(D=true | I) |
|---|---|---|---|
| true | 0.01 | 0.90 | 0.80 |
| false | 0.99 | 0.10 | 0.20 |
joint(i,f,d):
p_i = 0.01 if i else 0.99
p_flag = 0.90 if i else 0.10
p_slow = 0.80 if i else 0.20
return p_i * (p_flag if f else 1-p_flag) * (p_slow if d else 1-p_slow)
# infected, flagged, slow: 0.01 * 0.90 * 0.80 = 0.0072
# All flagged-and-slow cases add the two infection-status routes:
p_flag_and_slow = joint(True,True,True) + joint(False,True,True)
# 0.0072 + 0.0198 = 0.027
p_infected_given_both = joint(True,True,True) / p_flag_and_slow
# 0.0072 / 0.027 is about 0.267
Adding over an unobserved variable is called marginalization: it is the law of total probability in tabular form. Dividing a joint evidence-and-case probability by the total evidence probability is Bayes’ theorem. A network gives an organized way to repeat these same operations; it does not permit assuming arbitrary events are independent.
21Random variables#
A random variable is a rule attached to outcomes, not a new random event. In X:S → ℝ, X names the rule, S is the sample space, the arrow means “maps into,” and ℝ is the set of real numbers. Uppercase X is the rule; lowercase x is one numerical value it may take. X = x describes the event consisting of all outcomes mapped to that value. The subscript X in pX names which variable’s distribution is being described; pX(x) is that value’s probability mass.
| Outcome | X = number of heads | Outcome probability |
|---|---|---|
| HH | 2 | 1/4 |
| HT | 1 | 1/4 |
| TH | 1 | 1/4 |
| TT | 0 | 1/4 |
mass = {0:0, 1:0, 2:0}
for outcome in [HH, HT, TH, TT]:
x = count_heads(outcome)
mass[x] += 1/4
# mass = {0:1/4, 1:1/2, 2:1/4}; the masses sum to 1.
A probability mass function (PMF) is this value-to-probability table. The sum ∑x adds over its possible values, not over each original outcome a second time.
Toss two fair coins. The outcomes HH, HT, TH, TT each have probability 1/4. Let X count heads: its values are 2, 1, 1, 0. Thus P(X=1) = 1/2 because two different outcomes give that value. A random variable converts outcomes to numbers; its distribution groups and adds their probabilities.
A random variable is a function:
assigning a number to every outcome.
For a discrete random variable, the probability mass function is:
Requirements:
and:
21.1Expected value#
E[X] reads “expected value of X,” often called the mean and named μ (“mu”). For each possible value x, multiply x by its probability P(X = x), then add. The repeated x in ∑x xP(X=x) is not x squared: the subscript says which values to loop over, while the factor x supplies the numerical payoff.
| Two-coin head count x | P(X=x) | Weighted contribution xP(X=x) |
|---|---|---|
| 0 | 1/4 | 0 |
| 1 | 1/2 | 1/2 |
| 2 | 1/4 | 1/2 |
| Sum | 1 | E[X] = 1 |
In the code, mass.items() supplies each stored (value, probability) pair once; the loop gives those two parts the names x and probability. The final sum weights the values, not their positions in the table.
mean = 0
for x, probability in mass.items():
mean += x * probability
# For a transformation g, change the value, not its weight.
expected_transformed = sum(g(x) * prob for x, prob in mass.items())
In E[g(X)], g is a function applied to the observed number, such as squaring it or converting gross prize to net profit. In aX+b, a and b are fixed constants: multiply every observed value by a, then add b. X and Y in E[X+Y] are two random variables from the same experiment; they may be dependent. Expected values add whenever the expectations exist. A game’s fair expected net payoff is zero, not its ticket price.
Expected value is a long-run probability-weighted average. It need not be a possible individual outcome. A fair die has E[X] = (1 + 2 + 3 + 4 + 5 + 6)/6 = 3.5, a value no single roll shows, yet the average of many rolls settles near it. Each value is weighted by how often it occurs.
For transformed values:
Linearity:
The second identity does not require independence.
For games, use net payoff:
and represent losses as negative.
A fair game has expected net value .
A game costs $2 and pays $10 when a die shows 6. The net payoff is +8 with probability 1/6 and −2 with probability 5/6, so the expected net value is 8/6 − 10/6 = −1/3: about 33 cents lost per play in the long run, so the game is not fair.
21.2Variance and standard deviation#
Variance describes spread around the mean μ = E[X]. The difference X−μ is a deviation. Its square is always nonnegative; taking the expectation averages these squared distances using the same probability weights. E[X²] means “average the squared values”; (E[X])² means “square the average.” They are different operations. The square-root symbol √ reverses squaring for nonnegative values. Standard deviation σX (“sigma sub X”) is measured in X’s units; variance is measured in squared units.
| x | Probability | x² × probability | (x−1)² × probability |
|---|---|---|---|
| 0 | 1/4 | 0 | 1/4 |
| 1 | 1/2 | 1/2 | 0 |
| 2 | 1/4 | 1 | 1/4 |
| Total | E[X²] = 1.5 | Var(X) = 0.5 |
mean = sum(x * p for x,p in mass.items())
variance = sum((x-mean)**2 * p for x,p in mass.items())
# Equivalent: sum(x*x*p for x,p in mass.items()) - mean**2
standard_deviation = sqrt(variance) # sqrt(0.5), about 0.707 heads
Adding a constant b slides every value and the mean equally, so their distances do not change. Multiplying by a multiplies squared distances by a², even when a is negative. Independent variables have additive variances. Without independence or another justification, variables may move together and the variance of their sum need not equal the sum of variances. The additivity of means in 21.1 has no such independence requirement.
Variance measures squared deviation from the mean:
Why square? The plain deviations X − E[X] average to exactly 0, because values above the mean balance those below. Squaring makes every deviation count as positive and weights large deviations more. Standard deviation then takes the square root to return to the original units.
Computational form:
Standard deviation:
Scaling properties:
Adding a constant changes the mean but not variance.
For independent :
For the number of heads in two fair coin tosses (chapter 21’s opening example), X is 0, 1 or 2 with probabilities 1/4, 1/2, 1/4. Then E[X] = 1 and E[X2] = 0 + 1/2 + 4/4 = 1.5, so Var(X) = 1.5 − 12 = 0.5.
22Common discrete distributions#
Before choosing a distribution, name the random variable and explain its experiment in one sentence. In the next sections, p is a constant success probability, 1−p is the failure probability, and k is a requested value of the count. A “success” is simply whichever outcome you are counting, even if it is a defective component.
| Model | What X counts | Experiment structure | Example |
|---|---|---|---|
| Bernoulli | 0 or 1 success | One trial | Did one coin show heads? |
| Binomial | Number of successes | Fixed n independent identical trials | Heads in ten tosses |
| Geometric | Trials up to and including first success | Independent trials, stop at first success | Tosses until first head |
| Hypergeometric | Successes in a sample | Uniform sample without replacement | Red marbles in a handful |
if one_trial: model = "Bernoulli"
elif fixed_n and independent and constant_p: model = "Binomial"
elif independent and constant_p and stop_at_first_success: model = "Geometric"
elif uniform_without_replacement: model = "Hypergeometric"
else: model = "describe the actual experiment; do not force a formula"
A distribution is a reusable model for a random variable. Choose it from the experiment, not from the formula’s appearance: one success/failure trial, a fixed number of trials, trials until the first success, or a sample without replacement.
22.1Bernoulli#
A Bernoulli variable is an indicator: use 1 for success and 0 for failure. The brace in its piecewise formula means “choose the row whose condition applies.” Thus P(X=0)=1−p and P(X=1)=p. The weighted mean is 0(1−p)+1p=p; because 0²=0 and 1²=1, E[X²]=p also, giving variance p−p²=p(1−p).
| One fair toss | X | Probability | Mean contribution |
|---|---|---|---|
| Tails | 0 | 1/2 | 0 |
| Heads | 1 | 1/2 | 1/2 |
X = 1 if outcome_is_success else 0
pmf = {0:1-p, 1:p}
mean = p
variance = p * (1-p)
A Bernoulli random variable represents one trial:
with:
Mean:
Variance:
22.2Binomial#
Here n is the fixed number of trials, k is the desired number of successes (0 through n), and p is the same success chance on every independent trial. The three factors C(n,k), pk, and (1−p)n−k mean “number of success-position patterns,” “weight of their successes,” and “weight of their failures.” Do not multiply by k! as well: order has already been captured by selecting the positions.
| Three fair tosses, exactly two heads | Success positions | Probability |
|---|---|---|
| HHT | 1 and 2 | (1/2)²(1/2) = 1/8 |
| HTH | 1 and 3 | 1/8 |
| THH | 2 and 3 | 1/8 |
| Total | C(3,2) = 3 patterns | 3/8 |
binomial_pmf(n,k,p):
if k < 0 or k > n: return 0
return choose(n,k) * p**k * (1-p)**(n-k)
# Any success at all excludes only the zero-success pattern.
p_at_least_one = 1 - (1-p)**n
mean = n*p
variance = n*p*(1-p)
A binomial model requires:
- a fixed number of trials;
- two outcomes per trial;
- independent trials;
- constant success probability .
If counts successes:
Each part of the formula has a job. One particular pattern of k successes and n − k failures, such as SSF, has probability pk(1 − p)n−k by independence, and C(n, k) counts the patterns, one for each choice of which trials succeed. Exactly two heads in three fair tosses: 3 patterns (HHT, HTH, THH), each with probability 1/8, so 3/8.
The mean and variance follow from 21.1 and 21.2: a binomial count is the sum of n independent Bernoulli trials, each with mean p and variance p(1 − p). Two fair tosses give mean 1 and variance 0.5, as computed in 21.2.
Mean:
Variance:
“At least one” is:
22.3Geometric#
This section counts the successful trial too, so X starts at 1. To have X=k, the first k−1 trials must fail and trial k must succeed. There is exactly one such success/failure pattern; no binomial coefficient is needed. Assume independent trials with the same p, where 0 < p ≤ 1. Some books count failures before the first success instead; that variable starts at 0 and has a different formula.
| Requested trial k | Required pattern | Probability |
|---|---|---|
| 1 | S | p |
| 2 | FS | (1−p)p |
| 3 | FFS | (1−p)²p |
# Integer k; 0 < p <= 1. This convention includes the successful trial.
geometric_pmf(k,p):
if k < 1: return 0
return (1-p)**(k-1) * p
k = 3
p = 1/6
required_pattern = [FAILURE] * (k-1) + [SUCCESS] # repeat F twice, append S: F,F,S
probability = geometric_pmf(k,p) # (5/6)**2 * (1/6) = 25/216
For the memoryless formula, s and t are nonnegative integer trial counts. X>s says the first s trials all failed; X>s+t says another t failures followed them. Conditioning divides (1−p)s+t by (1−p)s, leaving (1−p)t, as long as the conditioning event has positive probability. Past failures do not make success “due.”
If counts the number of trials until the first success:
for .
Mean:
The geometric distribution is memoryless:
Previous failures do not alter the success probability of future independent trials.
Roll a die until the first 6. Needing exactly three rolls means two failures and then a success: (5/6)2(1/6) = 25/216 ≈ 0.116. On average it takes 1/p = 6 rolls. After ten rolls without a 6, the chance that the next roll is a 6 is still 1/6; the die has no memory of the drought.
22.4Hypergeometric#
Here the capital N is the population size, capital K is the number of success-labeled objects in it, lowercase n is the sample size, and lowercase k is the successes you want in that sample. All four counts are integers, with 0 ≤ K ≤ N and 0 ≤ n ≤ N. All size-n subsets must be equally likely. The numerator chooses k objects from the K successes and the remaining n−k from the N−K failures. The denominator counts all samples of size n.
| Bag: N=10, K=5 red; sample n=3 | Choices | Count |
|---|---|---|
| Select k=2 reds from 5 | C(5,2) | 10 |
| Select n−k=1 blue from 5 | C(5,1) | 5 |
| Favorable handfuls | 10 × 5 | 50 |
| All handfuls | C(10,3) | 120 |
| Probability | 50/120 | 5/12 |
hypergeometric_pmf(N,K,n,k):
if k < max(0, n-(N-K)) or k > min(n,K): return 0
return choose(K,k) * choose(N-K,n-k) / choose(N,n)
In the snippet, max(a,b) selects the larger of two lower-bound candidates, while min(a,b) selects the smaller of two upper-bound candidates. The bounds prevent impossible requests: you cannot select more successes than exist or fit in the sample, and you must select enough successes if there are too few failures to fill it. For example, n−(N−K) is the minimum forced successes when the sample is larger than the available failure population. Assume 0 ≤ K ≤ N and 0 ≤ n ≤ N.
Use the hypergeometric distribution for sampling without replacement.
Suppose:
- population size ;
- successful objects;
- sample size ;
- counts sampled successes.
Then:
This differs from binomial because the trials are dependent and the success probability changes. Draw 3 marbles from a bag of 5 red and 5 blue. Exactly 2 red has probability C(5, 2) · C(5, 1)/C(10, 3) = 10 · 5/120 = 5/12: choose which 2 of the reds and which 1 of the blues, out of all 3-marble handfuls. With replacement, every draw would be red with probability 1/2, and the binomial answer would be 3/8 instead.
23Models of computation#
We now change the question from “how many strings?” to “which strings should a machine accept?” A model of computation specifies the machine’s memory and legal operations. A finite-state machine remembers one state from a fixed finite set. It can read an input of any finite length, but its memory does not grow with that length. A language describes the yes-instances, not necessarily a human spoken language.
|
23.1Alphabets, strings, and languages#
The Greek letter Σ (“sigma”) names an alphabet, a finite set of allowed symbols. A string is an ordered finite sequence of those symbols. ε (“epsilon”) is the string with no symbols, represented as
A language may be finite or infinite, even though its alphabet is finite and each individual string is finite. The star and plus are operations on sets of strings, not arithmetic powers. A finite automaton will give a memory-limited implementation of a membership rule such as this one. |
|||||||||||||||
| For: | |||||||||||||||
| examples of strings include: | |||||||||||||||
| The empty string is: | |||||||||||||||
| with length: | |||||||||||||||
| String concatenation joins strings in order: xy is x followed by y. | |||||||||||||||
| is the set of all finite strings over , including . | |||||||||||||||
| contains all nonempty finite strings. | |||||||||||||||
| A language is any set of strings: | |||||||||||||||
|
For example, the strings over {0, 1} with an even number of 1s form a language. It contains ε, 0, 11 and 101 but not 1 or 100. Chapter 24 builds a two-state machine that decides membership in exactly this language. |
24Deterministic finite automata#
A deterministic finite automaton (DFA) reads one symbol at a time with no choice about its next state. A state summarizes the relevant past; it is a name, not an input character. A five-tuple is an ordered list of five parts. Q is the set of states; Σ is the input alphabet; δ (“delta”) is the next-state rule; q0 is the particular starting state; F is the set of accepting states. F ⊆ Q means every accepting state must be one of the states in Q.
| Formal part | Even-number-of-1s example | Role |
|---|---|---|
| Q | {even, odd} | Two possible memory summaries |
| Σ | {0,1} | Allowed input symbols |
| δ | δ(even,1)=odd; δ(odd,1)=even; 0 leaves either unchanged | Update the summary |
| q0 | even | Zero symbols read means zero 1s. |
| F | {even} | Accept exactly when the final parity is even. |
In δ:Q × Σ → Q, the multiplication-shaped × forms a Cartesian product: all ordered (state, input-symbol) pairs. The arrow says the function returns one state in Q for each pair. It does not multiply a state by a symbol. For this example, δ(even,1)=odd is a lookup, just like next_state[("even","1")].
next_state = {
("even","0"): "even", ("even","1"): "odd",
("odd", "0"): "odd", ("odd", "1"): "even"
}
state = "even"
for bit in input_string:
if bit not in {"0","1"}: return REJECT
state = next_state[(state, bit)]
return ACCEPT if state == "even" else REJECT
A DFA answers one question: is this string in the language? Imagine a light that remembers whether the number of 1s read so far is even or odd. Two states suffice because the exact count is irrelevant; only its parity affects the answer.
A DFA is a five-tuple:
where:
- : finite state set;
- : input alphabet;
- : transition function;
- : start state;
- : accepting states.
For every state and every input symbol, a DFA has exactly one next state.
24.1State diagrams#
A diagram is a picture of the same lookup rule. An arrow label is an input; the circles are the memory states. A loop returns to the same state. In the text picture below, double parentheses stand for a double accepting circle, and the free start arrow does not consume input.
1
start ──► ((even)) ───────► (odd)
▲ │
└────── 1 ──────┘
0 loops here 0 loops here
To translate an arrow into code: the arrow from even to odd labeled 1 becomes next_state[("even","1")] = "odd". The two 0 loops become assignments whose destination equals their source. A DFA needs exactly one outgoing transition for each symbol at each state, even if some are loops.
- circles represent states;
- a free incoming arrow marks the start state;
- double circles mark accepting states;
- labeled arrows represent transitions.
The state is the machine’s entire memory.
24.2Tracing a DFA#
A trace writes down every memory update. The first row is the start state before any input; later rows record the state after consuming that row’s symbol. The expression q0 ∈ F reads “the start state is an element of the accepting set.” For empty input, that first row is also the final row.
| Symbols consumed | Next symbol read | State after reading | Accept if input ended here? |
|---|---|---|---|
| ε | None | even (start) | Yes |
| 1 | 1 | odd | No |
| 10 | 0 | odd | No |
| 101 | 1 | even | Yes |
trace = [start_state]
state = start_state
for symbol in input_string:
state = delta(state, symbol)
trace.append(state)
accepted = (state in accepting_states) # only after the loop
The row for prefix 10 explains why visiting even at the beginning did not guarantee acceptance: later input changed the state. The final row, not whether an accepting circle was ever visited, decides membership.
- begin at ;
- read the input from left to right;
- follow one transition per symbol;
- after consuming the entire string, inspect the final state;
- accept exactly when that state lies in .
Entering an accepting state before the input ends does not guarantee acceptance. Remaining symbols may lead elsewhere.
The empty string is accepted precisely when:
because no transitions are taken.
For the even-number-of-1s machine, start in the accepting even state. Reading 101 gives even → odd → odd → even, so it accepts. Reading 10 ends odd and rejects. The labels describe what the machine remembers, not which character it just read.
24.3Transition tables#
A transition table is another drawing of δ: the row tells you the current state, the column the input symbol, and the cell the next state. The table below is a different example from the parity machine. Let q0 mean “empty input or most recent symbol was 0,” and q1 mean “most recent symbol was 1.” Start at q0 and accept only q1. It recognizes strings ending in 1, not strings with an even number of 1s. State names alone do not specify what a machine remembers.
state = "q0"
for symbol in "101":
state = table[state][symbol]
# q0 --1--> q1 --0--> q0 --1--> q1: accepted (ends in 1).
The following table encodes this “ends in 1” machine:
| State | 0 | 1 |
|---|---|---|
Every row-symbol combination must contain exactly one destination in a DFA.
24.4State design patterns#
Design states by asking what information about the processed prefix can affect future acceptance. Two prefixes may share a state only if every possible remaining suffix will give them the same acceptance answer. “I have seen 7 characters” is usually too much history; “the number seen is odd” may be exactly enough. Each pattern below is one way to compress a potentially long history into finitely many possibilities.
| Task | Remember | Number of possibilities |
|---|---|---|
| Parity of 1 count | Even or odd | 2 |
| Count modulo k | Remainder 0 through k−1 | k |
| Has a 1 occurred? | No or yes | 2 |
| Ends in 01 | Longest useful suffix: empty, 0, or 01 | 3 |
# A state is a summary, updated without rereading the whole prefix.
summary = update(summary, next_symbol)
A state should represent exactly the finite information needed about the input read so far.
Even or odd parity#
Parity means evenness or oddness. A 0 changes no count of 1s; a 1 increases it by one and flips its parity. Store a Boolean (“true or false”) or one of two labels, rather than storing the exact unbounded count.
odd = False
for bit in input_string:
if bit == "1": odd = not odd
accept = not odd
| Read | ε | 1 | 10 | 101 | 1011 |
|---|---|---|---|---|---|
| Parity | even | odd | odd | even | odd |
Use two states:
- even number of s seen;
- odd number of s seen.
Reading switches states. Reading leaves the state unchanged.
Count modulo #
A count modulo k remembers only the remainder after division by the fixed positive integer k. There are k possible remainders, 0 through k−1. To recognize a multiple of three occurrences of a symbol, use three states, and advance around the cycle whenever that symbol appears. This is counting occurrences, distinct from interpreting the whole input as a binary number in 24.5.
remainder = 0
for symbol in input_string:
if symbol == "1": remainder = (remainder + 1) % 3
accept = (remainder == 0)
count of 1s: 0 1 2 3 4 5 6
remainder state: 0 → 1 → 2 → 0 → 1 → 2 → 0
accept accept accept
Reading 0 leaves the current state unchanged.
Use states representing residues:
Each new symbol updates the residue.
Has a symbol appeared?#
Once the desired symbol has appeared, later symbols cannot undo that fact. This is a two-state summary that only moves one way: “not yet” can become “seen,” and “seen” stays “seen.” If the task is to accept strings containing a 1, the seen state is accepting.
seen = False
for symbol in input_string:
seen = seen or (symbol == "1")
accept = seen
not-yet ──1──► seen
▲ ▲
└─0 loop └─0 or 1 loop
Trace 0010: not-yet → not-yet → not-yet → seen → seen
Use:
- “not yet seen”;
- “already seen.”
Ends with a pattern#
A prefix is a beginning segment; a suffix is an ending segment. To test whether the entire string ends in 01, keep the longest suffix that could still be the beginning of that pattern. State s represents the empty useful suffix; a represents 0; f represents 01. A previous full match can be destroyed by later input, because “ends in” is different from “has ever contained.”
table = {s:{"0":a,"1":s}, a:{"0":a,"1":f}, f:{"0":a,"1":s}}
state = s
for bit in input_string: state = table[state][bit]
accept = (state == f)
| Prefix processed | Useful suffix | State |
|---|---|---|
| ε | ε | s |
| 1 | ε | s |
| 10 | 0 | a |
| 101 | 01 | f (accept if end) |
| 1011 | ε | s (not accepting) |
States represent the longest suffix of the input seen so far that is also a prefix of the target pattern.
For strings ending in 01, three states suffice: s (no progress), a (the input ends in 0) and f (the input ends in 01, accepting). A 0 always means “the pattern may be starting”; a 1 completes the pattern only right after a 0:
state on 0 on 1
s a s
a a f
f a s f is accepting
From f, reading 1 gives an input ending in 11, which has made no progress toward 01, so the machine returns to s, not to a.
Trap state#
A nonaccepting trap records an irreversible violation. For a language allowing only strings of 0s, seeing a 1 should reject regardless of what follows. Give that violation its own state and send every later symbol back to it. This also fills transitions that a DFA is not allowed to leave missing.
if state == trap:
next_state = trap
elif symbol == "1":
next_state = trap
else:
next_state = okay
start ──► ((okay)) ──1──► (trap)
0 loop 0,1 loop
Input 00100 enters trap on its 1 and can never escape.
A trap or sink state is a nonaccepting state whose transitions all return to itself.
Once entered, acceptance is impossible.
24.5Binary numbers modulo #
Here r is the remainder of the binary number read so far, b is the next bit (0 or 1), and k is the fixed positive divisor. The congruence symbol ≡ means “has the same remainder”; “mod k” identifies the divisor. In code, (2*r+b) % k returns the representative remainder from 0 through k−1. Multiplying by 2 shifts a binary value one position left; adding b fills the new last position.
| Prefix | Decimal value | New bit | Remainder modulo 3 |
|---|---|---|---|
| ε | 0 (initial accumulator) | None | 0 |
| 1 | 1 | 1 | (2×0+1) mod 3 = 1 |
| 11 | 3 | 1 | (2×1+1) mod 3 = 0 |
| 110 | 6 | 0 | (2×0+0) mod 3 = 0 |
remainder = 0
for bit in input_string:
b = integer_value(bit)
remainder = (2*remainder + b) % k
accept = (remainder == 0)
With this start/accept convention, ε is accepted as the zero-length accumulator value, and leading zeros are allowed. If the specification requires a nonempty numeral or forbids leading zeros, add states that enforce those syntax rules; the remainder update alone does not enforce them.
If a state represents the remainder of a binary prefix modulo , appending bit produces:
This is a common way to construct automata recognizing binary numbers divisible by .
The rule holds because appending a bit to a binary number doubles it and adds the bit: 11 followed by 0 is 110, and 3 · 2 + 0 = 6. Remainders follow along, since congruence respects addition and multiplication (14.3). For divisibility by 3, read 110: the remainder goes 0 → 1 → (2 · 1 + 1) mod 3 = 0 → (2 · 0 + 0) mod 3 = 0. The final remainder is 0, so 6 is accepted.
25Nondeterministic finite automata#
A nondeterministic finite automaton (NFA) may branch on the same input symbol. This is a mathematical description of alternatives, not randomness: no probabilities are assigned. A real simulation keeps all possible current states. An absent transition kills that path, not the whole computation. In the code, bool(a_set) is true exactly when the set is nonempty; testing the intersection with F therefore asks whether at least one accepting state remains. δ still names the transition rule, but its result is now a set of states. The symbol 𝒫(Q), “power set of Q,” means the set of all subsets of Q, including ∅ and Q itself. Thus δ(s,0) = {s,a} means both destinations remain possible.
| NFA recognizing strings ending in 01 | On 0 | On 1 |
|---|---|---|
| s (start) | {s,a} | {s} |
| a | ∅ | {f} |
| f (accepting) | ∅ | ∅ |
current = {s}
for symbol in input_string:
next_set = empty_set
for state in current:
next_set = next_set.union(delta(state,symbol))
current = next_set
accept = bool(current.intersection({f}))
Input: ε 1 11 110 1101
Possible: {s} → {s} → {s} → {s,a} → {s,f}
On the final 1: s stays s; a can reach f. At least one full path accepts.
For an epsilon-NFA, Σ ∪ {ε} means the ordinary alphabet symbols plus the special no-input transition label ε. Epsilon is not an extra character present in the input string. The next section explains how the simulation accounts for those free moves.
Keep the same string and acceptance question, but allow several possible next states. Track all possibilities as a set, not as a guess about which branch is right. Subset construction turns that set into one ordinary DFA state.
An NFA may have:
- no transition for a state-symbol pair;
- one transition;
- several transitions;
- -transitions that consume no input.
Its transition function returns a set of states:
or, with epsilon transitions:
An NFA accepts a string when at least one possible path:
- consumes the full input;
- ends in an accepting state.
One failed branch does not cause rejection if another branch succeeds.
An NFA for strings ending in 01 needs no bookkeeping. State s loops on both symbols, a 0 may also go from s to a, and a 1 goes from a to the accepting state f. In effect it guesses where the final 01 begins. Tracking every possible state on input 1101 gives {s} → {s} → {s} → {s, a} → {s, f}. The last set contains f, so the string is accepted.
25.1Epsilon closure#
The epsilon closure of a set includes its starting states and everything reachable by following only ε arrows. “Zero or more” includes zero moves, so a state belongs to its own closure even with no outgoing epsilon arrow. Follow chains and cycles until no new state can be added; cycles do not require infinite processing because each state is added once. The pending list stores states whose outgoing epsilon arrows still need examination; pending.pop() removes one state to examine, and copy(states) makes a separate working copy so the starting set is not altered.
epsilon_closure(states):
reached = copy(states)
pending = list(states)
while pending:
q = pending.pop()
for destination in delta(q, EPSILON):
if destination not in reached:
reached.add(destination)
pending.append(destination)
return reached
s ──ε──► a ──ε──► b ──1──► f
closure({s}) = {s,a,b}; f is excluded because reaching it consumes 1.
read 1 from {s,a,b}: reach {f}; then take closure({f}) again.
The complete input loop starts at closure({q0}), unions the destinations for the next actual symbol, and then takes closure again. Accept if the last closed state set intersects F. Empty input is accepted exactly when the initial closure contains an accepting state.
The -closure of a state set is every state reachable using zero or more -transitions.
When tracing an NFA with epsilon transitions:
- take the epsilon closure;
- consume the next symbol from every current state;
- take epsilon closure again;
- repeat.
25.2NFA-to-DFA subset construction#
A DFA can store an entire NFA-state subset as one of its state labels. This removes branching without changing which strings are accepted. The subset {s,a} is one DFA state, not a command to choose one of two destinations. Each next subset is uniquely determined by the current subset and symbol. Include ∅ if it is reachable: it is the rejecting trap representing no remaining paths. In this pseudocode, subset labels are immutable set values; a literal Python implementation would use frozenset so the labels themselves can be stored in known and used as dictionary keys. union(...) below joins all the listed destination sets; the union of no destination sets is ∅.
| DFA state = NFA subset | On 0 | On 1 | Accepting? |
|---|---|---|---|
| {s} | {s,a} | {s} | No |
| {s,a} | {s,a} | {s,f} | No |
| {s,f} | {s,a} | {s} | Yes, because it contains f |
start_subset = epsilon_closure({nfa_start})
queue = [start_subset]
known = {start_subset}
while queue:
subset = queue.pop()
for symbol in alphabet:
reached = union(delta(q,symbol) for q in subset)
destination = epsilon_closure(reached)
dfa_delta[(subset,symbol)] = destination
if destination not in known:
known.add(destination)
queue.append(destination)
# A subset accepts if it contains at least one NFA accepting state.
For n NFA states, each can be either included or excluded, producing 2n possible subsets by the product rule. Only reachable subsets become necessary DFA states; the three-state example above uses 3 of the possible 2³ = 8 subsets.
NFAs and DFAs recognize exactly the same class of languages.
Convert an NFA to a DFA by making each DFA state represent a set of NFA states.
- The DFA start state is the NFA start-state epsilon closure.
- For each DFA state-set and input symbol, collect every reachable NFA state.
- Take epsilon closure.
- Treat the resulting set as one DFA state.
- A DFA state is accepting if its set contains any accepting NFA state.
An NFA with states may produce up to:
DFA states.
Applied to the NFA for strings ending in 01, the construction reaches only three sets: {s}, {s, a} and {s, f}. Rename them s, a and f, and the result is the three-state DFA of 24.4. Worst-case blowup is possible, but many NFAs produce far fewer than 2n reachable sets.
26Regular expressions and regular languages#
A formal regular expression denotes a set of strings, just as a DFA recognizes a set. A literal such as 0 denotes the one-string language {"0"}; ε denotes {ε}; ∅ denotes the empty language. Expressions R and S below stand for whole languages, not single variables holding input characters. A match means the whole string belongs to that language; substring search by a programming library is a different operation.
| Expression | Meaning | Examples of whole strings |
|---|---|---|
| 0|1 | Choose one alternative | 0 or 1, but not 01 |
| 01 | Concatenate two literals | 01 only |
| 0* | Repeat 0 any nonnegative number of times | ε, 0, 00, 000, … |
| 0+ | Repeat 0 a positive number of times | 0, 00, 000, …; not ε |
Parentheses group expressions. Repetition (* or +) binds most tightly, concatenation next, and union | last. Thus 0|10* means 0 or a 1 followed by any number of 0s, while (0|1)0* may start with either symbol. Read the pieces before trying to trace the whole expression.
R = {"0", "1"}
S = {"01"}
union_result = R.union(S) # {"0","1","01"}
concat_result = {x+y for x in R for y in S} # {"001","101"}
two_repeats = {x+y for x in R for y in R} # {"00","01","10","11"}
up_to_two = {""}.union(R).union(two_repeats)
# This finite preview of R* includes 0, 1, and 2 repetitions.
# It is not the whole infinite language R*.
A state machine describes how to recognize strings. A regular expression describes how to build the same set of strings from alternatives, concatenation, and repetition. These are formal-language expressions; programming-library features such as backreferences can go beyond this model.
Regular-expression operations are:
26.1Union#
The bar R | S is an alternative: accept any string in either language. It does not concatenate their strings and is not the conditioning bar from probability. The helper matches(text,R) in the pseudocode means “the whole string belongs to expression R”; it is not a built-in Python function. If a string belongs to both alternatives, it remains one member of the union.
matches_union(text,R,S) = matches(text,R) or matches(text,S)
| R={0,01}, S={1,01} | Union R|S |
|---|---|
| Keep each distinct member | {0,1,01} |
| Do not combine pieces | 001 is not added merely because 0∈R and 01∈S. |
matches strings from or .
26.2Concatenation#
To belong to RS, the input must be splittable into a prefix from R and a remaining suffix from S. The two strings are joined in order, with no inserted separator. Try every split because the expression may permit more than one length for the first piece.
matches_concat(text,R,S):
for i in 0 through len(text):
if matches(text[:i],R) and matches(text[i:],S): return True
return False
# text[:i] is the first i symbols; text[i:] is the rest.
| R={0,1}, S={01} | Joined strings |
|---|---|
| 0 followed by 01 | 001 |
| 1 followed by 01 | 101 |
| Resulting language RS | {001,101} |
matches a string from followed by one from .
26.3Kleene star#
The star R* means concatenate zero, one, two or more strings from R. Zero repetitions contribute ε. If R contains several strings, each repetition may choose a different one. This is the same star used in Σ*, where the repeated pieces are alphabet symbols.
# For a nonempty text, try a nonempty first matched piece.
# The strictly shorter remainder makes recursion terminate.
matches_star(text,R):
if text == "": return True
for i in 1 through len(text):
if matches(text[:i],R) and matches_star(text[i:],R): return True
return False
| R={01} | R* members by number of repetitions |
|---|---|
| 0 repetitions | ε |
| 1 repetition | 01 |
| 2 repetitions | 0101 |
| 3 repetitions | 010101 |
Allowing empty pieces would not create additional nonempty strings; it would only permit a non-progressing recursive call. The pseudocode omits those calls deliberately. It explains the language operation, not an efficient regex engine.
matches zero or more repetitions of .
26.4One or more#
R+ is R followed by R*: require at least one chosen piece. If R itself contains ε, then R+ can still contain ε; “one or more” counts repetitions of a piece, not necessarily consumed characters. For the literal 0, each piece is nonempty, so 0+ excludes empty input.
matches_plus(text,R) = matches_concat(text, R, star(R))
# For R={"01"}, accepted strings start at "01", not "".
| Expression | Concrete decomposition | Why it matches |
|---|---|---|
| (0|1)*01 on 1101 | 11 + 0 + 1 | Arbitrary binary prefix, then final 01 |
| 0*(10*10*)* on 0010010 | 00 + (1·00·1·0) | The repeated block contributes exactly two 1s. |
| 0*(10*10*)* on ε | Zero initial 0s and zero blocks | The empty string has an even number of 1s. |
Closure means applying an operation to regular languages always produces a regular language. It does not mean the original language contains its own results. These are constructions, not extra memory features:
| Operation | Machine or expression idea |
|---|---|
| Union | Run two DFAs in paired states; accept when either accepts. |
| Intersection | Run the paired states; require both accept. |
| Complement | For a complete DFA over a fixed alphabet, flip accepting versus rejecting states. |
| Difference A−B | Accept when A accepts and B rejects, using paired states. |
| Concatenation | Add ε arrows from each first-machine accepting state to the second start. |
| Kleene star | Add a new accepting start and ε paths for zero or more full runs. |
| Reversal | Reverse an NFA’s arrows; use a new start with ε arrows to the old accepting states, and accept at the old start. |
# Product-state construction for intersection:
state = (start_A, start_B)
for symbol in input_string:
state = (delta_A(state[0],symbol), delta_B(state[1],symbol))
accept = state[0] in F_A and state[1] in F_B
The complement construction requires a transition for every state-symbol pair. Add a rejecting trap to an incomplete machine before flipping acceptance; otherwise missing paths would not become accepting paths. All complements are relative to the same alphabet’s Σ*.
matches one or more repetitions.
The strings ending in 01 are (0|1)*01: anything at all, then 0, then 1. The strings with an even number of 1s are 0*(10*10*)*: some 0s, then any number of blocks that each contain exactly two 1s. The first language has the machines of 24.4 and chapter 25; the second has the parity machine of 24.2.
A language is regular when it can be recognized by a finite automaton. Equivalent representations include:
- DFA;
- NFA;
- regular expression.
Regular languages are closed under:
- union;
- intersection;
- complement;
- difference;
- concatenation;
- Kleene star;
- reversal.
Some closures are easy to see with machines. For the complement, take a DFA and swap its accepting and nonaccepting states; every string that was accepted is now rejected, and vice versa.
27Limits of finite-state computation#
The notation {0n1n : n ≥ 0} describes a set by a rule: for every nonnegative integer n, form n zeros followed by n ones. The colon reads “such that,” and the superscripts here mean string repetition, not numerical exponentiation. For n=0, both blocks are empty, giving ε. This language needs exact equality of unbounded counts; fixed-remainder counting cannot establish that equality.
| String | In the language? | Reason |
|---|---|---|
| ε | Yes | 0 zeros followed by 0 ones |
| 01 | Yes | One of each |
| 0011 | Yes | Two of each |
| 000111 | Yes | Three of each |
| 001 | No | Two zeros but only one 1 |
| 0101 | No | All zeros must precede all ones. |
Imagine a supposed DFA with k states. After reading the k+1 different zero-only prefixes ε, 0, 00, …, 0k, some two prefixes must share a state by pigeonhole. Name their lengths i and j, with i ≠ j (“not equal”). From one shared state, the machine must make identical moves on the same remaining suffix. Appending i ones should accept the prefix of length i but reject the prefix of length j. It cannot give different answers from the same state on the same suffix. That contradiction proves no DFA can recognize the language.
0...0 (i zeros) ──► shared state ── i ones ──► same final state
0...0 (j zeros) ──► shared state ── i ones ──► same final state
Required answers: first ACCEPT (i=i), second REJECT (j≠i).
A DFA cannot supply both answers from identical future behavior.
# An algorithm with growing memory can decide this language.
# This integer counter is not a finite-state implementation.
count = 0
seen_one = False
for bit in input_string:
if bit == "0" and not seen_one: count += 1
elif bit == "1":
seen_one = True
count -= 1
if count < 0: return REJECT
else: return REJECT # includes a 0 after the first 1
return ACCEPT if count == 0 else REJECT
The counter must distinguish arbitrarily many values as input length grows. A real computer has finite hardware, but the algorithmic model allows memory to grow with the input; a fixed DFA does not. A Turing machine models that growing workspace as a tape with readable and writable cells. Unbounded memory is not a promise that every problem is solvable. A decider must halt with the right yes/no answer on every input; a procedure that loops forever on some inputs does not establish decidability.
The parity machine works because many different prefixes can share one state: their relevant future behavior is identical. Exact unbounded counting cannot be compressed into finitely many such states. That is a memory limit, not a slow-algorithm problem.
A finite automaton has only finitely many memory states.
It can remember:
- parity;
- a remainder modulo a fixed integer;
- whether a pattern has occurred;
- a bounded suffix;
- any other finite classification.
It cannot remember an arbitrary unbounded integer exactly.
For example, the language:
Here 0n1n means n 0s followed by n 1s. Recognizing this language requires remembering how many s occurred so the machine can verify exactly the same number of s. No finite automaton can do this for arbitrarily large .
The pigeonhole principle (19.13) proves it. Suppose some DFA with k states recognized the language. Feed it the k + 1 prefixes ε, 0, 00, …, 0k. Two of them, say 0i and 0j with i ≠ j, must end in the same state. From that state the machine behaves identically on whatever follows, so it gives 0i1i and 0j1i the same answer. Only the first belongs to the language, so no such DFA exists.
A Turing machine adds an unbounded read-write tape and can perform computations beyond finite automata. The central distinction is memory:
- finite automaton: fixed finite memory;
- Turing machine: unbounded working memory in principle.
A language is decidable when some algorithm halts on every input and answers membership correctly.
28Moore and Mealy machines#
These are transducers: finite-state machines that produce outputs while processing input. An output alphabet is a finite set of possible emitted labels, separate from the input alphabet. The output function, often named λ (“lambda”), returns an output label. For a Moore machine it takes only a state: λ(q). For a Mealy machine it takes both the old state and the current input: λ(q,b). The next-state function δ still determines where the machine moves.
| Machine | Where output is written | Parity example |
|---|---|---|
| Moore | On a state, often state/output | even/E and odd/O |
| Mealy | On an arrow, often input/output | even --1/O→ odd; odd --1/E→ even |
# Moore: emit the start-state output, then one after every move.
state = even
outputs = [label[state]]
for bit in input_string:
state = delta(state,bit)
outputs.append(label[state])
# Mealy: emit on each move, using old state and current input.
state = even
outputs = []
for bit in input_string:
outputs.append(output_rule(state,bit))
state = delta(state,bit)
| Input consumed | State | Moore output at that point | Mealy output on that move |
|---|---|---|---|
| ε | even | E (initial) | None; no move yet |
| 1 | odd | O | O |
| 10 | odd | O | O |
| 101 | even | E | E |
This table uses the convention of recording the Moore machine’s initial output. For input length n, that gives n+1 outputs versus n Mealy outputs. Some presentations omit the initial Moore output; check the stated timing convention rather than treating the count difference as universal. A Mealy output can change when the input changes even if the destination state stays the same, while a Moore state always carries the same output. In the parity example, we deliberately made the Mealy output equal the destination’s Moore label so the post-input outputs correspond.
So far, a machine has reported acceptance after reading a string. Moore and Mealy machines instead attach outputs to its operation. Ask where the output label lives: on a state or on a transition.
Finite-state machines may also produce outputs.
A Moore machine associates output with the current state.
A Mealy machine associates output with transitions, depending on current state and input.
The recognition distinction is:
- Moore output changes after entering a state;
- Mealy output is generated while taking a transition.
Make the parity machine of chapter 24 report its parity. As a Moore machine, each state carries its label, so reading 101 outputs even (the start state), odd, odd, even: one output more than the input has symbols. As a Mealy machine, each arrow carries the label of the state it enters, so the same input outputs odd, odd, even: one output per symbol read.
The essential classification table#
Shared reference. This end matter covers C959 and C960 together and appears in both guides. Links into the other course open its guide.
Using the reference pages. The tables below compress the full lessons; they do not replace their definitions. An expression is meaningful only with its domain and assumptions. The prose before each formula family names its symbols and links back to the worked explanation, including code and diagrams.
| What the question says | What it is invoking |
|---|---|
| “ only if ” | |
| “ is necessary for ” | |
| “Not every ” | for which the property fails |
| “Some is ” | |
| Original implication rewritten equivalently | Contrapositive |
| Same truth column for every row | Logical equivalence |
| One counterexample | Disproves a universal claim |
| All subsets | Power set, |
| Every input gets one output | Function |
| No output is shared | Injective |
| Every codomain element is hit | Surjective |
| Inner matrix dimensions agree | Product exists |
| Pivot missing but no contradiction | Free variable, infinitely many solutions |
| Row | No solution |
| Constant difference | Arithmetic sequence |
| Constant ratio | Geometric sequence |
| Infinite geometric sum | Requires |
| Every diagonal relation pair exists | Reflexive |
| Reverse edge always exists | Symmetric |
| No distinct pair occurs both ways | Antisymmetric |
| Every two-step chain has shortcut | Transitive |
| Reflexive, symmetric, transitive | Equivalence relation |
| Reflexive, antisymmetric, transitive | Partial order |
| Uses every edge once | Euler |
| Visits every vertex once | Hamilton |
| Connected and acyclic | Tree |
| Loop variable doubles or halves | Logarithmic |
| Sequential blocks | Add, retain dominant term |
| Nested independent loops | Multiply iteration counts |
| Large modular power | Repeated squaring |
| Modular division | Find an inverse |
| Find modular inverse | Extended Euclidean algorithm |
| RSA private exponent | |
| Prove all integers from a base onward | Induction |
| Next case depends on several prior cases | Strong induction |
| Order matters | Permutation |
| Order does not matter | Combination |
| Unordered choice with repetition | Stars and bars |
| “At least one” | Complement is often easiest |
| “Given that” | Conditional probability |
| Reverse a conditional after observing evidence | Bayes |
| Fixed independent success trials | Binomial |
| Sampling without replacement | Hypergeometric |
| Weighted average result | Expected value |
| One next state per state-symbol pair | DFA |
| At least one successful computational path | NFA |
| Machine needs unbounded exact counting | Not finite-state |
Distinctions the exams can exploit#
Element versus subset#
says is an element.
says every element of set belongs to .
Empty set versus a set containing it#
Codomain versus range#
The codomain is declared. The range is actually reached.
Surjectivity means they are equal.
Converse versus contrapositive#
is equivalent to:
not to:
Symmetric versus antisymmetric#
Symmetric requires reverse pairs.
Antisymmetric forbids reverse pairs only for distinct elements.
They are not logical opposites.
Sequence versus series#
A sequence is a list:
A series is its sum:
Necessary divergence test#
proves diverges.
does not prove convergence.
Walk, trail, and path#
- walk: repetitions allowed;
- trail: no repeated edge;
- path: open walk with no repeated vertex;
- cycle: closed trail of length at least 1 whose only repeated vertex is its start and end.
Euler versus Hamilton#
- Euler concerns edges.
- Hamilton concerns vertices.
MST versus shortest-path tree#
An MST minimizes total selected edge weight.
Dijkstra minimizes source-to-vertex distances.
These objectives are not equivalent.
Big-O versus Theta#
Big-O is an upper bound.
Theta is a tight bound.
A function can have many valid Big-O bounds.
Modular inverse versus reciprocal#
There are no ordinary fractions modulo .
To divide by , multiply by , which exists only when:
Permutation versus combination#
Ask whether swapping selected objects creates a different outcome.
- yes: permutation;
- no: combination.
Mutually exclusive versus independent#
Mutually exclusive means they cannot occur together.
Independent means one does not affect the probability of the other.
Nontrivial mutually exclusive events are dependent.
Accepting state versus accepted string#
A finite automaton accepts only after consuming the entire string and ending in an accepting state.
Passing through one earlier is not enough.
Formulas to know cold#
Logic#
Read the truth operators: p and q are claims; ¬ is NOT, ∧ AND, ∨ inclusive OR, → “if … then,” ↔ “if and only if,” and ≡ “has the same truth value on every assignment.” P(x) is a claim about input x; ∀ means every allowed x and ∃ at least one. Thus “not every x passes” is exactly “some x fails.”
Sets and functions#
Sets: A and B are collections; |A| counts A's members; 𝒫(A) collects its subsets; × forms ordered pairs; ∪ means union and ∩ overlap. Read the first formula as “the number of subsets is two raised to the number of elements.” The last counts functions: every input in A independently chooses one of |B| outputs. Superscripts here are powers of counts, not complements.
Number of functions :
Sequences#
Sequences: aₙ is the term in position n, a₁ the first term, d the constant difference, r the constant ratio, and Sₙ the sum of the first n terms. Σ means add the terms at all indicated integer indices; a lower limit of 0 starts with a₀, not a₁. ∞ marks an unending sum interpreted as the limit of finite partial sums. |r| is the absolute size of r; the infinite geometric formula requires |r|<1, while the finite ratio formula requires r≠1. The last two formulas add the integers 1 through n and their squares.
Graphs and trees#
Graphs: V is the vertex set, E the edge set, | | counts its elements, deg(v) is the number of edge incidences at vertex v, and Kₙ is the simple complete graph on n vertices. Summing degrees counts every undirected edge twice. In a full m-ary tree, every internal node has m children; n counts all nodes, i internal nodes, and ℓ leaves. In matrix A, (Aᵏ)ᵢⱼ selects row i, column j of the kth product; k counts steps, not a vertex label. Boolean products use OR and AND to record existence; ordinary products count walks. G⁺ means transitive closure: all positive-length reachable pairs. In the Boolean matrix sum, + means entrywise OR, not ordinary addition.
Full -ary tree:
Adjacency matrix powers, Boolean arithmetic: the entry is 1 exactly when a walk of length runs from to :
With ordinary arithmetic the same entry counts those walks.
Transitive closure on vertices, as graphs and as a Boolean matrix sum:
Counting#
Counting: n is the number of available types or distinct objects and r the number selected, as specified by the model. n! means n×(n−1)×…×1, with 0!=1. P(n,r) counts ordered selections without replacement; the stacked symbol “n choose r” counts unordered ones. For stars and bars, x₁,…,xₖ are integer occupancies in k labeled boxes, n the total objects, and ≥ means “at least.” The final ceiling, ⌈N/k⌉, rounds N divided by k upward: some box must hold at least that many when N objects occupy k boxes. Check k>0 and the positivity conditions before choosing a formula.
for generalized pigeonhole occupancy.
Probability#
Probability: P(A) is the probability of event A, Aᶜ its complement, ∪ “either,” ∩ “both,” and P(A|B) “probability of A given B,” requiring P(B)>0. This P is not the permutation count in the preceding family. In Bayes' formula the events Aᵢ partition the possibilities; the denominator sums over every hypothesis j. A random variable X assigns a number to an outcome; E[X] is its probability-weighted average, Var(X) its mean squared spread, and Σₓ adds over possible values x. For the last, binomial formula, n is the number of independent trials, p their common success probability, and k the number of successes, from 0 through n.
Number theory and RSA#
Number theory: a≡b (mod n) means a and b differ by a multiple of positive integer n; n|(a−b) says n divides that difference. gcd is greatest common divisor and lcm least positive common multiple (the displayed product rule applies to positive a,b). a⁻¹ modulo n is a multiplicative inverse, not an ordinary fraction. RSA: p and q are distinct primes, n=pq, φ(n), read “phi of n,” counts remainders from 0 through n−1 whose greatest common divisor with n is 1; e is the public exponent, d its modular inverse modulo φ(n), m the message representative, and c the ciphertext representative. These are textbook arithmetic formulas, not a secure encryption implementation; practical RSA requires suitable padding.
Complexity#
Growth: n is input size; log n is a logarithm (any fixed base greater than one gives the same asymptotic class). O means eventual upper bound, Ω eventual lower bound, and Θ matching upper and lower bounds, ignoring constant multipliers. A bound applies to a stated best, worst, or average case; the symbol alone does not choose one. Recurrences: T(n) counts work, T(n−1) a call with one item removed, T(n/2) a half-sized call, and 2T(n/2) two such calls. The displayed solutions assume constant-cost base cases and the shown nonrecursive work.
Exam preparation chiefly depends on recognizing which definition or mechanism a question invokes.