Bytebook · DMS
Discrete Mathematics
The mathematics under computer science: logic, proofs, sets, relations, functions, counting, recurrences, graph theory, number theory and Boolean algebra.
Chapter 1 · Logic & Proofs
Propositional Logic
Propositions and the logical connectives — negation, AND, OR, XOR, implication and biconditional — with truth tables, converse and contrapositive, and translating English.
Logic is the language of precise reasoning. It underlies every if statement you write, digital circuits, database queries and mathematical proof.
Propositions
A proposition is a declarative statement that is either true (T) or false (F), but not both.
| Statement | Proposition? |
|---|---|
| "7 is a prime number." | yes — true |
| "Delhi is the capital of Kerala." | yes — false |
| "x + 2 = 5" | no — its truth depends on x |
| "Close the door." | no — a command |
| "Is it raining?" | no — a question |
We name propositions with letters p, q, r …
Connectives
| Name | Symbol | Read as | True when |
|---|---|---|---|
| Negation | ¬p | not p | p is false |
| Conjunction | p ∧ q | p and q | both are true |
| Disjunction | p ∨ q | p or q | at least one is true |
| Exclusive or | p ⊕ q | p xor q | exactly one is true |
| Implication | p → q | if p then q | except when p is true and q is false |
| Biconditional | p ↔ q | p if and only if q | p and q have the same value |
| p | q | ¬p | p ∧ q | p ∨ q | p ⊕ q | p → q | p ↔ q |
|---|---|---|---|---|---|---|---|
| T | T | F | T | T | F | T | T |
| T | F | F | F | T | T | F | F |
| F | T | T | F | T | T | T | F |
| F | F | T | F | F | F | T | T |
Implication: the tricky one
"If you score above 90, you get an O grade." The promise is broken only when you score above 90 and do not get an O. If you score below 90, the promise says nothing — so the implication is true (vacuously).
Ways to say p → q in English: "if p, then q", "p implies q", "q if p", "p only if q", "p is sufficient for q", "q is necessary for p", "q whenever p".
From p → q we form three related statements:
| Name | Form | Equivalent to p → q? |
|---|---|---|
| Converse | q → p | no |
| Inverse | ¬p → ¬q | no |
| Contrapositive | ¬q → ¬p | yes |
"If it rains, the ground is wet." Contrapositive: "If the ground is not wet, it did not rain" — same meaning. Converse: "If the ground is wet, it rained" — not the same (a sprinkler could have wet it).
Build any truth table
Type a formula and press Enter. Use ! or ~ for NOT, & for AND, | for OR, ^ for XOR, -> for implies and <-> for if and only if.
Truth tables in code
With n variables a truth table has 2ⁿ rows. Python can generate it:
from itertools import product
def implies(a, b):
return (not a) or b
print(f"{'p':5} {'q':5} {'r':5} | (p→q)∧(q→r) → (p→r)")
for p, q, r in product([True, False], repeat=3):
value = implies(implies(p, q) and implies(q, r), implies(p, r))
print(f"{p!s:5} {q!s:5} {r!s:5} | {value}")p q r | (p→q)∧(q→r) → (p→r) True True True | True True True False | True True False True | True True False False | True False True True | True False True False | True False False True | True False False False | True
Every row is True: this formula (the hypothetical syllogism) is a tautology.
Tautology, contradiction, contingency
| Kind | Meaning | Example |
|---|---|---|
| Tautology | true in every row | p ∨ ¬p |
| Contradiction | false in every row | p ∧ ¬p |
| Contingency | true in some rows, false in others | p → q |
Precedence
¬ binds tightest, then ∧, then ∨, then →, then ↔. So ¬p ∨ q ∧ r means (¬p) ∨ (q ∧ r). Use brackets when in doubt.
Translating English
Let p = "it is raining", q = "I carry an umbrella", r = "I get wet".
| English | Logic |
|---|---|
| It is raining and I don't carry an umbrella. | p ∧ ¬q |
| If it rains and I have no umbrella, I get wet. | (p ∧ ¬q) → r |
| I get wet only if it is raining. | r → p |
| I carry an umbrella if and only if it is raining. | q ↔ p |
Chapter 2 · Logic & Proofs
Logical Equivalences & Normal Forms
When two formulas mean the same thing: the laws of logic including De Morgan's, simplifying expressions step by step, and converting to disjunctive and conjunctive normal form.
Two formulas are logically equivalent, written P ≡ Q, if they have the same truth value in every row of the truth table — equivalently, if P ↔ Q is a tautology.
The laws
| Law | Equivalence |
|---|---|
| Identity | p ∧ T ≡ p, p ∨ F ≡ p |
| Domination | p ∨ T ≡ T, p ∧ F ≡ F |
| Idempotent | p ∨ p ≡ p, p ∧ p ≡ p |
| Double negation | ¬(¬p) ≡ p |
| Commutative | p ∨ q ≡ q ∨ p, p ∧ q ≡ q ∧ p |
| Associative | (p ∨ q) ∨ r ≡ p ∨ (q ∨ r) |
| Distributive | p ∧ (q ∨ r) ≡ (p ∧ q) ∨ (p ∧ r); p ∨ (q ∧ r) ≡ (p ∨ q) ∧ (p ∨ r) |
| De Morgan's | ¬(p ∧ q) ≡ ¬p ∨ ¬q; ¬(p ∨ q) ≡ ¬p ∧ ¬q |
| Absorption | p ∨ (p ∧ q) ≡ p; p ∧ (p ∨ q) ≡ p |
| Negation | p ∨ ¬p ≡ T, p ∧ ¬p ≡ F |
Equivalences involving implication:
| Equivalence | |
|---|---|
| p → q ≡ ¬p ∨ q | implication as OR |
| p → q ≡ ¬q → ¬p | contrapositive |
| ¬(p → q) ≡ p ∧ ¬q | negating an implication |
| p ↔ q ≡ (p → q) ∧ (q → p) | biconditional |
| (p → q) ∧ (p → r) ≡ p → (q ∧ r) |
Proving equivalences by the laws
Show ¬(p ∨ (¬p ∧ q)) ≡ ¬p ∧ ¬q:
| Step | Reason |
|---|---|
| ¬(p ∨ (¬p ∧ q)) | given |
| ≡ ¬p ∧ ¬(¬p ∧ q) | De Morgan |
| ≡ ¬p ∧ (¬¬p ∨ ¬q) | De Morgan |
| ≡ ¬p ∧ (p ∨ ¬q) | double negation |
| ≡ (¬p ∧ p) ∨ (¬p ∧ ¬q) | distributive |
| ≡ F ∨ (¬p ∧ ¬q) | negation law |
| ≡ ¬p ∧ ¬q | identity |
Check any equivalence instantly — compare both columns:
Normal forms
Every formula can be rewritten in a standard shape:
- Disjunctive normal form (DNF): an OR of AND-terms — a "sum of products". Example: (p ∧ ¬q) ∨ (¬p ∧ q).
- Conjunctive normal form (CNF): an AND of OR-clauses — a "product of sums". Example: (p ∨ q) ∧ (¬p ∨ ¬q).
DNF from a truth table: for each row where the formula is T, write the AND of the literals that make that row (p if p is T, ¬p if p is F); OR them together.
CNF from a truth table: for each row where the formula is F, write the OR of the negated literals; AND them together.
from itertools import product
def formula(p, q, r):
return (p or q) and not r # the function we want in normal form
names = "pqr"
minterms, maxterms = [], []
for row in product([True, False], repeat=3):
lits = [n if v else "¬" + n for n, v in zip(names, row)]
if formula(*row):
minterms.append("(" + " ∧ ".join(lits) + ")")
else:
maxterms.append("(" + " ∨ ".join("¬" + n if v else n for n, v in zip(names, row)) + ")")
print("DNF:", " ∨ ".join(minterms))
print("CNF:", " ∧ ".join(maxterms))DNF: (p ∧ q ∧ ¬r) ∨ (p ∧ ¬q ∧ ¬r) ∨ (¬p ∧ q ∧ ¬r) CNF: (¬p ∨ ¬q ∨ ¬r) ∧ (¬p ∨ q ∨ ¬r) ∧ (p ∨ ¬q ∨ ¬r) ∧ (p ∨ q ∨ ¬r) ∧ (p ∨ q ∨ r)
CNF matters in computer science because SAT solvers work on CNF formulas; DNF and CNF also correspond to sum-of-products and product-of-sums circuits in Boolean algebra.
Functionally complete sets
{¬, ∧, ∨} can express every Boolean function. So can {¬, ∧} (by De Morgan), and so can NAND alone or NOR alone — which is why chips can be built from a single gate type.
Chapter 3 · Logic & Proofs
Predicates & Quantifiers
Statements with variables: predicates, the universal and existential quantifiers, domains, negating quantified statements, nested quantifiers and translating English.
Propositional logic cannot express "every student passed" or "some number is prime" in a useful way. Predicate logic adds variables, predicates and quantifiers.
Predicates
A predicate P(x) is a statement with a variable; it becomes a proposition once x is given a value.
- P(x): "x > 3". Then P(5) is true and P(1) is false.
- Q(x, y): "x + y = 10". Q(4, 6) is true.
The set of values x may take is the domain (universe of discourse).
Quantifiers
| Quantifier | Symbol | Meaning | True when |
|---|---|---|---|
| Universal | ∀x P(x) | "for all x, P(x)" | P(x) holds for every x in the domain |
| Existential | ∃x P(x) | "there exists x such that P(x)" | P(x) holds for at least one x |
| Uniqueness | ∃!x P(x) | "there is exactly one x" | exactly one x makes it true |
With domain = {1, 2, 3, 4}:
- ∀x (x > 0) — true.
- ∀x (x is even) — false (counterexample: 1).
- ∃x (x is even) — true (witness: 2).
- ∃x (x > 10) — false.
On a finite domain, ∀ is a big AND and ∃ is a big OR — exactly Python's all and any:
domain = range(1, 21)
print("∀x x² ≥ x: ", all(x * x >= x for x in domain))
print("∃x x² = 49: ", any(x * x == 49 for x in domain))
print("∀x ∃y y > x: ", all(any(y > x for y in domain) for x in domain)) # false here: nothing in 1..20 exceeds 20
print("∃y ∀x y ≥ x: ", any(all(y >= x for x in domain) for y in domain))
print("counterexample to 'all odd numbers are prime':", next(x for x in domain if x % 2 == 1 and x > 1 and any(x % d == 0 for d in range(2, x))))∀x x² ≥ x: True ∃x x² = 49: True ∀x ∃y y > x: False ∃y ∀x y ≥ x: True counterexample to 'all odd numbers are prime': 9
Negating quantifiers (De Morgan for quantifiers)
| Statement | Negation |
|---|---|
| ∀x P(x) | ∃x ¬P(x) |
| ∃x P(x) | ∀x ¬P(x) |
"Not every student passed" = "some student did not pass". "There is no perfect algorithm" = "every algorithm is imperfect".
Nested quantifiers
The order of different quantifiers matters:
- ∀x ∃y (y > x): "for every number there is a bigger one" — true for the integers.
- ∃y ∀x (y > x): "there is a number bigger than every number" — false for the integers.
Negate nested quantifiers by flipping each one and negating the inside: ¬∀x ∃y P(x, y) ≡ ∃x ∀y ¬P(x, y).
Translating English
Let S(x): "x is a student", C(x): "x has a laptop", the domain being all people.
| English | Logic |
|---|---|
| Every student has a laptop. | ∀x (S(x) → C(x)) |
| Some student has a laptop. | ∃x (S(x) ∧ C(x)) |
| No student has a laptop. | ∀x (S(x) → ¬C(x)) |
| Not every student has a laptop. | ∃x (S(x) ∧ ¬C(x)) |
Rules of inference for quantifiers
| Rule | From | Conclude |
|---|---|---|
| Universal instantiation | ∀x P(x) | P(c) for any c |
| Universal generalisation | P(c) for an arbitrary c | ∀x P(x) |
| Existential instantiation | ∃x P(x) | P(c) for some (new) c |
| Existential generalisation | P(c) for some c | ∃x P(x) |
Classic argument: "All humans are mortal; Socrates is human; therefore Socrates is mortal" uses universal instantiation followed by modus ponens.
Chapter 4 · Logic & Proofs
Proof Techniques
How mathematicians and computer scientists prove things: rules of inference, direct proof, proof by contrapositive, contradiction, cases, counterexamples, and existence proofs.
A proof is a chain of logically valid steps from accepted facts to a conclusion. Proofs are how we know an algorithm is correct for all inputs, not just the ones we tested.
Rules of inference
| Rule | Form | Example |
|---|---|---|
| Modus ponens | p, p → q ∴ q | It rains; if it rains the road is wet ∴ the road is wet |
| Modus tollens | ¬q, p → q ∴ ¬p | The road is dry; if it rained it would be wet ∴ it did not rain |
| Hypothetical syllogism | p → q, q → r ∴ p → r | chaining implications |
| Disjunctive syllogism | p ∨ q, ¬p ∴ q | It is tea or coffee; not tea ∴ coffee |
| Addition | p ∴ p ∨ q | |
| Simplification | p ∧ q ∴ p | |
| Conjunction | p, q ∴ p ∧ q | |
| Resolution | p ∨ q, ¬p ∨ r ∴ q ∨ r | the rule behind SAT solvers |
Direct proof
To prove p → q: assume p, then use definitions and known facts to reach q.
Theorem. If n is odd, then n² is odd. Proof. Suppose n is odd, so n = 2k + 1 for some integer k. Then n² = 4k² + 4k + 1 = 2(2k² + 2k) + 1, which is odd. ∎
Proof by contrapositive
Prove ¬q → ¬p instead, which is equivalent to p → q. Use it when the negations are easier to work with.
Theorem. If n² is even, then n is even. Proof. Contrapositive: if n is odd, then n² is odd — proved above. ∎
Proof by contradiction
Assume the statement is false and derive something impossible.
Theorem. √2 is irrational. Proof. Suppose √2 = a/b in lowest terms. Then 2b² = a², so a² is even, so a is even: a = 2c. Then 2b² = 4c², so b² = 2c² is even, so b is even. But then a and b share the factor 2, contradicting "lowest terms". ∎
Theorem (Euclid). There are infinitely many primes. Proof. Suppose there are finitely many: p₁, p₂, …, pₙ. Let N = p₁p₂…pₙ + 1. No pᵢ divides N (each leaves remainder 1), so N's prime factors are not in the list — a contradiction. ∎
Proof by cases
Split into cases that cover every possibility and prove each one.
Theorem. n² + n is even for every integer n. Proof. If n is even, n² and n are both even, so their sum is even. If n is odd, n² and n are both odd, and odd + odd is even. ∎
Counterexamples
To disprove a universal statement ∀x P(x), one counterexample is enough.
Claim: "n² + n + 41 is prime for every n ≥ 0." It holds for n = 0 … 39, but fails at n = 40. Testing many cases is evidence, never proof:
def is_prime(n):
return n > 1 and all(n % d for d in range(2, int(n ** 0.5) + 1))
for n in range(45):
if not is_prime(n * n + n + 41):
print(f"counterexample: n = {n}, n^2 + n + 41 = {n*n + n + 41} = 41 x {(n*n + n + 41) // 41}")
break
else:
print("no counterexample found")counterexample: n = 40, n^2 + n + 41 = 1681 = 41 x 41
Existence proofs
- Constructive: exhibit the object. "There is an integer that is the sum of two cubes in two ways": 1729 = 1³ + 12³ = 9³ + 10³.
- Non-constructive: show it must exist without naming it (often by contradiction or the pigeonhole principle).
Which technique?
| Statement looks like | Try |
|---|---|
| p → q with a usable p | direct proof |
| q is a negative statement ("not divisible", "irrational") | contradiction or contrapositive |
| "for all n ≥ 1" about integers | induction |
| a few distinct situations (even/odd, sign) | cases |
| "every x" that you suspect is false | counterexample |
Chapter 5 · Logic & Proofs
Mathematical Induction
Prove statements for all natural numbers: the base case and inductive step, sums, divisibility and inequalities, strong induction, and the link to recursion and loop invariants.
Mathematical induction proves that a statement P(n) is true for every integer n ≥ n₀. Think of an infinite line of dominoes:
- Base case: prove P(n₀) — the first domino falls.
- Inductive step: prove that if P(k) is true (the inductive hypothesis), then P(k + 1) is true — each domino knocks over the next.
Together they prove P(n) for all n ≥ n₀.
Example 1: sum of the first n integers
Claim. 1 + 2 + … + n = n(n + 1)/2 for all n ≥ 1.
Base case (n = 1): left side 1, right side 1·2/2 = 1. ✓
Inductive step: assume 1 + 2 + … + k = k(k + 1)/2. Then
1 + 2 + … + k + (k + 1) = k(k + 1)/2 + (k + 1) = (k + 1)(k/2 + 1) = (k + 1)(k + 2)/2,
which is the formula for n = k + 1. ∎
Example 2: divisibility
Claim. 3 divides n³ − n for all n ≥ 0.
Base: n = 0 gives 0, divisible by 3. ✓
Step: assume 3 | k³ − k. Then (k + 1)³ − (k + 1) = k³ + 3k² + 3k + 1 − k − 1 = (k³ − k) + 3(k² + k). Both parts are divisible by 3. ∎
Example 3: an inequality
Claim. 2ⁿ > n² for all n ≥ 5.
Base: 2⁵ = 32 > 25. ✓
Step: assume 2ᵏ > k² with k ≥ 5. Then 2ᵏ⁺¹ = 2·2ᵏ > 2k² = k² + k² > k² + 2k + 1 = (k + 1)², because k² > 2k + 1 when k ≥ 3. ∎
(The base case matters: the claim is false for n = 2, 3, 4.)
Checking a formula is not proving it
Code can test a formula for many n — useful for catching a wrong formula before you try to prove it:
def check(name, lhs, rhs, upto=50):
bad = [n for n in range(1, upto + 1) if lhs(n) != rhs(n)]
print(f"{name}: {'holds' if not bad else 'FAILS at n = ' + str(bad[0])} for n = 1..{upto}")
check("sum of first n odd numbers = n^2", lambda n: sum(2 * i - 1 for i in range(1, n + 1)), lambda n: n * n)
check("sum of squares = n(n+1)(2n+1)/6", lambda n: sum(i * i for i in range(1, n + 1)), lambda n: n * (n + 1) * (2 * n + 1) // 6)
check("sum of cubes = (n(n+1)/2)^2", lambda n: sum(i ** 3 for i in range(1, n + 1)), lambda n: (n * (n + 1) // 2) ** 2)
check("2^n > n^2", lambda n: 2 ** n > n * n, lambda n: True)sum of first n odd numbers = n^2: holds for n = 1..50 sum of squares = n(n+1)(2n+1)/6: holds for n = 1..50 sum of cubes = (n(n+1)/2)^2: holds for n = 1..50 2^n > n^2: FAILS at n = 2 for n = 1..50
The last check fails at n = 2 — which is why that claim starts at n = 5.
Strong induction
In strong induction the inductive step may assume P(n₀), P(n₀ + 1), …, P(k) — all earlier cases — to prove P(k + 1). Use it when the next case depends on cases further back.
Claim. Every integer n ≥ 2 is a product of primes. Proof. Base: 2 is prime. Step: consider k + 1. If it is prime, done. Otherwise k + 1 = a·b with 2 ≤ a, b ≤ k; by the hypothesis both a and b are products of primes, so k + 1 is too. ∎
Claim. Every postage of 12 or more rupees can be made from 4- and 5-rupee stamps. Proof. Base cases 12 = 4+4+4, 13 = 4+4+5, 14 = 4+5+5, 15 = 5+5+5. Step: for k + 1 ≥ 16, take the solution for k − 3 (true by hypothesis) and add one 4-rupee stamp. ∎
Induction and computer science
- Recursion is induction in code: the base case of a recursive function is the base case of the proof, and the recursive call relies on the "inductive hypothesis" that smaller inputs are handled correctly.
- Loop invariants (correctness proofs) are induction on the number of iterations.
- Structural induction proves properties of trees and lists: true for the empty structure, and preserved when building larger ones.
Chapter 6 · Sets, Relations & Functions
Sets
Sets and their notation, subsets and power sets, union, intersection, difference and complement with Venn diagrams, set identities, Cartesian products and cardinality.
A set is an unordered collection of distinct objects called elements. Order and repetition do not matter: {1, 2, 3} = {3, 1, 2} = {1, 1, 2, 3}.
Notation
| Notation | Meaning |
|---|---|
| A = {2, 4, 6} | roster form: list the elements |
| A = {x ∈ ℤ : x is even, 0 < x < 8} | set-builder form: a rule |
| x ∈ A, x ∉ A | x is / is not an element of A |
| ∅ or {} | the empty set |
| U | the universal set — everything under discussion |
| |A| | cardinality: the number of elements |
| ℕ, ℤ, ℚ, ℝ | natural numbers, integers, rationals, reals |
Subsets and the power set
- A ⊆ B (subset): every element of A is in B. Every set is a subset of itself, and ∅ ⊆ A for every A.
- A ⊂ B (proper subset): A ⊆ B and A ≠ B.
- A = B exactly when A ⊆ B and B ⊆ A — the standard way to prove two sets are equal.
The power set P(A) is the set of all subsets of A. If |A| = n, then |P(A)| = 2ⁿ (each element is either in a subset or not).
from itertools import combinations
A = {"a", "b", "c"}
power_set = [set(c) for r in range(len(A) + 1) for c in combinations(sorted(A), r)]
print(len(power_set), "subsets:")
print([sorted(s) for s in power_set])8 subsets: [[], ['a'], ['b'], ['c'], ['a', 'b'], ['a', 'c'], ['b', 'c'], ['a', 'b', 'c']]
Set operations
| Operation | Symbol | Definition | Python |
|---|---|---|---|
| Union | A ∪ B | x ∈ A or x ∈ B | A | B |
| Intersection | A ∩ B | x ∈ A and x ∈ B | A & B |
| Difference | A − B | x ∈ A and x ∉ B | A - B |
| Symmetric difference | A ⊕ B | in exactly one of them | A ^ B |
| Complement | A′ or Ā | x ∈ U and x ∉ A | U - A |
Try your own sets:
Set identities
They mirror the laws of logic — ∪ is like ∨, ∩ like ∧, complement like ¬:
| Law | Identity |
|---|---|
| Identity | A ∪ ∅ = A, A ∩ U = A |
| Domination | A ∪ U = U, A ∩ ∅ = ∅ |
| Idempotent | A ∪ A = A, A ∩ A = A |
| Complement | A ∪ A′ = U, A ∩ A′ = ∅, (A′)′ = A |
| Commutative | A ∪ B = B ∪ A, A ∩ B = B ∩ A |
| Distributive | A ∩ (B ∪ C) = (A ∩ B) ∪ (A ∩ C) |
| De Morgan | (A ∪ B)′ = A′ ∩ B′, (A ∩ B)′ = A′ ∪ B′ |
| Absorption | A ∪ (A ∩ B) = A, A ∩ (A ∪ B) = A |
| Difference | A − B = A ∩ B′ |
U = set(range(1, 11))
A, B = {1, 2, 3, 4, 5}, {4, 5, 6, 7}
comp = lambda X: U - X
print("(A ∪ B)′ =", sorted(comp(A | B)))
print("A′ ∩ B′ =", sorted(comp(A) & comp(B)))
print("De Morgan holds:", comp(A | B) == comp(A) & comp(B), comp(A & B) == comp(A) | comp(B))
print("|A ∪ B| = |A| + |B| - |A ∩ B|:", len(A | B), "=", len(A), "+", len(B), "-", len(A & B))(A ∪ B)′ = [8, 9, 10] A′ ∩ B′ = [8, 9, 10] De Morgan holds: True True |A ∪ B| = |A| + |B| - |A ∩ B|: 7 = 5 + 4 - 2
Cartesian product
The Cartesian product A × B is the set of all ordered pairs (a, b) with a ∈ A and b ∈ B. |A × B| = |A| · |B|.
from itertools import product
sizes = ["S", "M", "L"]
colours = ["red", "blue"]
pairs = list(product(sizes, colours))
print(len(pairs), pairs)6 [('S', 'red'), ('S', 'blue'), ('M', 'red'), ('M', 'blue'), ('L', 'red'), ('L', 'blue')]Order matters in pairs: (1, 2) ≠ (2, 1), so A × B ≠ B × A in general.
Cardinality of unions
For two sets: |A ∪ B| = |A| + |B| − |A ∩ B| — the intersection was counted twice. This is the inclusion–exclusion principle, extended in Counting.
Countable and uncountable
A set is countable if its elements can be listed as a sequence (a one-to-one match with ℕ). ℤ and ℚ are countable; Cantor's diagonal argument shows ℝ is uncountable — there are "more" real numbers than integers.
Chapter 7 · Sets, Relations & Functions
Relations
Relations as sets of ordered pairs, matrix and digraph representations, the reflexive, symmetric, antisymmetric and transitive properties, equivalence relations and classes, and closures.
A relation R from set A to set B is a subset of A × B. If (a, b) ∈ R we write a R b: "a is related to b". A relation on A is a subset of A × A.
Example on A = {1, 2, 3, 4}: R = "divides" = {(a, b) : a | b}.
R = {(1,1), (1,2), (1,3), (1,4), (2,2), (2,4), (3,3), (4,4)}
Representations
Matrix: M[i][j] = 1 if (i, j) ∈ R.
| 1 | 2 | 3 | 4 | |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | 1 |
| 2 | 0 | 1 | 0 | 1 |
| 3 | 0 | 0 | 1 | 0 |
| 4 | 0 | 0 | 0 | 1 |
Digraph: an arrow from a to b for each pair, with a loop at a for each (a, a).
Properties of a relation on A
| Property | Definition | Matrix test |
|---|---|---|
| Reflexive | a R a for every a ∈ A | all 1s on the diagonal |
| Irreflexive | a R a for no a | all 0s on the diagonal |
| Symmetric | a R b ⇒ b R a | M = Mᵀ |
| Antisymmetric | a R b and b R a ⇒ a = b | no 1 at both (i, j) and (j, i) for i ≠ j |
| Transitive | a R b and b R c ⇒ a R c | (M ⊙ M) ≤ M (Boolean product) |
"Divides" is reflexive, antisymmetric and transitive, but not symmetric (1 | 2, but 2 ∤ 1).
def properties(A, R):
return {
"reflexive": all((a, a) in R for a in A),
"symmetric": all((b, a) in R for (a, b) in R),
"antisymmetric": all(a == b for (a, b) in R if (b, a) in R),
"transitive": all((a, d) in R for (a, b) in R for (c, d) in R if b == c),
}
A = {1, 2, 3, 4}
divides = {(a, b) for a in A for b in A if b % a == 0}
same_parity = {(a, b) for a in A for b in A if a % 2 == b % 2}
less_than = {(a, b) for a in A for b in A if a < b}
for name, R in [("divides", divides), ("same parity", same_parity), ("less than", less_than)]:
print(f"{name:12}", properties(A, R))divides {'reflexive': True, 'symmetric': False, 'antisymmetric': True, 'transitive': True}
same parity {'reflexive': True, 'symmetric': True, 'antisymmetric': False, 'transitive': True}
less than {'reflexive': False, 'symmetric': False, 'antisymmetric': True, 'transitive': True}Equivalence relations
A relation that is reflexive, symmetric and transitive is an equivalence relation. It splits A into disjoint equivalence classes: [a] = {x : x R a}.
Example: congruence mod 3 on ℤ — a ≡ b (mod 3) when 3 | (a − b). Its classes are:
- [0] = {…, −3, 0, 3, 6, …}
- [1] = {…, −2, 1, 4, 7, …}
- [2] = {…, −1, 2, 5, 8, …}
The classes form a partition of the set: they are non-empty, pairwise disjoint, and together cover everything. Conversely, every partition defines an equivalence relation ("in the same block").
A = range(0, 12)
classes = {}
for x in A:
classes.setdefault(x % 3, []).append(x) # same remainder = same class
for r, members in classes.items():
print(f"[{r}] = {members}")[0] = [0, 3, 6, 9] [1] = [1, 4, 7, 10] [2] = [2, 5, 8, 11]
Closures
The closure of R with respect to a property is the smallest relation containing R that has the property.
| Closure | How to get it |
|---|---|
| Reflexive | add (a, a) for every a |
| Symmetric | add (b, a) for every (a, b) |
| Transitive | add (a, c) whenever there is a path a → … → c — Warshall's algorithm |
def transitive_closure(A, R):
R = set(R)
for k in A: # Warshall: allow k as an intermediate vertex
for i in A:
for j in A:
if (i, k) in R and (k, j) in R:
R.add((i, j))
return R
A = [1, 2, 3, 4]
R = {(1, 2), (2, 3), (3, 4)}
print(sorted(transitive_closure(A, R)))[(1, 2), (1, 3), (1, 4), (2, 3), (2, 4), (3, 4)]
(The same triple loop computes shortest paths in Floyd–Warshall.)
Chapter 8 · Sets, Relations & Functions
Partial Orders & Lattices
Ordering things that are not all comparable: posets, Hasse diagrams, maximal and minimal elements, upper and lower bounds, lattices and topological sorting.
A relation on A is a partial order if it is reflexive, antisymmetric and transitive. The pair (A, ≼) is a partially ordered set (poset).
Examples:
- (ℤ, ≤) — the usual order.
- (ℤ⁺, |) — divisibility.
- (P(S), ⊆) — subsets ordered by inclusion.
In a partial order, some pairs may be incomparable: neither 2 | 3 nor 3 | 2. If every pair is comparable, it is a total (linear) order — like ≤ on numbers.
Hasse diagrams
A Hasse diagram draws a poset compactly:
- Draw smaller elements lower, so arrows always point up (and are dropped).
- Omit self-loops (reflexivity is assumed).
- Omit edges implied by transitivity: draw a → b only if b covers a (nothing in between).
Special elements
For a poset (A, ≼) and a subset S:
| Term | Meaning |
|---|---|
| Maximal element | nothing in A is strictly greater |
| Minimal element | nothing in A is strictly smaller |
| Greatest element | greater than or equal to every element (unique if it exists) |
| Least element | less than or equal to every element |
| Upper bound of S | an element ≥ every element of S |
| Least upper bound (supremum, join, ∨) | the smallest upper bound |
| Lower bound of S | an element ≤ every element of S |
| Greatest lower bound (infimum, meet, ∧) | the largest lower bound |
In the divisors of 12: least element 1, greatest element 12. For S = {4, 6}: upper bounds {12}, so lub = 12; lower bounds {1, 2}, so glb = 2. For divisibility, lub = LCM and glb = GCD.
from math import gcd
D = [d for d in range(1, 13) if 12 % d == 0]
leq = lambda a, b: b % a == 0 # a divides b
def lub(x, y): return min(u for u in D if leq(x, u) and leq(y, u)) # smallest common multiple in D
def glb(x, y): return max(l for l in D if leq(l, x) and leq(l, y)) # largest common divisor in D
print("divisors of 12:", D)
print("lub(4, 6) =", lub(4, 6), " lcm =", 4 * 6 // gcd(4, 6))
print("glb(4, 6) =", glb(4, 6), " gcd =", gcd(4, 6))
print("covering pairs (Hasse edges):",
[(a, b) for a in D for b in D if a != b and leq(a, b) and not any(c not in (a, b) and leq(a, c) and leq(c, b) for c in D)])divisors of 12: [1, 2, 3, 4, 6, 12] lub(4, 6) = 12 lcm = 12 glb(4, 6) = 2 gcd = 2 covering pairs (Hasse edges): [(1, 2), (1, 3), (2, 4), (2, 6), (3, 6), (4, 12), (6, 12)]
Lattices
A poset is a lattice if every pair of elements has both a least upper bound and a greatest lower bound.
- (Divisors of n, |) is a lattice (join = lcm, meet = gcd).
- (P(S), ⊆) is a lattice (join = ∪, meet = ∩).
- A poset with two maximal elements and nothing above them is not a lattice.
Topological sorting
Every finite poset can be extended to a total order that respects it: repeatedly remove a minimal element. This is exactly topological sorting of a DAG — see Graph Algorithms.
Chapter 9 · Sets, Relations & Functions
Functions
Functions as special relations: domain, codomain and range, one-to-one, onto and bijective functions, composition, inverses, floor and ceiling, and counting functions.
A function f : A → B assigns to each element of A exactly one element of B.
- A is the domain; B is the codomain.
- f(a) = b: b is the image of a; a is a pre-image of b.
- The range is the set of all images: {f(a) : a ∈ A} ⊆ B.
A relation fails to be a function if some element of A has no image or two images.
Types of functions
| Type | Also called | Definition |
|---|---|---|
| One-to-one | injective | different inputs → different outputs: f(a₁) = f(a₂) ⇒ a₁ = a₂ |
| Onto | surjective | every element of B is hit: range = codomain |
| Bijection | one-to-one correspondence | both injective and surjective |
| Function on ℤ → ℤ | Injective? | Surjective? |
|---|---|---|
| f(x) = x + 1 | yes | yes — bijection |
| f(x) = 2x | yes | no (odd numbers are missed) |
| f(x) = x² | no (f(2) = f(−2)) | no |
| f(x) = ⌊x / 2⌋ | no | yes |
def classify(f, A, B):
images = [f(a) for a in A]
injective = len(set(images)) == len(images)
surjective = set(images) == set(B)
return f"injective={injective}, surjective={surjective}, bijective={injective and surjective}"
A = range(-3, 4)
print("x+1 :", classify(lambda x: x + 1, A, range(-2, 5)))
print("x^2 :", classify(lambda x: x * x, A, range(0, 10)))
print("2x :", classify(lambda x: 2 * x, A, range(-6, 7)))x+1 : injective=True, surjective=True, bijective=True x^2 : injective=False, surjective=False, bijective=False 2x : injective=True, surjective=False, bijective=False
Composition
(g ∘ f)(x) = g(f(x)) — apply f first, then g. Composition is associative but not commutative:
f = lambda x: x + 3
g = lambda x: 2 * x
print("(g∘f)(5) =", g(f(5))) # 2 * (5 + 3) = 16
print("(f∘g)(5) =", f(g(5))) # 2 * 5 + 3 = 13(g∘f)(5) = 16 (f∘g)(5) = 13
Inverse functions
A function has an inverse f⁻¹ if and only if it is a bijection. Then f⁻¹(b) = a exactly when f(a) = b, and f⁻¹(f(x)) = x. Example: f(x) = 3x + 2 on ℝ has f⁻¹(y) = (y − 2)/3.
Floor and ceiling
- ⌊x⌋ (floor): the largest integer ≤ x. ⌊3.7⌋ = 3, ⌊−3.7⌋ = −4.
- ⌈x⌉ (ceiling): the smallest integer ≥ x. ⌈3.2⌉ = 4, ⌈−3.2⌉ = −3.
They appear constantly in algorithm analysis: a binary tree with n nodes has height at least ⌈log₂(n + 1)⌉ − 1; splitting n items into pages of 10 needs ⌈n/10⌉ pages.
Counting functions
For finite sets with |A| = m and |B| = n:
| Count | Formula |
|---|---|
| all functions A → B | nᵐ |
| injective functions (m ≤ n) | n!/(n − m)! |
| bijections (m = n) | n! |
| surjective functions | Σₖ (−1)ᵏ C(n, k)(n − k)ᵐ (by inclusion–exclusion) |
Pigeonhole connection
If m > n, no function A → B can be injective: some two elements of A must share an image. That is the pigeonhole principle.
Chapter 10 · Counting
Counting Principles & Pigeonhole
The sum and product rules, counting with complements, tree diagrams, and the pigeonhole principle with its generalised form and classic applications.
Counting answers "how many?" — how many passwords, how many paths, how many comparisons an algorithm makes. Almost every counting problem is built from a few simple rules.
The sum rule
If a task can be done in one of m ways or one of n ways, with no overlap, there are m + n ways.
A student picks one elective from 5 CS electives or 3 maths electives: 5 + 3 = 8 choices.
The product rule
If a task is a sequence of steps with m ways for the first and n ways for the second, there are m × n ways.
| Question | Count |
|---|---|
| Bit strings of length 8 | 2⁸ = 256 |
| 4-digit PINs | 10⁴ = 10,000 |
| Car plates: 2 letters then 4 digits | 26² × 10⁴ = 6,760,000 |
| Functions from a 5-set to a 3-set | 3⁵ = 243 |
| Subsets of an n-set | 2ⁿ |
Counting with the complement
Often it is easier to count what you don't want and subtract:
How many 8-bit strings contain at least one 1? All strings minus the all-zero string: 2⁸ − 1 = 255.
from itertools import product
passwords = ["".join(p) for p in product("ab1", repeat=4)]
with_digit = [p for p in passwords if "1" in p]
print(len(passwords), "passwords of length 4 over {a, b, 1}")
print(len(with_digit), "contain the digit, and 3^4 - 2^4 =", 3**4 - 2**4)81 passwords of length 4 over {a, b, 1}
65 contain the digit, and 3^4 - 2^4 = 65The pigeonhole principle
If n + 1 (or more) pigeons fly into n holes, some hole contains at least two pigeons.
It sounds obvious, but it proves surprising things:
- Among any 13 people, two share a birth month.
- In any group of 367 people, two share a birthday.
- A hash table with m slots and m + 1 keys must have a collision.
- Among any 6 integers, two have the same remainder mod 5 — so their difference is divisible by 5.
Generalised pigeonhole principle
If N objects go into k boxes, some box contains at least ⌈N/k⌉ objects.
- 100 students, 12 months → some month has at least ⌈100/12⌉ = 9 birthdays.
- How many cards must you draw from a standard deck to guarantee 3 of the same suit? Need ⌈N/4⌉ ≥ 3, so N = 2 × 4 + 1 = 9.
from math import ceil
def guarantee(per_box, boxes):
return (per_box - 1) * boxes + 1 # smallest N with ceil(N/boxes) >= per_box
print("cards to guarantee 3 of a suit:", guarantee(3, 4))
print("people to guarantee 2 share a birth month:", guarantee(2, 12))
print("students so that 5 share a grade (grades O, A+, A, B+, P, F):", guarantee(5, 6))
print("minimum in the fullest month for 100 birthdays:", ceil(100 / 12))cards to guarantee 3 of a suit: 9 people to guarantee 2 share a birth month: 13 students so that 5 share a grade (grades O, A+, A, B+, P, F): 25 minimum in the fullest month for 100 birthdays: 9
Tree diagrams
When later choices depend on earlier ones, draw a tree. Example: how many bit strings of length 4 have no two consecutive 1s? Branching only on allowed bits gives 8 leaves: 0000, 0001, 0010, 0100, 0101, 1000, 1001, 1010. (In general the count is a Fibonacci number — see Recurrence Relations.)
Chapter 11 · Counting
Permutations & Combinations
Ordered arrangements and unordered selections: nPr and nCr, factorials, repetition, arrangements with identical items, circular permutations and stars and bars.
Factorial
n! = n × (n − 1) × … × 2 × 1, with 0! = 1. n! is the number of ways to arrange n distinct objects in a row.
Permutations: order matters
The number of ways to arrange r objects chosen from n distinct objects:
P(n, r) = n! / (n − r)!
Example: gold, silver and bronze among 10 runners: P(10, 3) = 10 × 9 × 8 = 720.
Combinations: order does not matter
The number of ways to choose r objects from n, ignoring order:
C(n, r) = n! / (r! (n − r)!), also written ⁿCᵣ or (n choose r)
Example: choose a 3-person team from 10: C(10, 3) = 720 / 3! = 120. Each team was counted 3! = 6 times among the permutations.
Useful identities: C(n, r) = C(n, n − r); C(n, 0) = C(n, n) = 1; and Pascal's rule C(n, r) = C(n − 1, r − 1) + C(n − 1, r).
from math import comb, perm, factorial
from itertools import permutations, combinations
print("P(10, 3) =", perm(10, 3), " C(10, 3) =", comb(10, 3))
print("arrangements of ABC:", ["".join(p) for p in permutations("ABC")])
print("2-subsets of ABCD: ", ["".join(c) for c in combinations("ABCD", 2)])
print("5-card poker hands: ", comb(52, 5))
print("C(n, r) == C(n, n-r):", comb(12, 5) == comb(12, 7))P(10, 3) = 720 C(10, 3) = 120 arrangements of ABC: ['ABC', 'ACB', 'BAC', 'BCA', 'CAB', 'CBA'] 2-subsets of ABCD: ['AB', 'AC', 'AD', 'BC', 'BD', 'CD'] 5-card poker hands: 2598960 C(n, r) == C(n, n-r): True
Which formula?
| Order matters? | Repetition allowed? | Count |
|---|---|---|
| yes | no | P(n, r) = n!/(n − r)! |
| yes | yes | nʳ |
| no | no | C(n, r) |
| no | yes | C(n + r − 1, r) (stars and bars) |
Arrangements with identical objects
The number of distinct arrangements of n objects where n₁ are alike, n₂ are alike, …:
n! / (n₁! n₂! … nₖ!)
"MISSISSIPPI" has 11 letters: M×1, I×4, S×4, P×2 → 11! / (1! 4! 4! 2!) = 34,650.
from math import factorial
from collections import Counter
def arrangements(word):
result = factorial(len(word))
for count in Counter(word).values():
result //= factorial(count)
return result
for w in ["BOOK", "BANANA", "MISSISSIPPI", "KALVIUM"]:
print(f"{w}: {arrangements(w)}")BOOK: 12 BANANA: 60 MISSISSIPPI: 34650 KALVIUM: 5040
Circular permutations
Arranging n people around a round table, where rotations count as the same: (n − 1)!. If clockwise and anticlockwise are also the same (a necklace): (n − 1)!/2.
Stars and bars
The number of ways to put r identical items into n distinct boxes (or the number of non-negative integer solutions of x₁ + … + xₙ = r) is
C(n + r − 1, r)
Example: distribute 10 identical chocolates among 4 children: C(13, 10) = 286. If each child must get at least one, give one each first, then distribute 6: C(9, 6) = 84.
from math import comb
from itertools import product
r, n = 10, 4
brute = sum(1 for xs in product(range(r + 1), repeat=n) if sum(xs) == r)
print("brute force:", brute, " formula C(n+r-1, r):", comb(n + r - 1, r))brute force: 286 formula C(n+r-1, r): 286
Chapter 12 · Counting
Inclusion–Exclusion & the Binomial Theorem
Count unions without double counting using inclusion–exclusion, apply it to divisibility and derangements, and expand (x + y)ⁿ with the binomial theorem and Pascal's triangle.
Inclusion–exclusion
When sets overlap, adding their sizes counts the overlaps more than once. Correct for it:
|A ∪ B| = |A| + |B| − |A ∩ B|
|A ∪ B ∪ C| = |A| + |B| + |C| − |A ∩ B| − |A ∩ C| − |B ∩ C| + |A ∩ B ∩ C|
In general: add the single sets, subtract all pairwise intersections, add all triple intersections, and so on with alternating signs.
Example: divisibility
How many integers from 1 to 1000 are divisible by 2, 3 or 5?
| Term | Count |
|---|---|
| |A₂| + |A₃| + |A₅| | 500 + 333 + 200 = 1033 |
| − |A₆| − |A₁₀| − |A₁₅| | − 166 − 100 − 66 = −332 |
| + |A₃₀| | + 33 |
| Total | 734 |
N = 1000
direct = sum(1 for n in range(1, N + 1) if n % 2 == 0 or n % 3 == 0 or n % 5 == 0)
formula = (N//2 + N//3 + N//5) - (N//6 + N//10 + N//15) + N//30
print("count directly:", direct, " inclusion-exclusion:", formula)
print("not divisible by 2, 3 or 5:", N - formula)count directly: 734 inclusion-exclusion: 734 not divisible by 2, 3 or 5: 266
Derangements
A derangement is a permutation where no element stays in its original position — n letters put into n envelopes so that every letter is in the wrong one. By inclusion–exclusion:
Dₙ = n! (1 − 1/1! + 1/2! − 1/3! + … + (−1)ⁿ/n!)
from math import factorial
from itertools import permutations
def derangements(n):
return round(factorial(n) * sum((-1) ** k / factorial(k) for k in range(n + 1)))
for n in range(1, 8):
brute = sum(1 for p in permutations(range(n)) if all(p[i] != i for i in range(n)))
print(f"D{n} = {derangements(n)} (brute force {brute}), fraction {derangements(n) / factorial(n):.4f}")D1 = 0 (brute force 0), fraction 0.0000 D2 = 1 (brute force 1), fraction 0.5000 D3 = 2 (brute force 2), fraction 0.3333 D4 = 9 (brute force 9), fraction 0.3750 D5 = 44 (brute force 44), fraction 0.3667 D6 = 265 (brute force 265), fraction 0.3681 D7 = 1854 (brute force 1854), fraction 0.3679
The fraction of permutations that are derangements approaches 1/e ≈ 0.3679.
The binomial theorem
(x + y)ⁿ = Σₖ₌₀ⁿ C(n, k) xⁿ⁻ᵏ yᵏ
The coefficients C(n, k) are the binomial coefficients — the rows of Pascal's triangle, where each entry is the sum of the two above it.
from math import comb
for n in range(7):
print(" " * (2 * (6 - n)) + "".join(f"{comb(n, k):4}" for k in range(n + 1)))
n = 4
terms = [f"{comb(n, k)}x^{n-k}y^{k}" for k in range(n + 1)]
print("(x + y)^4 =", " + ".join(terms)) 1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
(x + y)^4 = 1x^4y^0 + 4x^3y^1 + 6x^2y^2 + 4x^1y^3 + 1x^0y^4Consequences:
- Setting x = y = 1: Σ C(n, k) = 2ⁿ (the number of subsets).
- Setting x = 1, y = −1: Σ (−1)ᵏ C(n, k) = 0.
- The coefficient of x³y⁵ in (x + y)⁸ is C(8, 5) = 56.
- The coefficient of x⁴ in (2x − 3)⁶ is C(6, 2) · 2⁴ · (−3)² = 15 · 16 · 9 = 2160.
Chapter 13 · Counting
Recurrence Relations
Define sequences recursively, model counting problems with recurrences, and solve linear homogeneous recurrences with constant coefficients using the characteristic equation.
A recurrence relation defines each term of a sequence using earlier terms, together with initial conditions.
- aₙ = aₙ₋₁ + 3, a₀ = 2 → 2, 5, 8, 11, … (arithmetic)
- aₙ = 2aₙ₋₁, a₀ = 1 → 1, 2, 4, 8, … (geometric)
- Fₙ = Fₙ₋₁ + Fₙ₋₂, F₀ = 0, F₁ = 1 → Fibonacci
Modelling with recurrences
| Problem | Recurrence |
|---|---|
| Tower of Hanoi moves | Hₙ = 2Hₙ₋₁ + 1, H₁ = 1 |
| Bit strings of length n with no "11" | aₙ = aₙ₋₁ + aₙ₋₂ (end in 0, or end in 01) |
| Compound interest at 8% | Pₙ = 1.08 Pₙ₋₁ |
| Binary search comparisons | T(n) = T(n/2) + 1 |
Linear homogeneous recurrences with constant coefficients
Form: aₙ = c₁aₙ₋₁ + c₂aₙ₋₂ + … + cₖaₙ₋ₖ.
Method for degree 2 (aₙ = c₁aₙ₋₁ + c₂aₙ₋₂):
- Write the characteristic equation: r² − c₁r − c₂ = 0.
- Find its roots.
- Distinct roots r₁, r₂: aₙ = α·r₁ⁿ + β·r₂ⁿ.
- Repeated root r: aₙ = (α + βn)·rⁿ.
- Use the initial conditions to find α and β.
Example 1: distinct roots
aₙ = aₙ₋₁ + 2aₙ₋₂, a₀ = 2, a₁ = 7.
Characteristic equation r² − r − 2 = 0 → (r − 2)(r + 1) = 0 → r = 2, −1. So aₙ = α·2ⁿ + β(−1)ⁿ.
a₀: α + β = 2. a₁: 2α − β = 7. Adding: 3α = 9 → α = 3, β = −1.
aₙ = 3·2ⁿ − (−1)ⁿ.
Example 2: repeated root
aₙ = 6aₙ₋₁ − 9aₙ₋₂, a₀ = 1, a₁ = 6.
r² − 6r + 9 = 0 → (r − 3)² = 0 → r = 3 (double). So aₙ = (α + βn)3ⁿ.
a₀ = α = 1. a₁ = (1 + β)·3 = 6 → β = 1. aₙ = (1 + n)·3ⁿ.
Example 3: Fibonacci (Binet's formula)
r² − r − 1 = 0 → r = (1 ± √5)/2. With F₀ = 0, F₁ = 1:
Fₙ = (φⁿ − ψⁿ)/√5, where φ = (1 + √5)/2 ≈ 1.618 and ψ = (1 − √5)/2
So Fibonacci numbers grow like φⁿ — which is why naive recursive Fibonacci is exponential.
from math import sqrt
def by_recurrence(n, a0, a1, c1, c2):
a, b = a0, a1
for _ in range(n):
a, b = b, c1 * b + c2 * a
return a
phi, psi = (1 + sqrt(5)) / 2, (1 - sqrt(5)) / 2
print("n rec1 3*2^n-(-1)^n rec2 (1+n)3^n fib Binet")
for n in range(8):
r1 = by_recurrence(n, 2, 7, 1, 2)
r2 = by_recurrence(n, 1, 6, 6, -9)
f = by_recurrence(n, 0, 1, 1, 1)
print(f"{n:<3} {r1:<5} {3 * 2**n - (-1)**n:<13} {r2:<5} {(1 + n) * 3**n:<9} {f:<4} {round((phi**n - psi**n) / sqrt(5))}")n rec1 3*2^n-(-1)^n rec2 (1+n)3^n fib Binet 0 2 2 1 1 0 0 1 7 7 6 6 1 1 2 11 11 27 27 1 1 3 25 25 108 108 2 2 4 47 47 405 405 3 3 5 97 97 1458 1458 5 5 6 191 191 5103 5103 8 8 7 385 385 17496 17496 13 13
Non-homogeneous recurrences
For aₙ = c₁aₙ₋₁ + c₂aₙ₋₂ + F(n), the solution is (homogeneous solution) + (one particular solution). For Tower of Hanoi, Hₙ = 2Hₙ₋₁ + 1 has homogeneous part α·2ⁿ and particular solution −1; with H₁ = 1, Hₙ = 2ⁿ − 1.
Divide-and-conquer recurrences
Recurrences like T(n) = aT(n/b) + f(n) come from recursive algorithms. They are solved with the Master Theorem — see Solving Recurrences in Algorithms.
Chapter 14 · Graphs, Number Theory & Algebra
Graph Theory
Graphs as mathematics: simple graphs and multigraphs, degree and the handshake lemma, special graphs, isomorphism, Euler and Hamilton paths, planarity and graph colouring.
A graph G = (V, E) has a set of vertices V and a set of edges E, each joining two vertices. Graph theory began in 1736 when Leonhard Euler proved that nobody could walk across all seven bridges of Königsberg exactly once.
Kinds of graphs
| Kind | Allows |
|---|---|
| Simple graph | undirected edges, no loops, no multiple edges |
| Multigraph | multiple edges between the same pair |
| Pseudograph | loops and multiple edges |
| Directed graph | edges with direction (arcs) |
| Weighted graph | a number on each edge |
Degree and the handshake lemma
The degree deg(v) is the number of edges at v (a loop counts twice). In every undirected graph:
Σ deg(v) = 2|E|
So the number of odd-degree vertices is always even. In a directed graph, Σ in-degree = Σ out-degree = |E|.
Special graphs
| Graph | Description | Edges |
|---|---|---|
| Complete graph Kₙ | every pair of vertices adjacent | n(n − 1)/2 |
| Cycle Cₙ | a single ring of n vertices | n |
| Wheel Wₙ | a cycle Cₙ plus a hub joined to all | 2n |
| Hypercube Qₙ | vertices are n-bit strings; edges join strings differing in one bit | n·2ⁿ⁻¹ |
| Bipartite | vertices split into two sets, edges only between sets | — |
| Complete bipartite Kₘ,ₙ | every vertex of one side joined to every vertex of the other | m·n |
A graph is bipartite if and only if it has no odd-length cycle — checkable by 2-colouring with BFS.
Isomorphism
Two graphs are isomorphic if there is a bijection between their vertices that preserves adjacency — they are the same graph drawn differently. Isomorphic graphs share every invariant: number of vertices and edges, degree sequence, number of cycles of each length, connectedness. Different invariants prove two graphs are not isomorphic; equal invariants do not prove that they are.
Paths, circuits and connectivity
- A path is a sequence of edges from one vertex to another; a circuit (cycle) returns to its start.
- A graph is connected if every pair of vertices has a path between them.
- A cut vertex (articulation point) or bridge is a vertex or edge whose removal disconnects the graph.
Euler paths and circuits
An Euler circuit uses every edge exactly once and returns to the start; an Euler path uses every edge once without returning.
A connected graph has an Euler circuit iff every vertex has even degree. It has an Euler path (but no circuit) iff it has exactly two odd-degree vertices — the path starts at one and ends at the other.
Königsberg had four land masses with degrees 5, 3, 3, 3 — four odd vertices, so no Euler path exists.
from collections import Counter
def euler_status(edges):
deg = Counter()
for u, v in edges:
deg[u] += 1
deg[v] += 1
odd = [v for v in deg if deg[v] % 2]
if not odd:
return "Euler circuit exists"
if len(odd) == 2:
return f"Euler path from {odd[0]} to {odd[1]}"
return f"neither ({len(odd)} odd vertices)"
konigsberg = [("A","B"), ("A","B"), ("A","C"), ("A","C"), ("A","D"), ("B","D"), ("C","D")]
house = [(1,2), (2,3), (3,4), (4,1), (1,5), (2,5), (1,3), (2,4)]
square = [(1,2), (2,3), (3,4), (4,1)]
for name, g in [("Konigsberg", konigsberg), ("envelope house", house), ("square", square)]:
print(f"{name:15} {euler_status(g)}")Konigsberg neither (4 odd vertices) envelope house Euler path from 3 to 4 square Euler circuit exists
Hamilton paths and circuits
A Hamilton circuit visits every vertex exactly once and returns to the start. Unlike the Euler case, no simple test is known — deciding whether one exists is NP-complete. Sufficient conditions exist: Dirac's theorem says a simple graph with n ≥ 3 vertices where every vertex has degree ≥ n/2 has a Hamilton circuit.
| Euler | Hamilton | |
|---|---|---|
| Visits every | edge once | vertex once |
| Easy test | yes — degrees | no — NP-complete |
| Application | route inspection, drawing without lifting the pen | travelling salesperson |
Planar graphs
A graph is planar if it can be drawn without edges crossing. For a connected planar graph, Euler's formula holds:
V − E + F = 2 (F = number of faces, including the outer one)
Consequence: a connected simple planar graph with V ≥ 3 has E ≤ 3V − 6. K₅ has E = 10 > 3·5 − 6 = 9, so K₅ is not planar. By Kuratowski's theorem, a graph is planar iff it contains no subdivision of K₅ or K₃,₃.
Graph colouring
A proper colouring gives adjacent vertices different colours; the minimum number of colours is the chromatic number χ(G).
- χ(Kₙ) = n; χ(Cₙ) = 2 if n is even, 3 if odd; χ of a bipartite graph = 2.
- Four colour theorem (1976, proved with a computer): every planar graph has χ ≤ 4.
- Applications: exam timetabling (subjects sharing a student cannot share a slot), register allocation in compilers, frequency assignment.
Chapter 15 · Graphs, Number Theory & Algebra
Trees
Trees in discrete mathematics: characterisations, rooted and m-ary trees with their counting formulas, spanning trees, Cayley's formula and prefix codes.
A tree is a connected, acyclic undirected graph. Trees are the most important graphs in computer science: file systems, parse trees, decision trees, search trees, heaps, tries.
Equivalent characterisations
For a graph T with n vertices, these are all equivalent — any one of them makes T a tree:
- T is connected and has no cycles.
- There is a unique simple path between every pair of vertices.
- T is connected and has exactly n − 1 edges.
- T has no cycles and has exactly n − 1 edges.
- T is connected, but removing any edge disconnects it (every edge is a bridge).
- T has no cycles, but adding any edge creates exactly one cycle.
A graph with no cycles that may be disconnected is a forest — a collection of trees. A forest with n vertices and c components has n − c edges.
Rooted and m-ary trees
Choosing one vertex as the root gives every edge a direction away from it, with the usual parent, child, leaf, level and height terms (see Trees & Binary Trees in Data Structures).
An m-ary tree gives each internal vertex at most m children; a full m-ary tree gives each internal vertex exactly m children.
| For a full m-ary tree with… | |
|---|---|
| i internal vertices | n = mi + 1 vertices, l = (m − 1)i + 1 leaves |
| n vertices | i = (n − 1)/m internal, l = ((m − 1)n + 1)/m leaves |
| l leaves | n = (ml − 1)/(m − 1), i = (l − 1)/(m − 1) |
An m-ary tree of height h has at most mʰ leaves, so a tree with l leaves has height at least ⌈log_m l⌉.
def full_mary(m, internal):
n = m * internal + 1
leaves = (m - 1) * internal + 1
return n, leaves
# A knockout chess tournament: each match (internal vertex) has 2 players (children)
players = 64
matches = (players - 1) // (2 - 1)
print(f"{players} players need {matches} matches")
print("full ternary tree with 10 internal vertices (vertices, leaves):", full_mary(3, 10))
# Chain letter: each person sends to 4 others; 100 people never send (leaves)
m, l = 4, 100
print(f"people involved: {(m * l - 1) // (m - 1)}, senders: {(l - 1) // (m - 1)}")64 players need 63 matches full ternary tree with 10 internal vertices (vertices, leaves): (31, 21) people involved: 133, senders: 33
Spanning trees
A spanning tree of a connected graph G is a subgraph that is a tree and includes every vertex of G. Every connected graph has one (delete cycle edges until none are left); BFS and DFS each produce one. With weights, the cheapest is the minimum spanning tree — Kruskal and Prim.
Cayley's formula: the complete graph Kₙ has nⁿ⁻² different spanning trees (equivalently, there are nⁿ⁻² labelled trees on n vertices). K₄ has 4² = 16.
from itertools import combinations
def is_tree(n, edges):
parent = list(range(n))
def find(x):
while parent[x] != x:
x = parent[x]
return x
for u, v in edges:
ru, rv = find(u), find(v)
if ru == rv:
return False # a cycle
parent[ru] = rv
return len(edges) == n - 1 # n - 1 edges and no cycle: connected tree
for n in range(2, 6):
all_edges = list(combinations(range(n), 2))
count = sum(1 for es in combinations(all_edges, n - 1) if is_tree(n, es))
print(f"K{n}: {count} spanning trees, Cayley n^(n-2) = {n ** (n - 2)}")K2: 1 spanning trees, Cayley n^(n-2) = 1 K3: 3 spanning trees, Cayley n^(n-2) = 3 K4: 16 spanning trees, Cayley n^(n-2) = 16 K5: 125 spanning trees, Cayley n^(n-2) = 125
Prefix codes and decision trees
- Binary trees whose leaves are symbols give prefix codes — the basis of Huffman coding.
- A decision tree for sorting n items has n! leaves, so its height (worst-case comparisons) is at least log₂(n!) = Ω(n log n) — the proof that comparison sorting cannot beat O(n log n).
Chapter 16 · Graphs, Number Theory & Algebra
Number Theory
Divisibility, primes and the fundamental theorem of arithmetic, GCD and LCM with Euclid's algorithm, modular arithmetic, congruences, modular inverses, fast exponentiation and an RSA preview.
Number theory studies the integers. Once called the "purest" mathematics, it now secures every HTTPS connection and UPI payment through cryptography.
Divisibility
a divides b (written a | b) if b = a·k for some integer k. Examples: 3 | 12, 7 ∤ 12.
Division algorithm: for integers a and d > 0, there are unique q and r with a = d·q + r and 0 ≤ r < d. Here q = a div d and r = a mod d.
Primes
A prime is an integer p > 1 whose only positive divisors are 1 and p. Every integer > 1 is either prime or composite.
Fundamental theorem of arithmetic: every integer > 1 can be written as a product of primes in exactly one way (up to order): 360 = 2³ · 3² · 5.
There are infinitely many primes (Euclid's proof is in the proofs lesson). To test n, try divisors up to √n; to list all primes up to n, use the Sieve of Eratosthenes:
def sieve(n):
is_prime = [True] * (n + 1)
is_prime[0] = is_prime[1] = False
for p in range(2, int(n ** 0.5) + 1):
if is_prime[p]:
for multiple in range(p * p, n + 1, p): # cross off multiples of p
is_prime[multiple] = False
return [i for i, ok in enumerate(is_prime) if ok]
def factorise(n):
factors, d = {}, 2
while d * d <= n:
while n % d == 0:
factors[d] = factors.get(d, 0) + 1
n //= d
d += 1
if n > 1:
factors[n] = factors.get(n, 0) + 1
return factors
print(sieve(60))
for n in (360, 1001, 97, 2026):
print(n, "=", " · ".join(f"{p}^{e}" if e > 1 else str(p) for p, e in factorise(n).items()))[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59] 360 = 2^3 · 3^2 · 5 1001 = 7 · 11 · 13 97 = 97 2026 = 2 · 1013
GCD and LCM
- gcd(a, b): the largest integer dividing both. gcd(84, 36) = 12.
- lcm(a, b): the smallest positive integer divisible by both. lcm(4, 6) = 12.
- gcd(a, b) · lcm(a, b) = a · b.
- a and b are coprime (relatively prime) if gcd(a, b) = 1.
Euclid's algorithm: gcd(a, b) = gcd(b, a mod b), and gcd(a, 0) = a. It takes O(log min(a, b)) steps.
Bézout's identity: there exist integers s, t with gcd(a, b) = s·a + t·b. The extended Euclidean algorithm finds them.
def extended_gcd(a, b):
if b == 0:
return a, 1, 0
g, s, t = extended_gcd(b, a % b)
return g, t, s - (a // b) * t
a, b = 252, 198
x, y = a, b
while y:
print(f"gcd({x}, {y})", end=" = ")
x, y = y, x % y
print(f"gcd({x}, 0) = {x}")
g, s, t = extended_gcd(a, b)
print(f"Bezout: {g} = {s}*{a} + {t}*{b}")
print("lcm =", a * b // g)gcd(252, 198) = gcd(198, 54) = gcd(54, 36) = gcd(36, 18) = gcd(18, 0) = 18 Bezout: 18 = 4*252 + -5*198 lcm = 2772
Modular arithmetic
a ≡ b (mod m) — "a is congruent to b modulo m" — means m | (a − b): a and b leave the same remainder when divided by m. Clock arithmetic is mod 12; days of the week are mod 7.
Congruences can be added and multiplied: if a ≡ b and c ≡ d (mod m), then a + c ≡ b + d and a·c ≡ b·d (mod m). So you can reduce at every step:
(a + b) mod m = ((a mod m) + (b mod m)) mod m (a · b) mod m = ((a mod m) · (b mod m)) mod m
Fast modular exponentiation
Compute aᵉ mod m by repeated squaring in O(log e) multiplications:
def mod_pow(a, e, m):
result = 1
a %= m
while e > 0:
if e & 1: # this bit of the exponent is set
result = result * a % m
a = a * a % m # square for the next bit
e >>= 1
return result
print(mod_pow(3, 200, 13), pow(3, 200, 13)) # Python's pow does the same
print("last 3 digits of 7^2026:", mod_pow(7, 2026, 1000))
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
print("100 days after a Saturday is a", days[(days.index("Sat") + 100) % 7])9 9 last 3 digits of 7^2026: 649 100 days after a Saturday is a Mon
Modular inverses
The inverse of a modulo m is a number a⁻¹ with a·a⁻¹ ≡ 1 (mod m). It exists iff gcd(a, m) = 1, and the extended Euclidean algorithm finds it. Example: 3⁻¹ mod 7 = 5, since 3·5 = 15 ≡ 1.
Fermat's little theorem: if p is prime and p ∤ a, then aᵖ⁻¹ ≡ 1 (mod p). So a⁻¹ ≡ aᵖ⁻² (mod p).
Applications
| Application | Number theory used |
|---|---|
| Hash functions h(k) = k mod m | division, choosing prime m |
| ISBN and Aadhaar-style check digits | weighted sums mod 11 / Verhoeff checks |
| Pseudo-random generators xₙ₊₁ = (a·xₙ + c) mod m | linear congruences |
| RSA encryption | n = p·q for large primes; e·d ≡ 1 mod (p−1)(q−1); encrypt c = mᵉ mod n |
p, q = 61, 53 # toy RSA (real keys use primes with hundreds of digits)
n, phi = p * q, (p - 1) * (q - 1)
e = 17 # public exponent, coprime with phi
d = pow(e, -1, phi) # private exponent: modular inverse (Python 3.8+)
message = 65
cipher = pow(message, e, n)
print(f"n={n}, e={e}, d={d}")
print(f"encrypt {message} -> {cipher}, decrypt -> {pow(cipher, d, n)}")n=3233, e=17, d=2753 encrypt 65 -> 2790, decrypt -> 65
Chapter 17 · Graphs, Number Theory & Algebra
Boolean Algebra
The algebra of 0 and 1 behind every circuit: Boolean operations and laws, duality, Boolean functions, sum-of-products and product-of-sums forms, logic gates and Karnaugh-map simplification.
Boolean algebra works with two values, 0 and 1, and three operations:
| Operation | Notation | Logic | Gate |
|---|---|---|---|
| Complement | x′ or x̄ | NOT | inverter |
| Boolean sum | x + y | OR | OR gate |
| Boolean product | x · y or xy | AND | AND gate |
It is propositional logic in algebraic dress (T = 1, F = 0) and set algebra in disguise (∪ = +, ∩ = ·). George Boole introduced it in 1854; Claude Shannon showed in 1937 that it describes switching circuits — the birth of digital electronics.
Laws
| Law | Sum form | Product form |
|---|---|---|
| Identity | x + 0 = x | x · 1 = x |
| Domination | x + 1 = 1 | x · 0 = 0 |
| Idempotent | x + x = x | x · x = x |
| Complement | x + x′ = 1 | x · x′ = 0 |
| Involution | (x′)′ = x | |
| Commutative | x + y = y + x | xy = yx |
| Associative | x + (y + z) = (x + y) + z | x(yz) = (xy)z |
| Distributive | x(y + z) = xy + xz | x + yz = (x + y)(x + z) |
| Absorption | x + xy = x | x(x + y) = x |
| De Morgan | (x + y)′ = x′y′ | (xy)′ = x′ + y′ |
Duality: swap + with ·, and 0 with 1, in any identity and you get another valid identity. That is why the laws come in pairs.
Boolean functions and normal forms
A Boolean function of n variables maps each of the 2ⁿ input combinations to 0 or 1. There are 2^(2ⁿ) such functions.
- Sum of products (SOP / DNF): OR together a minterm (an AND of all variables, complemented where 0) for every row where f = 1.
- Product of sums (POS / CNF): AND together a maxterm for every row where f = 0.
Example: the majority function of three inputs (1 when at least two inputs are 1):
| x | y | z | f |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 0 | 1 | 0 |
| 0 | 1 | 0 | 0 |
| 0 | 1 | 1 | 1 |
| 1 | 0 | 0 | 0 |
| 1 | 0 | 1 | 1 |
| 1 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 |
SOP: f = x′yz + xy′z + xyz′ + xyz, which simplifies to f = xy + yz + xz.
Logic gates
NAND and NOR are each functionally complete: any Boolean function can be built from NAND gates alone. For example, x′ = (x·x)′ and x·y = ((x·y)′)′.
Simplifying with Karnaugh maps
A Karnaugh map (K-map) arranges a function's truth table in a grid whose rows and columns are in Gray code order (00, 01, 11, 10), so neighbouring cells differ in exactly one variable. To simplify:
- Write a 1 in each cell where f = 1.
- Circle rectangular groups of 1, 2, 4 or 8 adjacent 1s (groups may wrap around the edges), as large as possible.
- Each group becomes one product term: keep only the variables that do not change within the group.
- OR the terms together.
For the majority function (rows x, columns yz):
| x \ yz | 00 | 01 | 11 | 10 |
|---|---|---|---|---|
| 0 | 0 | 0 | 1 | 0 |
| 1 | 0 | 1 | 1 | 1 |
Three overlapping pairs give yz (column 11), xz (cells 101, 111) and xy (cells 111, 110): f = xy + yz + xz.
Verify any simplification by comparing truth tables:
from itertools import product
def majority_sop(x, y, z):
return (not x and y and z) or (x and not y and z) or (x and y and not z) or (x and y and z)
def majority_simple(x, y, z):
return (x and y) or (y and z) or (x and z)
same = all(bool(majority_sop(*v)) == bool(majority_simple(*v)) for v in product([0, 1], repeat=3))
print("simplified circuit matches:", same)
def nand(a, b): return int(not (a and b))
def xor_from_nand(a, b):
m = nand(a, b)
return nand(nand(a, m), nand(b, m)) # XOR built from four NAND gates
print("XOR from NANDs:", [xor_from_nand(a, b) for a, b in product([0, 1], repeat=2)])simplified circuit matches: True XOR from NANDs: [0, 1, 1, 0]
Half adder
Adding two bits produces a sum bit S = x ⊕ y and a carry bit C = x · y — one XOR gate and one AND gate. Chaining full adders gives the circuits that add numbers inside every CPU (see Logic Gates in Foundations).
Chapter 18 · Graphs, Number Theory & Algebra
Discrete Mathematics Formula Sheet
Every rule, law and formula from Discrete Mathematics on one page — logic, proofs, sets, relations, functions, counting, recurrences, graphs, trees, number theory and Boolean algebra.
Logic
| Concept | Formula |
|---|---|
| Implication | p → q ≡ ¬p ∨ q ≡ ¬q → ¬p |
| Negated implication | ¬(p → q) ≡ p ∧ ¬q |
| Biconditional | p ↔ q ≡ (p → q) ∧ (q → p) |
| De Morgan | ¬(p ∧ q) ≡ ¬p ∨ ¬q; ¬(p ∨ q) ≡ ¬p ∧ ¬q |
| Quantifier negation | ¬∀x P(x) ≡ ∃x ¬P(x); ¬∃x P(x) ≡ ∀x ¬P(x) |
| Modus ponens / tollens | p, p → q ⊢ q; ¬q, p → q ⊢ ¬p |
Proof methods
Direct · contrapositive · contradiction · cases · counterexample · induction (base + step) · strong induction.
Sets and functions
| Concept | Formula |
|---|---|
| Power set | |P(A)| = 2ⁿ |
| Cartesian product | |A × B| = |A|·|B| |
| Inclusion–exclusion (2 sets) | |A ∪ B| = |A| + |B| − |A ∩ B| |
| Inclusion–exclusion (3 sets) | Σ|A| − Σ|A ∩ B| + |A ∩ B ∩ C| |
| Functions A → B | nᵐ (m = |A|, n = |B|) |
| Injections (m ≤ n) | n!/(n − m)! |
| Bijections | n! |
Relations
| Type | Properties |
|---|---|
| Equivalence relation | reflexive, symmetric, transitive → partitions into classes |
| Partial order | reflexive, antisymmetric, transitive → Hasse diagram |
| Lattice | every pair has a lub (join) and glb (meet) |
Counting
| Situation | Count |
|---|---|
| Ordered, no repetition | P(n, r) = n!/(n − r)! |
| Ordered, with repetition | nʳ |
| Unordered, no repetition | C(n, r) = n!/(r!(n − r)!) |
| Unordered, with repetition | C(n + r − 1, r) |
| Arrangements with identical items | n!/(n₁! n₂! … nₖ!) |
| Circular arrangements | (n − 1)! |
| Derangements | Dₙ = n! Σ (−1)ᵏ/k! ≈ n!/e |
| Pigeonhole | N objects, k boxes → some box has ≥ ⌈N/k⌉ |
| Binomial theorem | (x + y)ⁿ = Σ C(n, k) xⁿ⁻ᵏ yᵏ |
| Pascal's rule | C(n, r) = C(n − 1, r − 1) + C(n − 1, r) |
Recurrences
| Characteristic roots | Solution of aₙ = c₁aₙ₋₁ + c₂aₙ₋₂ |
|---|---|
| distinct r₁, r₂ | aₙ = αr₁ⁿ + βr₂ⁿ |
| repeated r | aₙ = (α + βn)rⁿ |
| Fibonacci | Fₙ = (φⁿ − ψⁿ)/√5 |
Graphs and trees
| Fact | Formula |
|---|---|
| Handshake lemma | Σ deg(v) = 2|E| |
| Edges of Kₙ | n(n − 1)/2 |
| Euler circuit | connected, all degrees even |
| Euler path | connected, exactly two odd vertices |
| Euler's formula (planar) | V − E + F = 2; E ≤ 3V − 6 |
| Tree | n vertices, n − 1 edges, unique paths |
| Full m-ary tree | n = mi + 1, l = (m − 1)i + 1 |
| Cayley | nⁿ⁻² labelled trees |
| Four colour theorem | planar graphs: χ ≤ 4 |
Number theory
| Fact | Formula |
|---|---|
| Division algorithm | a = dq + r, 0 ≤ r < d |
| Euclid | gcd(a, b) = gcd(b, a mod b) |
| GCD–LCM | gcd(a, b)·lcm(a, b) = ab |
| Bézout | gcd(a, b) = sa + tb |
| Inverse exists | a⁻¹ mod m ⇔ gcd(a, m) = 1 |
| Fermat | aᵖ⁻¹ ≡ 1 (mod p), p prime, p ∤ a |
Boolean algebra
| Law | Identity |
|---|---|
| Absorption | x + xy = x; x(x + y) = x |
| De Morgan | (x + y)′ = x′y′; (xy)′ = x′ + y′ |
| Consensus | xy + x′z + yz = xy + x′z |
| Functionally complete | {NAND} or {NOR} alone |