Search Tech Journey

Find topics, journeys and posts

6-month learning plan16 / 130
back to blog
mathbeginner 55m read

S016 · Discrete Math — Sets, Logic, Combinatorics, Graphs

The vocabulary of computer science: sets, boolean logic, counting arguments, and graph theory — the four discrete-math tools you'll use in every subsequent session.

📐MathM02 · Math Foundations· Session 016 of 130 90 min

🎯 Fluently translate ‘programmer English’ into set / logic / graph notation, and back — enough to read a paper, a formal spec, or a database query plan without stalling.

Why this session exists

The interesting part of computer science lives in the discrete world: whole numbers, yes/no answers, choices, graphs, states. When you write WHERE a AND (b OR NOT c) you're doing propositional logic. When you index a dict you're using a partial function on a set. When you write a permission system you're doing set algebra. When you route packets you're walking a graph. Discrete math is the notation professionals use to think precisely about these things — and to write down algorithms in a way that other people (and mypy, and formal-verification tools) can actually verify.

You will be able to
  • Read and write set-builder notation ({x ∈ S | P(x)}), and translate to Python comprehensions and SQL.
  • Read a propositional-logic expression (¬, ∧, ∨, →, ⇔) fluently and simplify with De Morgan's laws.
  • Count arrangements with permutation and combination formulas (nPr, nCr) — and know which one to use.
  • Distinguish directed vs undirected, weighted vs unweighted graphs and pick the right representation.
  • Recognise which of BFS, DFS, or topological sort applies to a given real-world problem.

Prerequisites

  • S015 · Big-O — you'll count operations on sets and graphs.
  • High-school algebra. That's it — no calculus.


(a) Intuition · 5 min

Four vocabularies for four kinds of question
🌍 Real world

A city planner asks four completely different questions in a single meeting: "Which neighbourhoods have both schools and hospitals?" (sets) · "If we build a bridge, will the traffic-light logic still work?" (logic) · "How many bus routes are possible between five hubs?" (combinatorics) · "What's the shortest route from A to B?" (graphs). Same city, four vocabularies.

You'll do the same in code. A search filter is a set. An access-control rule is logic. A pagination-key generator is combinatorics. A dependency resolver is a graph. Different problems, one language once you learn each dialect.

💻 Code world

The four sub-fields aren't optional — they're the alphabet of everything downstream. SQL is applied set theory. Type systems are applied propositional logic. Machine-learning cost of a feature explosion is combinatorics. Every dependency graph, permission graph, social graph, model graph is graph theory.

You don't need to become a mathematician. You need to be fluent enough to read when someone writes it down.

The four sub-fields, in one line each

Discrete math for programmers · the four dialects
  • Sets — collections without order or duplicates. Union ∪, intersection ∩, difference ∖, subset ⊆.
  • Logic — propositions and connectives. ¬ (not), ∧ (and), ∨ (or), → (implies), ⇔ (iff).
  • Combinatorics — counting arrangements. Permutations P(n,r) = n!/(n-r)!, combinations C(n,r) = n!/(r!(n-r)!).
  • Graphs — vertices connected by edges. Directed vs undirected, weighted vs unweighted, cyclic vs DAG.

A quick history

  1. 1874
    Cantor · set theory
    Georg Cantor invents set theory, discovers different sizes of infinity. Foundations shake for 50 years.
  2. 1854
    Boole · logic as algebra
    George Boole writes The Laws of Thought — the algebra that becomes every digital circuit.
  3. 1736
    Euler · Seven Bridges of Königsberg
    Leonhard Euler solves a Prussian-town puzzle and accidentally invents graph theory.
  4. 1936
    Turing · computable numbers
    Alan Turing formalises what an algorithm IS using an abstract machine — the seed of CS.
  5. 1959
    Dijkstra's shortest-path algorithm
    Edsger Dijkstra thinks it up in 20 minutes at a café. Every routing system today descends from it.
  6. 1970s
    Codd · relational algebra = set theory for DBs
    Ted Codd shows a database is just sets of tuples with set-algebra operators. SQL is born.

(b) Visual walkthrough · 15 min

Sets: three operations, three Venn diagrams

Concrete: if A = {1,2,3} and B = {3,4,5} then A ∪ B = {1,2,3,4,5}, A ∩ B = {3}, A ∖ B = {1,2}.

Python translation:

A, B = {1,2,3}, {3,4,5}
A | B      # union
A & B      # intersection
A - B      # difference
A ^ B      # symmetric difference  {1,2,4,5}
A <= B     # subset check

