Discrete math 2 problem set, part a: DAGs, modular arithmetic, hashing
Overview
Section titled “Overview”| Module | S-M06a · solve · none · Pass 2 · 4 to 5 h |
| You build | answers in solve/S-M06a.toml (23 checked by SymPy) and 5 proofs in solve/S-M06a/qN.md (self-graded against their rubrics) |
| Contract | none: a pen and paper set |
| Tests | course/solve/S-M06a/key.toml (hidden): typed answers plus reject canaries; the problems are in course/solve/S-M06a/problems.md and in section 4 |
| Needs | S-M05 (proof methods, induction, counting, bijections). Reading: the Discrete Math 2 topic, graphs and number theory sections |
| Used by | no call site (a solve set). Do it before M06.1 (iterative toposort for backward; q20 is its correctness proof) and M06.3 (PCG32, SplitMix64, FNV-1a: wrapping arithmetic, rotations, and the odd multiplier of q22). Part b, S-M06b in Pass 3, covers trees, birthday bounds, MinHash, and Bloom filters |
| Milestone | MS-P2 (the Pass 2 gate runs ol check on every solve part of the pass) |
| Optional depth | Hammack, Book of Proof, ch. 11 and 12; Cormen et al., Introduction to Algorithms, ch. 11 (hashing) and 22.4 (topological sort); O’Neill, “PCG: A Family of Simple Fast Space-Efficient Statistically Good Algorithms” (2014), sections 4 and 6 |
Key Takeaways
Section titled “Key Takeaways”- A computation graph is a DAG; a topological order lists every node after its inputs, it is rarely unique, and backward walks it in reverse (q2, q3, q8).
- Recursion depth equals the longest input chain, so a recursive traversal of a -node chain dies at Python’s limit of 1000; the iterative DFS of q20 does not (q9).
- Fixed-width integers are arithmetic modulo : addition wraps, rotation moves bits around the word, and permutes the words exactly when is odd (q12, q13, q22, q23).
- FNV-1a XORs a byte in, then multiplies; swapping the two steps gives FNV-1, a different hash (q14).
- With keys in buckets the expected number of colliding pairs is , which is why collisions appear long before the table is full (q17).
How to work this chapter
Section titled “How to work this chapter”ol start S-M06a # writes solve/S-M06a.toml and one file per proofol check S-M06a # SymPy checks the answers, then asks each proof rubric (y/n)ol check S-M06a --regrade # ask the rubrics again after you change a proof1. Why now
Section titled “1. Why now”Your bigram model in Pass 1 needed no graph: its gradient was a count. In Pass 2 you write an autograd engine (L0.1), and backward is only correct if every node’s gradient is complete before it is passed on to that node’s inputs, which means visiting the graph in reverse topological order. M06.1 builds that order with an iterative depth-first search, because a recursive one crashes on the -node chains that long sequences produce. In the same pass M06.3 builds PCG32, the random generator every later module draws from, and FNV-1a, the hash that rt.04 uses to name KV-cache blocks. Both are arithmetic on 32- and 64-bit words that silently wrap. This set makes you fluent in directed acyclic graphs, modular arithmetic, and the basic probability of hashing before you write that code.
2. Principles
Section titled “2. Principles”| Symbol | Meaning | Type / shape |
|---|---|---|
| a directed graph: nodes , edges | ||
| an edge; in an autograd graph, is an input of | ||
| in-degree (edges into ), out-degree (edges out of ) | integer | |
| adjacency matrix, when | int[n][n] | |
| remainder of on division by , in | integer | |
| divides | relation | |
| greatest common divisor | integer | |
| bitwise XOR | uint64 | |
| number of keys, number of buckets | integer | |
| load factor | real |
2.1 Directed graphs and DAGs
Section titled “2.1 Directed graphs and DAGs”A directed graph is a set of nodes and a set of ordered pairs of nodes, the edges. A path is a sequence of edges , and a cycle is a path of length at least 1 that returns to its start. A graph with no cycle is a directed acyclic graph (DAG). Every expression a program evaluates is a DAG: the nodes are values, and when is an input of the operation that makes . A value used twice ( in ) has out-degree 2, which is why a gradient must be summed over consumers.
A topological order lists all nodes so that every edge goes forward: implies is listed before . A graph has one exactly when it is acyclic (q19 one way; q18 gives the other). Topological orders are rarely unique: any two nodes not connected by a path can be swapped. That is why the M06.1 tests check the property “every edge goes forward”, never one specific list.
Two algorithms produce one. Kahn’s algorithm keeps the in-degree of every node, repeatedly outputs a node of in-degree 0, and decrements the in-degrees of the nodes it points to. Depth-first search outputs a node after all of its inputs have been output (post-order). Written recursively, DFS uses one stack frame per node on the current path, so its depth is the length of the longest input chain, and Python raises RecursionError past 1000 frames. Written with an explicit stack (q20), its depth is bounded only by memory.
Paths also count. Entry of is the number of paths of length exactly from to , because extends each path of length by one edge. A DAG on nodes has no path longer than edges, so . In reverse mode, the derivative of an output with respect to an input is a sum over all paths between them of the product of the local derivatives along each path (M04.2’s chain rule, organized by the graph).
2.2 Modular arithmetic
Section titled “2.2 Modular arithmetic”Division with remainder: for integers and there are unique integers and with and ; write . For negative the remainder is still in : . (C’s % truncates toward zero and returns ; Python’s % returns 3.) Two integers are congruent modulo , , when divides ; congruence is an equivalence relation, and it respects and (q21), so you may reduce after every step.
The multiplicative inverse of modulo is an with ; it exists exactly when . Then multiplication by permutes (q22); when , it maps everything onto multiples of and loses states (q23).
2.3 Machine words
Section titled “2.3 Machine words”A uint32_t holds bits and its arithmetic is arithmetic modulo : wraps to 1. C, Rust, and Go all expose this (C on unsigned types, Rust with wrapping_add and wrapping_mul, Go on uint32 and uint64), and Python emulates it with & 0xFFFFFFFF. A logical shift right by drops the low bits and fills with zeros. A rotation right by moves bit to bit , so no bit is lost: in C, (x >> r) | (x << ((32 - r) & 31)). PCG32 advances a 64-bit LCG state with odd and odd , then outputs a 32-bit word built with a XOR-shift and a rotation by the state’s top 5 bits (course/contracts/spec/pcg32.md).
FNV-1a 64 hashes bytes by starting at the offset basis and setting with the prime . The order matters: FNV-1 multiplies first and XORs second, and gives a different value.
2.4 Hashing and load factor
Section titled “2.4 Hashing and load factor”A hash table with buckets stores a key in bucket . Chaining keeps a list per bucket. The load factor is the average list length. Under simple uniform hashing (each key lands in each bucket with probability , independently), an unsuccessful lookup computes the hash and scans one whole chain, probes on average. A universal family such as with a prime larger than any key and random , makes any two keys collide with probability at most about , whatever the keys.
Collisions are common long before the table fills. Each of the pairs of keys collides with probability , and expectation is linear, so the expected number of colliding pairs is : with and it is already about . Part b (S-M06b) turns this into the birthday bound.
3. Worked example by hand
Section titled “3. Worked example by hand”This is a sibling of q3 and q22, not one of the graded problems.
Count the topological orders of the graph with edges , , on nodes .
needs and before it, and needs , so is last and is third: positions 3 and 4 are forced. Positions 1 and 2 hold and in either order. Total: 2 orders, and . In general, count by cases on which source comes first, as in q3; never assume the order is unique.
Claim. is a bijection of , and is not.
Method: direct computation, then the general reason. The images under of are : all eight values, each once, so it is a bijection. The reason is , and , so multiplying by 5 is undone by multiplying by 5 again. Under the images are : two values, because and every image is a multiple of 4. A multiplier sharing a factor with the modulus collapses states. That is the content of q22 and q23, and why an LCG on 64-bit words uses an odd multiplier.
4. The problem set
Section titled “4. The problem set”Write each answer in solve/S-M06a.toml; lettered parts are their own tables:
[q4]answer = "[0, 0, 2, 1, 2]"[q7.a]answer = "[[0, 0, 1, 0], [0, 0, 0, 1], [0, 0, 0, 0], [0, 0, 0, 0]]"[q20]proof = "S-M06a/q20.md"Numbers are exact integers or fractions (3/4, 2^31 + 2^30); no hexadecimal, so convert to decimal.
Graphs and DAGs
Section titled “Graphs and DAGs”q1. In : (a) what is the in-degree of node 5? (b) What is the out-degree of node 2? [number]
q2. (a) Is a topological order of ? (b) Is ? [bool]
q3. How many topological orders does have? [number]
q4. Kahn’s algorithm starts from the in-degree of every node. Give the in-degrees of nodes of in that order. [vector]
q5. How many directed paths lead from node 2 to node 5 in ? (In reverse mode, the gradient of node 5 with respect to node 2 is a sum with one term per path.) [number]
q6. Is the graph with edges , , , a DAG? [bool]
q7. The chain has adjacency matrix with when is an edge. Entry of counts the paths of length from to . Give (a) and (b) . [matrix]
q8. The autograd graph of has nodes , , , and . Backward must visit a node only after every node that consumes it. Which order is valid for backward? [choice]
(a) (b) (c) (d)
q9. A recursive depth-first search that starts at the last node of a chain of nodes and recurses into each node’s input makes how many nested calls at its deepest point, counting the first call? (Python’s default recursion limit is 1000.) [number]
Modular arithmetic and hashing
Section titled “Modular arithmetic and hashing”Here is the remainder in , also for negative .
q10. (a) . (b) . [number]
q11. Give the multiplicative inverse of 3 modulo 11: the with . [number]
q12. A uint32_t adds modulo . What is ? [number]
q13. PCG32’s output step rotates a 32-bit word right. Rotating right by bits moves bit to bit . Give the rotation of (bits 31 and 0 set) right by 1 bit, as a decimal number. [number]
q14. FNV-1a 64 starts at and, for each byte , sets , where is bitwise XOR. Give the FNV-1a 64 hash of the one-byte string a (byte 97), as a decimal number. [number]
q15. A hash table with chaining has buckets and holds keys. (a) Give its load factor . (b) Under simple uniform hashing, an unsuccessful search hashes once and then scans a whole chain; its expected cost is probes. Give it. [number]
q16. For the universal hash with , , , , give . [number]
q17. keys are hashed independently and uniformly into buckets. What is the expected number of pairs of keys that share a bucket? [expr in n, m]
Proofs
Section titled “Proofs”q18. Prove that every finite, nonempty directed acyclic graph has a node with in-degree 0. [proof]
q19. Prove that a directed graph that has a topological order has no directed cycle. [proof]
q20. M06.1 orders the autograd graph with an iterative depth-first search. Each node has a finite list of inputs, inputs(v), and the graph reachable from the root is acyclic:
order = []; seen = {root}; stack = [(root, iterator over inputs(root))]while stack is not empty: (v, it) = top of stack if it yields an input u that is not in seen: add u to seen; push (u, iterator over inputs(u)) else if it is exhausted: pop (v, it); append v to orderreturn orderProve that every node reachable from the root appears in order exactly once, and after all of its inputs. (Backward then walks order in reverse.) [proof]
q21. Prove that for all integers and every integer . [proof]
q22. Prove that if , then is a bijection of . (This is why the LCG step inside PCG32 never merges two states: its multiplier is odd.) [proof]
q23. Is a bijection of ? [bool]
5. Pitfalls
Section titled “5. Pitfalls”| Pitfall | Symptom | Caught by |
|---|---|---|
| Testing for one specific topological order | a correct toposort fails on a valid order | q3 (canary: 1) |
| Swapping in-degree and out-degree | Kahn’s algorithm starts from sinks and emits nothing | q4 (canary: out-degrees) |
| Backward in forward order, or a shared node before one of its consumers | a gradient used before every consumer added to it | q8 (canaries b and d) |
| Recursing over a long chain | RecursionError at depth 1000 on a long sequence | q9 (canary 1000) |
Using C’s truncating % for a negative operand | a negative bucket index | q10 (canary -2) |
| Forgetting to wrap at the word size | a Python port of PCG32 diverges from C after one step | q12 (canary 4294967297) |
| Shifting instead of rotating | PCG32 output loses high bits | q13 (canary 1073741824) |
| Multiplying before XOR in FNV-1a | block hashes that disagree with every other implementation | q14 (canary: the FNV-1 value) |
| A multiplier sharing a factor with the modulus | the generator or hash collapses onto a fraction of its states | q22 rubric, q23 |
6. Where it’s used next
Section titled “6. Where it’s used next”| Direction | Module | How it uses this |
|---|---|---|
| Back | S-M05 | induction, pigeonhole, and the injective-and-surjective test behind q18 to q22 |
| Forward | M06.1 | toposort for backward, iterative, checked on a -deep chain; q20 is its correctness proof |
| Forward | M06.3 | PCG32, SplitMix64, FNV-1a, and universal_hash in Python and C |
| Forward | L0.1 | backward walks the topological order in reverse and sums gradients over consumers |
| Forward | rt.04 | KV blocks are named by chained FNV-1a 64 hashes (DESIGN D13) |
| Forward | S-M06b | trees, birthday bounds, MinHash, and Bloom filters build on q15 to q17 |