Logic: connectives and De Morgan's laws

Boolean connectives and their code equivalents

¬p — not p
Python: `not p`. SQL: `NOT p`. Circuit: NOT gate.
not
p ∧ q — p and q
Both true. Python: `p and q`. SQL: `p AND q`. Short-circuits left-to-right.
and
p ∨ q — p or q
At least one true. Python: `p or q`. SQL: `p OR q`. Short-circuits.
or
p → q — p implies q
Equivalent to `¬p ∨ q`. False only when p is true and q is false. `not p or q`.
implies
p ⇔ q — p iff q
Same truth value. Python: `p == q` for booleans. Aliases: `p ≡ q`.
iff

De Morgan's laws — the two identities that unblock 90% of boolean bugs:

¬(p ∧ q)  ≡  ¬p ∨ ¬q      (NOT of AND = OR of NOTs)
¬(p ∨ q)  ≡  ¬p ∧ ¬q      (NOT of OR  = AND of NOTs)

Combinatorics decision tree

1
Are you picking (choosing a subset) or arranging (choosing an order)?

Picking → combination. Arranging → permutation.

2
With or without replacement?

Without = each item used at most once (the usual case). With = same item can appear multiple times.

3
Does the whole population have to be used?

‘Arrange all n people in a line’ is n!. ‘Arrange r of n people’ is P(n,r) = n!/(n-r)!.

4
Are items distinguishable?

If two items are identical, divide out the ways they could be permuted (multinomial coefficient).

Formulas cheat sheet:

Permutation P(n,r)

Order matters

  • Number of ways to arrange r of n distinct items
  • P(n,r) = n! / (n-r)!
  • P(5,3) = 60 — sprint-podium finishes for 5 runners
  • Python: math.perm(5, 3)
Combination C(n,r)

Order doesn't matter

  • Number of ways to CHOOSE r of n distinct items
  • C(n,r) = n! / (r! (n-r)!) — the ‘binomial coefficient’
  • C(52,5) = 2 598 960 — five-card poker hands
  • Python: math.comb(52, 5)
With replacement

Same item reusable

  • Ordered w/ repl: n^r — 10⁴ = 10 000 four-digit PINs
  • Unordered w/ repl: C(n+r-1, r) — stars-and-bars
  • Passwords, hash outputs, IP subnets
Multiset perms

Some items identical

  • n! / (n₁! · n₂! · … · nk!)
  • ‘MISSISSIPPI’ = 11! / (4!·4!·2!·1!) = 34 650 anagrams
  • Rare in day-to-day code; useful in probability

Graph representations

Two ways to store the same graph:

# Adjacency list — O(V + E) space, best for sparse graphs
graph = {
    "A": ["B", "C"],
    "B": ["D"],
    "C": ["D", "E"],
    "D": ["E"],
    "E": [],
}
 
# Adjacency matrix — O(V^2) space, best for dense graphs / fast edge lookup
V = ["A", "B", "C", "D", "E"]
M = [[0,1,1,0,0],
     [0,0,0,1,0],
     [0,0,0,1,1],
     [0,0,0,0,1],
     [0,0,0,0,0]]

Choose the representation:

Adjacency list

Sparse graphs, most real graphs

  • O(V+E) memory
  • O(degree) to enumerate neighbours
  • O(V+E) full traversal
  • Web graph, social graph, roads
Adjacency matrix

Dense graphs, edge-lookup-heavy

  • O(V²) memory
  • O(1) ‘is A→B an edge?’
  • O(V²) full traversal
  • Small dense graphs, ML on graphs, image grids

Common misconception
✗ What most people think

"Discrete math is academic. Sets, logic, combinatorics, graphs — I use SQL and Python every day and never need any of it."

✓ What is actually true

You use it constantly, unnamed. A SQL JOIN is a filtered Cartesian product, DISTINCT is set semantics, GROUP BY is a partition into equivalence classes, a DAG scheduler is graph theory, a hash partition is modular arithmetic, and every WHERE clause is propositional logic that the optimiser rewrites using De Morgan's laws. Not knowing the names is what makes the failures look mysterious.

Why the myth is so sticky

Because the abstractions are genuinely good — they let you be productive without the theory, so the theory looks optional. It stops being optional at precisely the moments that cost the most. A NOT IN subquery that silently returns zero rows when the subquery contains one NULL is three-valued logic, and it is completely inexplicable under two-valued reasoning. A left join that produced more rows than the left table is a non-unique key breaking the function property you assumed. A pipeline that deadlocks is a cycle in a graph you believed was acyclic. Each of these looks like a database quirk and is in fact a theorem you didn't know you were relying on.

Prove it to yourself

Three-valued logic, in the wild — this returns nothing, and nothing warns you:

-- ids = (1, 2, NULL)
SELECT * FROM t WHERE t.id NOT IN (1, 2, NULL);   -- zero rows, always

-- because NOT IN expands to:
--   id <> 1 AND id <> 2 AND id <> NULL
-- and (anything <> NULL) is UNKNOWN, never TRUE
-- so the AND can never be TRUE. Use NOT EXISTS instead.
From first principles
Start with the question

Why does the pigeonhole principle guarantee that every hash function has collisions — and why does that force every hash table to carry a collision-handling mechanism it can never remove?

  1. 1
    A hash function maps an input domain (all possible keys) to a finite codomain (buckets, or fixed-width hash values).
    forced by · the output must fit in a fixed number of bits to be usable as an index or a stored digest
  2. 2
    The domain of possible keys is vastly larger than the codomain — unbounded, for strings — while the codomain has at most 2^k elements.
    forced by · keys are arbitrary-length data; hashes are fixed-length by construction
  3. 3
    By pigeonhole, mapping more items than containers forces at least two items into some container. With an infinite domain, every bucket has infinitely many keys mapping to it.
    forced by · you cannot injectively map a larger set into a smaller one — this is arithmetic, not a property of the function
  4. 4
    Therefore collisions are not a defect of a bad hash function; they are unavoidable for every hash function that will ever exist. A good hash only makes them uniformly distributed rather than rare.
    forced by · the counting argument is independent of how the function computes its output
  5. 5
    So a hash table must always include a resolution strategy — chaining or open addressing — and its O(1) guarantee is conditional on collisions staying rare, which requires both a good hash and a bounded load factor.
    forced by · with all n keys in one bucket, lookup degenerates to a linear scan
⇒ Therefore

Therefore "O(1) average" for hash tables carries a hidden assumption about key distribution, and that assumption is attackable. This is exactly the basis of hash-flooding denial-of-service attacks, and why Python randomises string hashing per process by default (PYTHONHASHSEED).

And note what this predicts in data engineering: hash-partitioning a dataset on a skewed key cannot distribute it evenly, no matter how good the hash — if 40% of rows share one customer id, they share one hash and therefore one partition. That is a straight consequence of the same counting argument, and it is why the fix is salting the key rather than changing the hash function. It also predicts why birthday-bound reasoning matters for ID generation: collision probability rises with the square root of the space, not linearly.

Mental modelSets, relations, graphs — the three shapes of data

Almost every data structure and query is one of three things. A set: unordered, no duplicates, membership is the only question. A relation: a set of tuples, i.e. a table, and every join, filter, and projection is an operation on relations. A graph: nodes and edges, which is what you have whenever things reference other things — dependencies, lineage, foreign keys, retries.

Naming which one you are holding tells you immediately which properties you get for free and which you must prove. Sets give you dedup and commutative union. Relations give you the whole algebra SQL implements. Graphs give you reachability and cycles — and the question "is this acyclic?" that scheduling depends on.

  • A join is a Cartesian product with a filter. Row explosion is not a bug; it is the product doing exactly what it does when the join key isn't unique on the side you assumed.
  • NULL is not a value, it's "unknown", so SQL logic is three-valued. NULL = NULL is UNKNOWN, which is why NOT IN with NULLs is empty and why GROUP BY nonetheless groups NULLs together.
  • De Morgan's laws are how you rewrite a filter you can't reason about: NOT (A AND B)(NOT A) OR (NOT B). Query planners do this constantly, and so should you when a predicate stops making sense.
  • Any dependency structure is a graph; if it must be executed, it must be acyclic. Topological sort is the only correct execution order, and "cannot topologically sort" is what a cycle error actually means.
🔔 Fires when you see

Fire this model the moment you see: a join that multiplied your row count · NOT IN returning nothing · a COUNT(*) and COUNT(col) disagreeing · a scheduler reporting a circular dependency · one partition taking 10× longer than the rest · a dedup that didn't dedup because the key had NULLs.

The tradeoff

You need to test membership of billions of keys — "have I seen this id before?" Exact set, sorted structure, or a probabilistic filter?

Exact hash set
+ you gain zero error, O(1) average lookup, and trivially correct — no threshold to tune and no false-positive handling downstream
− you pay memory proportional to the number of and size of keys; at billions of keys this exceeds a single machine, forcing distribution and its coordination cost
pick when the key set fits in memory with headroom, or a wrong answer is unacceptable — deduplicating financial transactions, enforcing uniqueness constraints
Sorted structure / index
+ you gain O(log n) lookup with bounded memory, supports range queries as well as membership, and spills to disk gracefully with sequential access patterns
− you pay slower per lookup than hashing, and you pay the sort or index-build up front; insertions are more expensive than into a hash table
pick when the data exceeds memory, or you need ordering and range predicates too — which is why databases build B-trees rather than hash indexes by default
Bloom filter
+ you gain memory per key measured in a handful of bits rather than the key's own size, so billions of keys fit in a manageable footprint; no false negatives — "not present" is always true
− you pay false positives at a rate you trade against space; no deletion in the basic form; and you cannot enumerate or retrieve what's in it, only query it
pick when a false positive is merely expensive rather than wrong — the canonical use being a pre-filter that avoids a disk or network lookup, where a false positive costs one wasted read
What a senior engineer actually does

The question that decides it is not "how much memory do I have" but "what does a wrong answer cost?" If a false positive triggers a cheap verification step, the probabilistic structure wins massively on memory. If a false positive silently drops a record, no space saving justifies it.

The layered answer is the one production systems actually use: a Bloom filter in front of an exact store, so the filter absorbs the overwhelming majority of negative lookups for a few bits per key, and the exact structure resolves the small fraction that pass. Storage engines are built this way for precisely this reason.


(c) Hands-on · 25 min

You'll compute a small dependency graph, run BFS + DFS + topological sort on it, and turn boolean-condition simplification into a testable function. Save as discrete_demo.py.

"""discrete_demo.py sets, logic, combinatorics, graphs in one file."""from __future__ import annotationsfrom collections import defaultdict, dequefrom itertools import combinations, permutationsfrom math import comb, perm # 1. Sets def demo_sets() -> None: students = {"Alice", "Bob", "Carol", "Dave"} passed_math = {"Alice"

Run it:

python discrete_demo.py

Expected output (BFS/DFS orders depend on insertion order):

SETS both: {'Dave', 'Alice'} | either: {'Alice', 'Bob', 'Carol', 'Dave'}only math: {'Bob'} | exactly one: {'Bob', 'Carol'}failed both: set() LOGIC De Morgan #1 verified for all 4 inputs COUNTING 5-card poker hands: 2598960podium arrangements: 336teams of 2: [('A','B'), ('A','C'), ('A','D'), ('B','C'), ('B','D'), ('C','D')]2-arrangements: [('A','B'),('A','C'),('A','D'),('B','A'),('B','C'),('B','D'),('C','A'),('C','B'),('C','D'),('D','A'),('D','B'),('D','C')] GRAPHS BFS from app: ['app', 'auth', 'ui', 'db', 'crypto', 'design-system']DFS from app: ['app', 'auth', 'crypto', 'ui', 'design-system', 'db']build order: ['app', 'auth', 'ui', 'db', 'crypto', 'design-system']

What each block does

Anatomy of the demo

Set operators on Python sets
&, |, -, ^ are built-in. This IS applied discrete math — SQL joins, permission checks, feature-flag audiences.
sets
prove_de_morgan
A ‘truth-table proof’ — enumerate all 4 input combinations and assert both sides agree. This is how test-driven development meets formal logic.
logic
math.comb / math.perm
Since Python 3.8, exact integer formulas. Use these instead of hand-computing factorials (which overflow floats and hide bugs).
combinatorics
BFS with a deque
collections.deque gives O(1) popleft — a plain list would be O(n). Same algorithm, wrong data structure = 100× slower.
graph-bfs
DFS with an explicit stack
Iterative form avoids Python's recursion limit (1000 by default). For deep dependency graphs this matters.
graph-dfs
Kahn's topological sort
Repeatedly emit any node with 0 remaining incoming edges. If any nodes are left over at the end, you had a cycle. Elegant O(V+E) DAG check.
topo
Try itRecognise ‘which discrete-math tool for which problem’

Match each real-world task to a discrete-math tool:

# 1. Users allowed if they're in group A OR group B, but NOT banned.
# 2. Total number of possible 8-character passwords using a-z A-Z 0-9.
# 3. Given a set of pages a user visited, find pages visited by both users A and B.
# 4. Order in which npm should install packages so no dep is installed after its dependent.
# 5. Find shortest path from London to Berlin on a road network.
# 6. Simplify: NOT (isAdmin AND (isBanned OR NOT hasPaid)).
Answer key
  1. Set + Logic (set membership + boolean combo)
  2. Combinatorics (ordered with replacement: 62⁸ ≈ 218 trillion)
  3. Sets (intersection)
  4. Graph (topological sort on a DAG)
  5. Graph (BFS if unweighted, Dijkstra if weighted)
  6. Logic (De Morgan: NOT isAdmin OR (NOT isBanned AND hasPaid))
💡 Hint · For each of the six tasks below, name the right tool (set / logic / combinatorics / graph) BEFORE looking at the answer key below the code.

(d) Production reality · 15 min

War story LinkedIn · engineering blog· 2015500M+ member social graph
🔥 What broke

LinkedIn's "People You May Know" feature computed 2nd-degree connections by BFS. Naive implementation: for each user, expand to friends, then friends-of-friends, dedupe with a set. Simple enough — until n hit 500 million and a single query took 30 s of database round-trips.

The graph was there. The algorithm was correct. The representation was wrong for the scale.

🧯 The fix
Move the graph into an in-memory adjacency-list service (Voldemort → Espresso → later Pinot for OLAP). Precompute 2nd-degree candidates offline in Hadoop batch jobs. Serve queries from a compact int-keyed adjacency list — a mostly-static representation traversed in microseconds. Latency dropped ~1000×.
🎓 Lesson to steal
Graph theory is trivial on 10 nodes; picking the right representation is the whole game at 10⁹. Adjacency list for sparse graphs, but the language and memory layout matter enormously — an int-indexed compact array in C++ beats a HashMap<String, Set<String>> in Java by orders of magnitude.
Post-mortem
War story Common failure · SQL query with wrong set operatorteam-wide, monthly
🔥 What broke

A data engineer writes SELECT user_id FROM active EXCEPT SELECT user_id FROM banned to find non-banned users. Except banned has NULL user_ids for guests. EXCEPT's handling of NULL is subtle across engines — some treat NULLs as equal, some don't — and the query silently drops different rows on PostgreSQL vs Snowflake vs BigQuery.

🧯 The fix
Use WHERE user_id NOT IN (SELECT ... WHERE user_id IS NOT NULL) — spell out the NULL handling. Add a unit test that runs the query against a fixture including NULLs. Read the engine's docs on 3-valued logic (TRUE / FALSE / UNKNOWN) once and remember forever.
🎓 Lesson to steal
SQL is 3-valued logic (TRUE, FALSE, UNKNOWN=NULL). Boolean identities you know from Python (De Morgan) still hold, but NULL breaks intuitive equality. Discrete math + engine-specific docs = correctness.
War story Common failure · cycle in a config graphpainful debugging
🔥 What broke
An ops team writes YAML config where each service can list its dependencies. Over months, service A → B → C → A creeps in. Startup loops forever, or worse, silently deadlocks.
🧯 The fix
Run Kahn's topological sort on the dependency graph at load time. If the result doesn't include every node, you have a cycle — fail startup with an explicit "cycle detected: A → B → C → A" message. 5 lines of Python that prevent hours of 3 a.m. debugging.
🎓 Lesson to steal
Every config-loading, module-loading, migration-running, dependency-injecting system is walking a graph. Detect cycles at edit-time or load-time, not at run-time.

Where this shows up in the rest of the plan

Discrete math threads through everything
S027 · Trees & tries
Trees are graphs with structure — traversal orders (preorder, inorder, postorder) are DFS variants.
S033 · DP
DP problems are usually graph-traversals on an implicit DAG of subproblems.
S035 · Relational model
Codd's algebra IS set theory. Every SQL query is set operations plus filters.
S050 · Graph databases
Neo4j, DGraph, TigerGraph — traversals are the first-class query.
S072 · Consensus protocols
Raft/Paxos correctness proofs use propositional + temporal logic.
S106 · Prompt graphs / LangGraph
LLM tool orchestration is a DAG of steps; topological sort chooses the run order.

(e) Recall + stretch · 10 min

Recall — click each to reveal · click to reveal
★ = stretch question

Explain-out-loud test

  1. What's the difference between a permutation and a combination? Give one real example of each.
  2. State one of De Morgan's laws in plain English.
  3. What is a DAG and name one everyday example.

What comes next

Hub: The 6-Month Learning Plan


Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.