41 min read

> "Order is not pressure which is imposed on society from without, but an equilibrium which is set up from within."

Prerequisites

  • 12
  • 1

Learning Objectives

  • Define a partial order from its three properties and draw the Hasse diagram of a finite poset.
  • Distinguish total orders from partial orders, and identify chains and antichains in a poset.
  • Produce a topological sort of a finite poset (or DAG) and prove that one always exists.
  • Identify whether a poset is a lattice by computing least upper bounds and greatest lower bounds.
  • State the laws of Boolean algebra and connect them to propositional logic and logic-gate circuits.

Chapter 13: Partial Orders, Lattices, and Boolean Algebra

"Order is not pressure which is imposed on society from without, but an equilibrium which is set up from within." — José Ortega y Gasset

Overview

Run make on a real project and something quietly remarkable happens: out of a tangle of source files, headers, object files, and a final binary, the build system figures out a legal order to compile everything — never building a file before the things it depends on. Type pip install with a dependency that needs another dependency that needs a third, and the resolver does the same trick. Open a spreadsheet, change one cell, and watch dozens of dependent cells recalculate in exactly the right sequence. These tools are all solving one problem, and it is a problem about order.

But not the order you learned in grade school. The integers are totally ordered — given any two, one is bigger. Dependencies are not like that. Your parser.o must be built before main, and lexer.o must be built before main, but parser.o and lexer.o have no order between them — you can build them in either sequence, or in parallel. Some pairs are comparable; some are not. That is a partial order, and once you can name it, you'll see it everywhere in computing: in the subtype relationships of a type system, in the version constraints of a package manager, in the "happens-before" relation that defines what concurrent code is even allowed to do.

This chapter is where the relations you met in Chapter 12 grow up into structures with real computational muscle. We'll take the same three properties you used to recognize an equivalence relation, swap one of them, and get a completely different — and equally fundamental — object. From there we build to the algorithm that powers every build system (topological sort), to a structure that captures "combine two things into their nearest common ancestor" (the lattice), and finally back to where Part I began: Boolean algebra, the two-element lattice that is, simultaneously, propositional logic and the mathematics of every digital circuit.

In this chapter you will learn to:

  • Recognize a partial order by its three defining properties, and draw its Hasse diagram — the compressed picture that throws away everything you can infer.
  • Tell a total order from a partial one, and find the longest chain and largest antichain in a poset.
  • Topologically sort any finite poset — turn a partial order into a compatible total order — and prove that this is always possible (this is the math under make, build pipelines, and task scheduling).
  • Decide whether a poset is a lattice by computing joins and meets, and connect that to the lub/glb operations that show up in type inference and static analysis.
  • Write the laws of Boolean algebra, see them as the same laws as the logical equivalences from Chapter 1, and translate an expression into a circuit of logic gates.

Learning Paths

🏃 Fast Track: If posets are familiar, skim 13.1–13.2 and go straight to 13.3 (topological sort — the algorithm you'll actually implement) and 13.5–13.6 (Boolean algebra and gates). Do the ⭐⭐ and ⭐⭐⭐ exercises. The Project Checkpoint (topo_sort) is the must-do.

📖 Standard Path: Read in order. Draw every Hasse diagram yourself before looking at ours, and work each 🔄 Check Your Understanding from memory. Pay special attention to the strategy paragraph before each proof — the strategy is more reusable than the proof.

🔬 Deep Dive: After the chapter, prove that the divisibility poset $(\{1,\dots,n\}, \mid)$ is a lattice and identify its join and meet operations (you'll find old friends from arithmetic). Then prove that every finite Boolean algebra has size a power of two. Both are in the exercise set, Part E.


13.1 Partial orders and Hasse diagrams

Let's start with the most familiar order of all and then break it on purpose. The integers under $\le$ have a property so obvious it's easy to miss: for any two integers $a$ and $b$, either $a \le b$ or $b \le a$. There are no incomparable integers. Now contrast that with a relation you already know from Chapter 4 — divisibility. Among $\{2, 3, 4, 6, 12\}$, we have $2 \mid 4$ and $2 \mid 6$ and $3 \mid 6$, but $2 \nmid 3$ and $3 \nmid 2$: the numbers 2 and 3 are simply incomparable under divisibility. Neither divides the other. Yet "divides" still behaves like an ordering in every other respect — it has a clear sense of "smaller" (a divisor is smaller than its multiples), it never contradicts itself, and it chains together.

What exactly are "all other respects"? Three properties, which you met for relations in general in Chapter 12.

🔗 Connection. In Chapter 12 you classified relations by reflexive, symmetric, antisymmetric, and transitive, and saw that reflexive + symmetric + transitive gives an equivalence relation (a relation that sorts a set into disjoint classes of "the same"). A partial order keeps reflexive and transitive but replaces symmetric with antisymmetric. That one swap changes "these things are the same" into "these things are ranked." Equivalence relations group; partial orders rank.

Definition. A relation $R$ on a set $S$ is a partial order if it is:

  • reflexive: $a \mathrel{R} a$ for every $a \in S$;
  • antisymmetric: if $a \mathrel{R} b$ and $b \mathrel{R} a$, then $a = b$;
  • transitive: if $a \mathrel{R} b$ and $b \mathrel{R} c$, then $a \mathrel{R} c$.

A set $S$ paired with a partial order on it, written $(S, \preceq)$, is called a partially ordered set, or poset for short. We write the generic partial-order symbol as $\preceq$ ("precedes or equals") to keep it distinct from the specific orders ($\le$, $\subseteq$, $\mid$) it generalizes. When $a \preceq b$ and $a \ne b$ we write $a \prec b$ ("strictly precedes"). Two elements $a, b$ are comparable if $a \preceq b$ or $b \preceq a$; otherwise they are incomparable, written $a \parallel b$.

Why "partial"? Because the order may decline to compare some pairs. The word marks the contrast with a total order (13.2), where every pair is comparable.

Three posets you should keep in your head as touchstones, because almost every example reduces to one of them:

  1. $(\mathbb{Z}, \le)$ — the integers under "less than or equal." Reflexive ($a \le a$), antisymmetric ($a \le b$ and $b \le a$ force $a = b$), transitive. Every pair comparable.
  2. $(\mathcal{P}(S), \subseteq)$ — the power set of any set $S$ ordered by the subset relation. Reflexive ($A \subseteq A$), antisymmetric (mutual subsets are equal — this is how we prove set equality, from Chapter 8), transitive. Most pairs incomparable: $\{1\}$ and $\{2\}$ are subsets of neither.
  3. $(\mathbb{Z}^{+}, \mid)$ — the positive integers under divisibility. Reflexive ($a \mid a$), antisymmetric (for positives, $a \mid b$ and $b \mid a$ force $a = b$), transitive. Lots of incomparable pairs, like 2 and 3.

⚠️ Common Pitfall: Divisibility is only antisymmetric on the positive integers. On all of $\mathbb{Z}$ it fails: $2 \mid -2$ and $-2 \mid 2$, yet $2 \ne -2$. So $(\mathbb{Z}, \mid)$ is not a poset; $(\mathbb{Z}^{+}, \mid)$ is. The domain is part of the definition — always state it.

Let's confirm one of these rather than wave at it, because verifying the axioms is a skill you'll need in the exercises.

The strategy first. To prove $(\mathcal{P}(S), \subseteq)$ is a poset we just check the three properties one at a time, each by unpacking the definition of $\subseteq$ from Chapter 8 ($A \subseteq B$ means every element of $A$ is an element of $B$). No cleverness — partial-order proofs are almost always "translate each axiom into the definition and confirm." The only mildly interesting one is antisymmetry, and it's exactly the set-equality argument you already know.

Claim. For any set $S$, the relation $\subseteq$ on $\mathcal{P}(S)$ is a partial order.

Proof. Let $A, B, C \in \mathcal{P}(S)$ (so each is a subset of $S$).

Reflexive: every element of $A$ is an element of $A$, so $A \subseteq A$.

Antisymmetric: suppose $A \subseteq B$ and $B \subseteq A$. Then every element of $A$ is in $B$, and every element of $B$ is in $A$; the two sets have exactly the same elements, so $A = B$ by the definition of set equality (Chapter 8).

Transitive: suppose $A \subseteq B$ and $B \subseteq C$. Take any $x \in A$. Since $A \subseteq B$, $x \in B$; since $B \subseteq C$, $x \in C$. So every element of $A$ is in $C$, i.e. $A \subseteq C$.

All three properties hold, so $(\mathcal{P}(S), \subseteq)$ is a poset. $\blacksquare$

Drawing a poset: the Hasse diagram

A relation on a finite set can always be drawn as a directed graph — a dot for each element, an arrow $a \to b$ whenever $a \preceq b$ (you saw this "digraph" representation in Chapter 12). But for a partial order that picture is a cluttered mess: reflexivity gives every dot a self-loop, and transitivity forces an arrow from $a$ all the way to $c$ whenever there's a path through $b$. Almost every arrow is implied by the order's own rules. So we draw the minimal picture from which the rest can be reconstructed.

Definition. The Hasse diagram of a finite poset $(S, \preceq)$ is the drawing obtained by:

  1. placing each element of $S$ as a point, with $a$ drawn below $b$ whenever $a \prec b$;
  2. drawing a line segment from $a$ up to $b$ exactly when $b$ covers $a$ — meaning $a \prec b$ and there is no element $z$ strictly between them ($a \prec z \prec b$ for no $z$);
  3. drawing no self-loops and no arrowheads (direction is encoded by "up").

The covering relation is the key idea: we keep only the immediate steps and let the eye recover everything else by following edges upward. If you can walk upward from $a$ to $b$ along the segments, then $a \preceq b$; that's the whole reading rule.

💡 Intuition: A Hasse diagram is a partial order with the redundancy compressed out — the same way a good dependency file lists only direct dependencies and lets the build tool compute the transitive ones. You don't tell make that main needs the C standard library and everything the library needs; you state the direct edges and let transitivity do the rest. The Hasse diagram is the transitive reduction of the order, and the full order is its transitive closure — a term you met in Chapter 12.

Here is the Hasse diagram for the divisibility poset on the divisors of 12, namely $\{1, 2, 3, 4, 6, 12\}$ under $\mid$:

            12
           /  \
          4    6
          |   / \
          2  /   3
           \/    |
           (2 connects up to 4 and 6; 3 connects up to 6)
            \   /
             \ /
              1

Read it cleanly as a layered picture:

         12
        /  \
       4    6
       |   / \
       2  3   (2 and 3 both sit above 1)
        \ /
         1

with edges: $1\!-\!2$, $1\!-\!3$, $2\!-\!4$, $2\!-\!6$, $3\!-\!6$, $4\!-\!12$, $6\!-\!12$. Notice what's absent: there is no edge drawn from 1 to 4, even though $1 \mid 4$, because the order can be recovered by walking $1 \to 2 \to 4$. There is no edge from 1 to 12 either, for the same reason. And 2 and 3 sit side by side with no edge between them — they're incomparable, which the picture shows by neither being above the other. The bottom element 1 divides everything; the top element 12 is divided by nothing else in the set.

Some vocabulary for the extreme points, which we'll need constantly:

  • An element $m$ is maximal if nothing is strictly above it ($m \prec x$ for no $x$); minimal if nothing is strictly below it. In the divisors-of-12 diagram, 12 is the only maximal element and 1 the only minimal one — but a poset can have several maximal or minimal elements.
  • An element is the greatest (or top) if it is $\succeq$ every element, and the least (or bottom) if it is $\preceq$ every element. A greatest element, if it exists, is unique (we'll prove this in 13.4); it's automatically maximal, but a maximal element need not be greatest.

⚠️ Common Pitfall: Maximal and greatest are not the same word. "Maximal" means "nothing above me"; "greatest" means "above everyone." In the poset $\{ \{a\}, \{b\}, \{a,b\}\}$ under $\subseteq$, the set $\{a,b\}$ is both maximal and greatest. But in just $\{ \{a\}, \{b\}\}$, both $\{a\}$ and $\{b\}$ are maximal (nothing is above either) and there is no greatest element (neither is above the other). Many maximal elements can coexist; a greatest element is at most one.

Let's build a poset in Python so the abstraction has something to stand on. We'll represent a partial order on a small set by the set of pairs $(a, b)$ with $a \preceq b$, and write a function that finds the cover relation — the edges a Hasse diagram would draw.

def covers(pairs, elements):
    """Given a partial order as a set of (a,b) pairs meaning a <= b,
    return the covering pairs (the Hasse-diagram edges)."""
    strict = {(a, b) for (a, b) in pairs if a != b}      # drop reflexive pairs
    cover = set()
    for (a, b) in strict:
        # b covers a unless some z lies strictly between a and b
        if not any((a, z) in strict and (z, b) in strict for z in elements):
            cover.add((a, b))
    return cover

elems = [1, 2, 3, 4, 6, 12]
divides = {(a, b) for a in elems for b in elems if b % a == 0}
print(sorted(covers(divides, elems)))
# Expected output:
# [(1, 2), (1, 3), (2, 4), (2, 6), (3, 6), (4, 12), (6, 12)]

Those seven pairs are exactly the edges we drew by hand. The function literally computes the transitive reduction: it keeps $a \prec b$ only when no intermediate $z$ relays the order.

🔄 Check Your Understanding 1. State the one property that an equivalence relation has but a partial order replaces, and what it is replaced by. 2. In the divisors-of-12 Hasse diagram, why is there no edge directly from 1 to 12? 3. Give a poset with two different maximal elements and no greatest element.

Answers

  1. Equivalence relations are symmetric; partial orders replace symmetry with antisymmetry.
  2. Because 12 does not cover 1 — there are elements strictly between them (e.g. $1 \prec 2 \prec 12$), so the order $1 \mid 12$ is recoverable by walking up through 2 or 3, and a Hasse diagram draws only covering edges. 3. $(\{ \{a\}, \{b\}\}, \subseteq)$: both singletons are maximal, and neither is $\supseteq$ the other, so there is no greatest element. (Equivalently, the two incomparable numbers $\{2, 3\}$ under $\mid$.)

13.2 Total orders, chains, and antichains

A partial order earns its name by sometimes refusing to compare. The special case where it never refuses is worth its own name, because it is the order our intuition defaults to.

Definition. A partial order $\preceq$ on $S$ is a total order (also called a linear order) if every two elements are comparable: for all $a, b \in S$, either $a \preceq b$ or $b \preceq a$. A set with a total order is a totally ordered set or chain.

The integers under $\le$, the reals under $\le$, and strings under dictionary (lexicographic) order are all total orders — that last one is exactly what sorted() uses on a list of strings, and it's why sorting works at all: a sort is only well-defined when the elements are totally ordered, so that "the smallest remaining element" always exists and is unique. A partial order has no single "smallest remaining" in general, which is precisely the difficulty topological sort (13.3) will overcome.

Two structural features of a poset measure how far it is from being total — how much it is willing to compare and how much it refuses.

Definition. In a poset $(S, \preceq)$:

  • A chain is a subset $C \subseteq S$ in which every two elements are comparable. (A chain is a totally ordered piece sitting inside the poset.)
  • An antichain is a subset $A \subseteq S$ in which every two distinct elements are incomparable.

In the divisors-of-12 poset, $\{1, 2, 4, 12\}$ is a chain of length 4 ($1 \mid 2 \mid 4 \mid 12$, all comparable), and $\{2, 3\}$ is an antichain (neither divides the other), as is $\{4, 6\}$. The chain captures "a longest dependency path"; the antichain captures "a set of mutually independent things" — in build terms, a batch you could compile fully in parallel.

💡 Intuition: Picture a Hasse diagram. A chain is a path you can trace by always moving upward — a stack of comparable elements. An antichain is a horizontal slice — a set of elements none of which sits above another. The longest chain is the critical path of a project; the largest antichain is the maximum parallelism available.

These two quantities are linked by a classic and surprisingly deep theorem, which we'll state but not prove in full (its proof is a lovely application of induction).

Mirsky's theorem (stated). In any finite poset, the size of the longest chain equals the minimum number of antichains needed to cover the whole poset.

The dual statement — that the size of the largest antichain equals the minimum number of chains needed to cover the poset — is Dilworth's theorem, one of the gems of combinatorics. We mention them so you recognize the names; the practical payoff is the intuition that chains and antichains are complementary ways to decompose a partial order, and decomposing a problem along a partial order is a recurring move in scheduling and parallelization.

Let's compute the longest chain in code, because the algorithm is short and it previews the dynamic programming you'll meet later. The length of the longest chain ending at an element $x$ is $1 + \max$ over all $y \prec x$ of (longest chain ending at $y$).

def longest_chain(pairs, elements):
    """Length of the longest chain in a poset given as <= pairs."""
    strict = {(a, b) for (a, b) in pairs if a != b}
    best = {}
    # process elements so that predecessors are computed first
    def length_to(x):
        if x in best:
            return best[x]
        below = [a for (a, b) in strict if b == x]
        best[x] = 1 + max((length_to(a) for a in below), default=0)
        return best[x]
    return max(length_to(x) for x in elements)

elems = [1, 2, 3, 4, 6, 12]
divides = {(a, b) for a in elems for b in elems if b % a == 0}
print(longest_chain(divides, elems))
# Expected output:
# 4

The answer 4 is the chain $1 \mid 2 \mid 4 \mid 12$ (or $1 \mid 2 \mid 6 \mid 12$). This is the critical path: even with unlimited parallel workers, a project with this dependency structure needs at least four sequential stages.

🔄 Check Your Understanding 1. Is $(\mathcal{P}(\{1,2,3\}), \subseteq)$ a total order? Give a pair witnessing your answer. 2. In the divisors-of-12 poset, find an antichain of size 2 other than $\{2,3\}$. 3. If a poset is a total order, what is its largest antichain?

Answers

  1. No. $\{1\}$ and $\{2\}$ are incomparable ($\{1\} \not\subseteq \{2\}$ and ${2} \not\subseteq {1}$), so not every pair is comparable. 2. ${4, 6}$ (neither divides the other); also ${3, 4}$ or $\{2, 3\}$. 3. Size 1 — in a total order every two distinct elements are comparable, so no antichain can contain two elements. (By Dilworth, the whole chain is covered by one chain.)

13.3 Topological sort: turning a partial order into a line

Here is the central computational problem of the chapter, and the one you've used without naming. You have a partial order — a set of tasks with "must-come-before" constraints — and you need to do the tasks one at a time, in some sequence that never violates a constraint. Compiling a project: each source file must be compiled before anything that includes it. Installing packages: each dependency before its dependent. Scheduling university courses: each prerequisite before the course that requires it. Evaluating a spreadsheet: each cell before the cells that reference it.

In every case you must flatten a partial order into a total order — pick an actual sequence — in a way that respects the partial order: if $a \prec b$, then $a$ comes before $b$ in your sequence. The partial order says "these constraints"; you must produce one concrete line obeying all of them.

Definition. A topological sort (or topological ordering, or linear extension) of a finite poset $(S, \preceq)$ is a total order $\le^{*}$ on $S$ that is consistent with $\preceq$: whenever $a \preceq b$, we also have $a \le^{*} b$. Equivalently, it is an arrangement of the elements in a line $s_1, s_2, \dots, s_n$ such that if $s_i \prec s_j$ then $i < j$ — every element appears after all the elements it depends on.

🔗 Connection. A finite poset is the same data as a directed acyclic graph (DAG): draw an edge $a \to b$ whenever $a \prec b$ (or just for the covers — same reachability). "Acyclic" is exactly antisymmetry-plus-transitivity in disguise: a cycle $a \prec b \prec \dots \prec a$ would force $a \prec a$, which a strict order forbids. So "topologically sort a DAG" and "linearly extend a poset" are the same problem. You'll meet DAGs and graphs formally in Chapter 27, and in Chapter 28 you'll see this very algorithm re-derived from depth-first search — when you do, this is the chapter it points back to.

Does such an ordering always exist? It is not obvious — maybe some tangle of constraints can't be flattened. The answer is yes for every finite poset, and the proof is constructive: it is the algorithm.

The strategy first. The whole proof rests on one fact: every finite nonempty poset has a minimal element (something with nothing below it). Given that, the algorithm writes itself — output a minimal element, delete it, and repeat on what's left. Each output element has all of its predecessors already placed (they were removed earlier), so the order is respected. To turn this into a proof, we (a) prove a minimal element always exists, then (b) use induction on $|S|$ to show the repeat-until-empty process produces a valid topological sort. This "peel off a minimal element" move is the engine; remember it even if you forget the details.

Lemma. Every finite nonempty poset $(S, \preceq)$ has at least one minimal element.

Proof. Suppose, for contradiction, that $S$ is nonempty but has no minimal element. Pick any $x_0 \in S$. Since $x_0$ is not minimal, some $x_1 \prec x_0$. Since $x_1$ is not minimal, some $x_2 \prec x_1$. Continuing, we build an infinite strictly descending sequence $x_0 \succ x_1 \succ x_2 \succ \cdots$. By transitivity all the $x_i$ are distinct (if $x_i = x_j$ for $i < j$ we'd have $x_i \prec x_i$, impossible for a strict order). But $S$ is finite, so it cannot contain infinitely many distinct elements — contradiction. Hence $S$ has a minimal element. $\blacksquare$

🔗 Connection. That argument is the well-ordering principle wearing a different hat — the idea from Chapter 7 that you cannot descend forever in the natural numbers. "No infinite descending chain" is precisely what makes induction and recursion terminate, and it's precisely what makes topological sort terminate: each round strictly shrinks the set.

Theorem (every finite poset has a topological sort). Every finite poset $(S, \preceq)$ has a topological ordering.

Proof (by induction on $n = |S|$). Let $P(n)$ be: "every poset on $n$ elements has a topological sort."

Base case ($n = 0$): the empty poset is sorted by the empty sequence, vacuously consistent. (If you prefer $n=1$: a single element is its own sorted sequence.)

Inductive step: fix $n \ge 1$ and assume $P(n-1)$. Let $(S, \preceq)$ have $n$ elements. By the Lemma, $S$ has a minimal element $m$. Consider $S' = S \setminus \{m\}$ with the same order restricted to it; $S'$ is a poset on $n-1$ elements, so by the inductive hypothesis it has a topological sort $s_2, s_3, \dots, s_n$. Place $m$ at the front: $m, s_2, s_3, \dots, s_n$. We claim this is a topological sort of $S$. The relative order of $s_2, \dots, s_n$ already respects $\preceq$. The only new pairs involve $m$, which sits first; and because $m$ is minimal, no element of $S$ strictly precedes $m$, so putting $m$ first violates no constraint $a \prec m$ (there are none). Thus every constraint is respected, and $P(n)$ holds. By induction, every finite poset has a topological sort. $\blacksquare$

The proof is the program. Here it is, written from scratch (we'll polish it into the Toolkit's topo_sort in the Project Checkpoint):

def topo_sort(elements, less_than):
    """Topologically sort `elements` so that whenever less_than(a, b),
    a appears before b. `less_than` is the strict relation a < b.
    Returns a list; assumes the relation is acyclic (a real partial order)."""
    remaining = list(elements)
    order = []
    while remaining:
        # find a minimal element: nothing remaining is strictly below it
        m = next(x for x in remaining
                 if not any(less_than(y, x) for y in remaining if y != x))
        order.append(m)
        remaining.remove(m)
    return order

elems = ['lexer.o', 'parser.o', 'main', 'utils.o']
deps = {('lexer.o', 'parser.o'), ('lexer.o', 'main'),
        ('parser.o', 'main'), ('utils.o', 'main')}
print(topo_sort(elems, lambda a, b: (a, b) in deps))
# Expected output:
# ['lexer.o', 'parser.o', 'utils.o', 'main']

Trace it: initially the minimal elements (nothing depends-into them) are lexer.o and utils.o. The generator scans elems in order and picks the first minimal one it finds — lexer.o. Remove it. Now parser.o and utils.o are minimal; first found is parser.o. Remove it; utils.o is minimal, take it. Finally only main remains. The result respects every dependency: lexer.o and parser.o both precede main, and utils.o precedes main. This is exactly what make -j computes to decide a legal build order.

💡 Intuition: A topological sort is not unique — that's the whole point. ['utils.o', 'lexer.o', 'parser.o', 'main'] is equally valid, because utils.o and lexer.o are incomparable and either may go first. The number of valid topological sorts is the number of linear extensions of the poset, and it equals 1 exactly when the poset is already a total order (a single chain). The freedom you see is exactly the freedom a build system has to parallelize.

⚠️ Common Pitfall: Topological sort requires acyclicity. If your dependencies contain a cycle — module A imports B which imports A — there is no minimal element among the cycle (each has something below it), the next(...) call finds nothing, and the algorithm fails (here, with a StopIteration). That failure is informative: a build tool that can't topologically sort is telling you that you have a circular dependency. The math (antisymmetry forbids cycles) is exactly the precondition the tool needs.

🔄 Check Your Understanding 1. Why must a topological sort exist for every finite poset, in one sentence? 2. Give a second valid topological sort of the lexer/parser/utils/main example. 3. What real-world fact is a build system reporting when topological sort fails?

Answers

  1. Because every finite nonempty poset has a minimal element, so you can repeatedly output a minimal element and delete it until nothing remains — each output respects the order because nothing below it is left. 2. e.g. ['utils.o', 'lexer.o', 'parser.o', 'main'] or ['lexer.o', 'utils.o', 'parser.o', 'main']. 3. A circular dependency — the dependency graph contains a cycle, so no legal build order exists.

13.4 Lattices: least upper bounds and greatest lower bounds

Posets let us rank. Lattices let us combine. The motivating question: given two elements of a poset, is there a canonical "smallest thing above both of them" and a canonical "largest thing below both of them"? When the answer is always yes, the poset has enough structure to do real algebra on, and that structure shows up in surprising places — from the type that a compiler infers for x if cond else y, to the "join" of two security clearances, to the result of unioning two sets.

Start concrete with the subset poset $(\mathcal{P}(\{1,2,3\}), \subseteq)$. Take $\{1\}$ and $\{2\}$. What sets contain both? Any superset of $\{1,2\}$: namely $\{1,2\}$ and $\{1,2,3\}$. Among these upper bounds, $\{1,2\}$ is the smallest — it's contained in the other. So the "least upper bound" of $\{1\}$ and $\{2\}$ is $\{1,2\} = \{1\} \cup \{2\}$. Dually, what's contained in both $\{1\}$ and $\{2\}$? Only $\emptyset$. So the "greatest lower bound" is $\emptyset = \{1\} \cap \{2\}$. The two combining operations are just union and intersection — and that is not a coincidence we'll be able to generalize beautifully.

Let's name these bounds precisely.

Definition. Let $(S, \preceq)$ be a poset and $a, b \in S$.

  • An upper bound of $a$ and $b$ is an element $u$ with $a \preceq u$ and $b \preceq u$. A least upper bound (lub), or join, written $a \vee b$, is an upper bound that precedes every upper bound: $a \preceq (a\vee b)$, $b \preceq (a \vee b)$, and for any upper bound $u$, $(a \vee b) \preceq u$.
  • A lower bound of $a$ and $b$ is an element $\ell$ with $\ell \preceq a$ and $\ell \preceq b$. A greatest lower bound (glb), or meet, written $a \wedge b$, is a lower bound that every lower bound precedes.

A least upper bound need not exist (there might be several minimal upper bounds, none least, or none at all). But when it exists, it is unique — and that uniqueness is what lets us write $a \vee b$ as if it names a single thing. Let's prove it, because the argument is short and is the prototype for "the least/greatest such thing is unique" all over mathematics.

The strategy first. To prove a "least" element is unique, suppose two of them, $u_1$ and $u_2$. Each is least, so each precedes the other ($u_1 \preceq u_2$ because $u_2$ is an upper bound and $u_1$ is least; symmetrically $u_2 \preceq u_1$). Then antisymmetry — the property that distinguishes a partial order — collapses them to equality. Antisymmetry is exactly the tool for "two things that each precede the other are the same."

Claim. If a least upper bound of $a$ and $b$ exists, it is unique.

Proof. Suppose $u_1$ and $u_2$ are both least upper bounds of $a$ and $b$. Since $u_2$ is an upper bound and $u_1$ is a least upper bound, $u_1 \preceq u_2$. Since $u_1$ is an upper bound and $u_2$ is least, $u_2 \preceq u_1$. By antisymmetry, $u_1 = u_2$. $\blacksquare$

(The same argument shows the greatest lower bound is unique when it exists — replace "upper/least" with "lower/greatest" throughout. This is the duality that pervades lattice theory: every statement has a mirror image obtained by flipping $\preceq$.)

Now the structure we've been building toward.

Definition. A lattice is a poset $(S, \preceq)$ in which every pair of elements has both a join $a \vee b$ and a meet $a \wedge b$.

💡 Intuition: "Lattice" = "a poset where any two elements have a nearest common ancestor (the join) and a nearest common descendant (the meet)." If you've ever computed the lowest common ancestor of two nodes in a tree, you've computed a meet (in the ancestor order). If a programming language infers that True if c else 3 has type object (the most specific type that is a supertype of both bool and int), it computed a join in the subtype lattice.

Three of our touchstone posets are lattices, with operations you already know:

Lattice Order $\preceq$ Join $a \vee b$ Meet $a \wedge b$
$(\mathcal{P}(S), \subseteq)$ subset $a \cup b$ (union) $a \cap b$ (intersection)
$(\mathbb{Z}^{+}, \mid)$ divides $\operatorname{lcm}(a,b)$ $\gcd(a,b)$
$(\mathbb{Z}, \le)$ $\le$ $\max(a,b)$ $\min(a,b)$

That second row is worth pausing on. In the divisibility order, the "smallest thing both $a$ and $b$ divide into" is their least common multiple, and the "largest thing dividing both" is their greatest common divisor. So $\gcd$ and $\operatorname{lcm}$ — which you'll study deeply in Chapter 22 for the Euclidean algorithm — are secretly the meet and join of a lattice. The notation even rhymes: $\vee$ ("join", goes up, like $\operatorname{lcm}$ and $\cup$ and $\max$) versus $\wedge$ ("meet", goes down, like $\gcd$ and $\cap$ and $\min$).

⚠️ Common Pitfall: Not every poset is a lattice. The classic non-example: take the poset with elements $\{a, b, c, d\}$ where $a \prec c$, $a \prec d$, $b \prec c$, $b \prec d$ (a "diamond without a top or bottom" — two minimal elements $a,b$ and two maximal $c,d$, drawn as an X). Then $a$ and $b$ have two upper bounds, $c$ and $d$, and neither is least ($c$ and $d$ are incomparable). So $a \vee b$ does not exist, and this poset is not a lattice. A poset must supply a unique nearest common ancestor for every pair to qualify.

Let's compute joins and meets in code for the divisibility lattice, to see the abstraction operate:

from math import gcd

def join_meet_divisibility(a, b):
    """In (Z+, |): join = lcm, meet = gcd."""
    meet = gcd(a, b)
    join = a * b // gcd(a, b)        # lcm(a,b) = a*b / gcd(a,b)
    return join, meet

print(join_meet_divisibility(4, 6))    # lcm=12, gcd=2
print(join_meet_divisibility(3, 5))    # coprime: lcm=15, gcd=1
# Expected output:
# (12, 2)
# (15, 1)

For $4$ and $6$: the join is $\operatorname{lcm}(4,6)=12$ (the least number both divide into) and the meet is $\gcd(4,6)=2$ (the greatest number dividing both) — and indeed, in the divisors-of-12 Hasse diagram from 13.1, walk up from both 4 and 6 and the first common point is 12; walk down and the first common point is 2. The picture and the arithmetic agree.

A lattice with a least element (a bottom, written $0$ or $\bot$) and a greatest element (a top, written $1$ or $\top$) is called bounded. Every finite lattice is automatically bounded, because you can take the meet of all elements (the bottom) and the join of all elements (the top). This matters next, because Boolean algebra is exactly a very special bounded lattice.

🔄 Check Your Understanding 1. In $(\mathcal{P}(\{1,2,3\}), \subseteq)$, compute $\{1,2\} \vee \{2,3\}$ and ${1,2} \wedge {2,3}$. 2. In $(\mathbb{Z}^{+}, \mid)$, what are $12 \vee 18$ and $12 \wedge 18$? 3. Why does antisymmetry guarantee that "the" join is a well-defined notation?

Answers

  1. Join $= \{1,2\} \cup \{2,3\} = \{1,2,3\}$; meet $= \{1,2\} \cap \{2,3\} = \{2\}$.
  2. $12 \vee 18 = \operatorname{lcm}(12,18) = 36$; $12 \wedge 18 = \gcd(12,18) = 6$.
  3. Antisymmetry forces any two least upper bounds to be equal (each precedes the other), so there is at most one — making "the join" unambiguous when it exists.

13.5 Boolean algebra and its laws

We now arrive back where Part I started — and close a loop the book has been holding open since Chapter 1. Boolean algebra is the algebra of True/False, of &&/||/!, of every if condition you've ever written and every circuit inside the machine running this code. We can finally say what it is, with the vocabulary of this chapter: it is a particular kind of bounded lattice with one extra operation.

The smallest and most important example is the two-element Boolean algebra. Take $B = \{0, 1\}$ (read as false/true), ordered by $0 \preceq 1$. This is a chain of two elements, hence trivially a lattice: the join is $\max$ and the meet is $\min$. But rename them: $a \vee b = \max(a,b)$ is exactly logical OR, and $a \wedge b = \min(a,b)$ is exactly logical AND. Add a complement operation $\overline{a}$ that flips $0 \leftrightarrow 1$ — logical NOT — and you have the full structure. That is no accident: this lattice is propositional logic, with $\vee, \wedge, \overline{\phantom{a}}$ standing in for $\lor, \land, \neg$.

🔗 Connection. In Chapter 1 you proved logical equivalences like De Morgan's laws ($\neg(p \land q) \equiv \neg p \lor \neg q$) using truth tables. You were doing Boolean algebra without the name. Every law below is one of those equivalences, restated in algebraic notation. The truth table verified them case by case; the algebraic laws let you manipulate expressions symbolically — which is exactly what a compiler's optimizer and a logic-synthesis tool do to shrink a circuit.

Definition. A Boolean algebra is a set $B$ with two binary operations $\vee$ (join/OR) and $\wedge$ (meet/AND), a unary operation $\overline{\phantom{a}}$ (complement/NOT), and two distinguished elements $0$ and $1$, satisfying these laws for all $a, b, c \in B$:

Law $\vee$ form $\wedge$ form
Identity $a \vee 0 = a$ $a \wedge 1 = a$
Domination $a \vee 1 = 1$ $a \wedge 0 = 0$
Idempotent $a \vee a = a$ $a \wedge a = a$
Commutative $a \vee b = b \vee a$ $a \wedge b = b \wedge a$
Associative $(a \vee b)\vee c = a \vee(b\vee c)$ $(a \wedge b)\wedge c = a \wedge(b\wedge c)$
Distributive $a \vee(b\wedge c)=(a\vee b)\wedge(a\vee c)$ $a \wedge(b\vee c)=(a\wedge b)\vee(a\wedge c)$
De Morgan $\overline{a \vee b} = \overline{a}\wedge\overline{b}$ $\overline{a \wedge b}=\overline{a}\vee\overline{b}$
Complement $a \vee \overline{a} = 1$ $a \wedge \overline{a}=0$

Two features deserve a comment. First, everything comes in pairs: each law has a $\vee$ form and a $\wedge$ form, obtained from each other by swapping $\vee \leftrightarrow \wedge$ and $0 \leftrightarrow 1$. This is the principle of duality — the lattice duality from 13.4, now sharpened into a law-by-law mirror. Prove a theorem of Boolean algebra and you get its dual for free. Second, the distributive law is what separates a Boolean algebra from a generic lattice: in an arbitrary lattice, $a \vee (b \wedge c)$ need not equal $(a \vee b) \wedge (a \vee c)$. Boolean algebras are distributive, complemented lattices.

Why care, as a programmer? Because these laws are the rules for simplifying conditions safely. When you rewrite if (a && b) || (a && c) as if a && (b || c), you are applying the distributive law, and the laws guarantee the two conditions are equivalent for all inputs — a guarantee no amount of testing can match. This is theme two of the book made tangible: a proof (here, the algebra) settles all cases at once.

Let's prove one law from the others to show the system is more than a list to memorize — and to model the kind of derivation you'll do in the exercises.

The strategy first. We'll prove the absorption law $a \vee (a \wedge b) = a$. The trick is a standard one in algebra: rewrite the part you want to absorb so the identity and domination laws can collapse it. We introduce $a = a \wedge 1$ (identity), then factor using distributivity, then use domination ($1 \vee b = 1$). Watch for that "multiply by a disguised identity, then factor" pattern — it's the workhorse of Boolean derivations.

Claim (absorption). In any Boolean algebra, $a \vee (a \wedge b) = a$.

Proof. $$ a \vee (a \wedge b) = (a \wedge 1) \vee (a \wedge b) \quad\text{(identity: } a = a \wedge 1\text{)} = a \wedge (1 \vee b) \quad\text{(distributive law)} = a \wedge 1 \quad\text{(domination: } 1 \vee b = 1\text{)} = a \quad\text{(identity).} $$ $\blacksquare$

By duality, the mirror law $a \wedge (a \vee b) = a$ holds too — no separate proof needed; swap $\vee \leftrightarrow \wedge$ and $0 \leftrightarrow 1$ in every step.

We can sanity-check a law on the two-element Boolean algebra by brute force — exactly the "computation tests, proof guarantees" pairing from theme four. Here we verify De Morgan's law over $\{0,1\}$:

def AND(a, b): return a & b
def OR(a, b):  return a | b
def NOT(a):    return 1 - a

# De Morgan: NOT(a OR b) == (NOT a) AND (NOT b), for every a,b in {0,1}
ok = all(NOT(OR(a, b)) == AND(NOT(a), NOT(b))
         for a in (0, 1) for b in (0, 1))
print(ok)
# Expected output:
# True

Four cases, all pass — which confirms De Morgan's law on the two-element algebra (a finite check that here is exhaustive, hence as good as a proof for this specific algebra). For an arbitrary Boolean algebra of unknown size, you'd reach for the axioms instead, as we did for absorption.

🚪 Threshold Concept. Boolean algebra is the hinge of this entire book. Look back: it is the propositional logic of Chapter 1 (with $\lor,\land,\neg$ renamed $\vee,\wedge,\overline{\phantom{a}}$). It is the two-element lattice of 13.4 (join = OR, meet = AND). It is the algebra of subsets (union, intersection, complement obey exactly these laws — that's why $\mathcal{P}(S)$ is a Boolean algebra). And — next section — it is the mathematics of physical circuits. One small set of laws unifies logic, set theory, lattices, and hardware. When you see that the same eight laws govern truth values, sets, and silicon, you've grasped why abstraction (theme three) is the master tool: learn the structure once, apply it in four domains.

🔄 Check Your Understanding 1. Which single law distinguishes a Boolean algebra from an arbitrary lattice? 2. State the dual of $a \wedge (b \vee c) = (a \wedge b) \vee (a \wedge c)$. 3. Use the laws (not a truth table) to simplify $a \wedge (\overline{a} \vee b)$.

Answers

  1. The distributive law (a Boolean algebra is a distributive, complemented lattice; general lattices need not distribute). 2. Swap $\wedge \leftrightarrow \vee$: $a \vee (b \wedge c) = (a \vee b) \wedge (a \vee c)$. 3. $a \wedge(\overline a \vee b) = (a \wedge \overline a) \vee (a \wedge b) = 0 \vee (a \wedge b) = a \wedge b$ (distributive, complement, identity).

13.6 From Boolean algebra to logic gates

The payoff that gives Boolean algebra its name in every CS curriculum: it is the exact mathematics of digital hardware. A logic gate is a physical device that computes a Boolean operation on voltage levels interpreted as 0 and 1. The three primitive gates correspond one-to-one to the three operations:

Operation Gate Boolean Behavior
meet / AND AND gate $a \wedge b$ output 1 iff both inputs 1
join / OR OR gate $a \vee b$ output 1 iff at least one input 1
complement / NOT NOT gate (inverter) $\overline{a}$ flips the input

A combinational circuit is just a Boolean expression drawn as wires and gates: inputs enter on the left, flow through gates, and the output emerges on the right. The expression $\overline{a} \wedge b$ is the circuit "invert $a$, then AND with $b$." Reading a circuit means reading off its Boolean expression; building a circuit from a spec means writing the expression and laying down a gate per operation.

   a ──►[NOT]──►──┐
                  [AND]──► out = (NOT a) AND b
   b ────────────►┘

This is why Boolean algebra matters to a hardware engineer, not just Boolean logic: the algebraic laws are circuit-simplification rules, and fewer operations means fewer gates — less silicon, less power, less delay. When the absorption law tells you $a \vee (a \wedge b) = a$, it is telling a chip designer that an OR gate and an AND gate can be deleted and replaced by a bare wire carrying $a$, with provably identical behavior on every input. Logic synthesis tools do exactly this, automatically, on circuits with millions of gates, and they rely on these laws being theorems — guaranteed, not merely tested.

Let's connect the layers explicitly with a tiny example a CPU actually contains: a half adder, which adds two bits $a$ and $b$ and produces a sum bit and a carry bit. Adding two bits: $0+0=00$, $0+1=01$, $1+0=01$, $1+1=10$. So the sum bit is 1 when exactly one input is 1 (that's exclusive-or, $a \oplus b$), and the carry bit is 1 only when both are 1 (that's $a \wedge b$). In Boolean terms, recalling from Chapter 1 that $a \oplus b = (a \wedge \overline b) \vee (\overline a \wedge b)$:

$$ \text{sum} = a \oplus b = (a \wedge \overline{b}) \vee (\overline{a} \wedge b), \qquad \text{carry} = a \wedge b. $$

def half_adder(a, b):
    """Add two bits. Returns (sum_bit, carry_bit) using Boolean ops."""
    sum_bit   = (a & (1 - b)) | ((1 - a) & b)   # a XOR b
    carry_bit = a & b                            # a AND b
    return sum_bit, carry_bit

for a in (0, 1):
    for b in (0, 1):
        print(a, b, "->", half_adder(a, b))
# Expected output:
# 0 0 -> (0, 0)
# 0 1 -> (1, 0)
# 1 0 -> (1, 0)
# 1 1 -> (0, 1)

Read the last line: $1 + 1$ gives sum bit 0 and carry bit 1 — i.e. binary $10$, which is 2. Chain a few of these (a full adder, then a ripple-carry adder) and you have built the integer addition unit of a processor entirely out of Boolean operations. Every arithmetic operation in the machine reduces, at the bottom, to the three gates that are the three operations of a Boolean algebra. The chapter's arc is complete: a single algebraic structure runs from if conditions (Chapter 1) through lattices to the silicon adding your numbers.

🔄 Check Your Understanding 1. Which Boolean operation does an AND gate compute, and which lattice operation is it? 2. In the half adder, why is the carry bit just $a \wedge b$? 3. How does the absorption law translate into a statement about a circuit?

Answers

  1. An AND gate computes $a \wedge b$, which is the meet of the two-element Boolean lattice.
  2. A carry occurs only when the sum of the two bits is at least 2, i.e. only when both bits are 1 — exactly $a \wedge b$. 3. $a \vee (a \wedge b) = a$ means a circuit computing "$a$ OR ($a$ AND $b$)" can be replaced by a plain wire carrying $a$ — two gates eliminated with identical behavior on every input.

Project Checkpoint: topo_sort for the Toolkit's relations.py

In Chapter 12 you began dmtoolkit/relations.py with the machinery of relations — checking whether a relation is an equivalence and computing its transitive closure. This chapter contributes the piece that turns a partial order into an actionable sequence: topo_sort, the function at the heart of every build system, package manager, and task scheduler. Per the Toolkit API, its signature is topo_sort(dag), where dag is a directed acyclic graph given as an adjacency map {node: [successors]} — node $u$ points to $v$ when $u$ must come before $v$.

We'll implement Kahn's algorithm, which is the "repeatedly remove a minimal element" proof of 13.3 made efficient by tracking each node's number of unmet predecessors (its in-degree). A node is "minimal" exactly when its in-degree is 0.

def topo_sort(dag):
    """Topologically sort a DAG given as {node: [successors]}.
    Returns a list where every edge u->v has u before v.
    Raises ValueError if the graph has a cycle (no valid order)."""
    indeg = {u: 0 for u in dag}
    for u in dag:
        for v in dag[u]:
            indeg[v] = indeg.get(v, 0) + 1
            indeg.setdefault(u, indeg[u])      # ensure u is tracked
    ready = sorted(u for u in indeg if indeg[u] == 0)   # minimal elements
    order = []
    while ready:
        u = ready.pop(0)                       # take a minimal element
        order.append(u)
        for v in dag.get(u, []):               # "delete" u from the graph
            indeg[v] -= 1
            if indeg[v] == 0:
                ready.append(v)
        ready.sort()
    if len(order) != len(indeg):
        raise ValueError("graph has a cycle; no topological order exists")
    return order

build = {'lexer.o': ['parser.o', 'main'],
         'parser.o': ['main'],
         'utils.o': ['main'],
         'main': []}
print(topo_sort(build))
# Expected output:
# ['lexer.o', 'parser.o', 'utils.o', 'main']

Hand-trace it. In-degrees: main has 3 (from lexer.o, parser.o, utils.o), parser.o has 1 (from lexer.o), lexer.o and utils.o have 0. So ready = ['lexer.o', 'utils.o'] (sorted). Pop lexer.o; decrement parser.o to 0 and main to 2; now ready = ['parser.o', 'utils.o']. Pop parser.o; decrement main to 1. Pop utils.o; decrement main to 0; push main. Pop main. Result: ['lexer.o', 'parser.o', 'utils.o', 'main'], length 4 = number of nodes, so no cycle. Every edge points forward. The cycle check is the same insight as 13.3's pitfall: if a cycle exists, those nodes never reach in-degree 0, so they never enter ready, and the output is short — which we report as the circular-dependency error a real tool would raise.

How this builds toward the capstone. topo_sort is the first algorithm on a directed graph in your Toolkit, and it is reused directly: Chapter 28 re-derives topological sort from depth-first search over the Graph class you'll build in Part V, and the capstone's social-network track (Chapter 39, Track B) leans on graph traversal throughout. More immediately, it completes relations.py: you can now take any relation, verify it's a partial order (antisymmetric + transitive + reflexive, via Chapter 12's predicates), and linearize it. That is the exact pipeline a dependency resolver runs.

Toolkit so far: logic.py (Chapters 1–3), recurrences.py (begun Chapter 6), sets.py (Chapter 8), and now relations.py complete with is_equivalence, closure_transitive, and topo_sort. You can already model logical conditions, sets, and ordered/equivalence structures — the structural core of the library.


Summary

A partial order is a relation that is reflexive, antisymmetric, and transitive — equivalence relations' close cousin, with symmetry replaced by antisymmetry (group vs. rank). A set with such a relation is a poset $(S, \preceq)$.

Core objects and facts:

Concept Definition Canonical example
Partial order reflexive + antisymmetric + transitive $(\mathcal{P}(S),\subseteq)$, $(\mathbb{Z}^+,\mid)$
Comparable / incomparable $a\preceq b$ or $b\preceq a$ / neither $2,3$ incomparable under $\mid$
Hasse diagram draw only covering edges; up = greater; no loops/arrows divisors of 12
Total order (chain) every pair comparable $(\mathbb{Z},\le)$, lexicographic strings
Antichain every pair incomparable $\{2,3\}$ under $\mid$
Topological sort total order consistent with $\preceq$ (linear extension) a legal make build order
Lattice every pair has a join $\vee$ and meet $\wedge$ $(\mathcal{P}(S),\subseteq)$, $(\mathbb{Z}^+,\mid)$
Boolean algebra distributive, complemented bounded lattice $\{0,1\}$; $\mathcal{P}(S)$; propositional logic

Key results:

  • Every finite nonempty poset has a minimal element (no infinite descent in a finite set) — hence every finite poset has a topological sort (peel off a minimal element, repeat; proved by induction on $|S|$).
  • A finite poset = a DAG; topological sort = linearizing a DAG. It fails iff there is a cycle (circular dependency).
  • Joins and meets, when they exist, are unique (by antisymmetry).
  • The lattice dictionary: $\vee/\wedge$ are $\cup/\cap$ in $(\mathcal{P}(S),\subseteq)$, $\operatorname{lcm}/\gcd$ in $(\mathbb{Z}^+,\mid)$, $\max/\min$ in $(\mathbb{Z},\le)$.
  • Boolean algebra = a lattice with $\vee$=OR, $\wedge$=AND, complement=NOT, obeying eight law pairs (identity, domination, idempotent, commutative, associative, distributive, De Morgan, complement). The distributive law is what makes it Boolean. Duality: every law/theorem has a mirror under $\vee\leftrightarrow\wedge$, $0\leftrightarrow1$.
  • The two-element Boolean algebra is simultaneously propositional logic, the smallest lattice, and the mathematics of logic gates. A half adder ($\text{sum}=a\oplus b$, $\text{carry}=a\wedge b$) shows arithmetic reducing to gates.

Decision aids:

  • Is it a poset? Check reflexive, antisymmetric, transitive — and state the domain (divisibility needs $\mathbb{Z}^+$).
  • Is it a total order? It's a poset and every pair is comparable (one chain, no incomparable pairs).
  • Is it a lattice? Every pair has a unique least upper bound and a unique greatest lower bound (the "X" poset fails: two incomparable upper bounds).
  • Need a build/eval order? Topologically sort the dependency DAG; a failure means a cycle.

Toolkit: relations.py gains topo_sort(dag) (Kahn's algorithm), completing the module begun in Chapter 12.


Spaced Review

Answer from memory before expanding the solutions. These revisit Chapters 11 and 8.

  1. (Ch. 11) The longest_chain code in 13.2 fills a table best[x] and reuses subresults instead of recomputing them. Recurrences and summations underlie that cost analysis: if computing each of $n$ entries scans up to $n$ predecessors, write the total work as a summation and give its closed form and Big-O.
  2. (Ch. 11) The longest chain in the divisors-of-$2^k$ poset $\{1,2,4,\dots,2^k\}$ under $\mid$ has what length, as a function of $k$? Is that set a chain or an antichain?
  3. (Ch. 8) We used "mutual subsets are equal" to prove antisymmetry of $\subseteq$. State the set-equality principle from Chapter 8 that licenses this, in the form "$A = B$ iff …".
  4. (Ch. 8) For $S = \{a, b, c\}$, how many elements does the Boolean algebra $(\mathcal{P}(S), \subseteq)$ have, and what are its bottom $0$ and top $1$?

Answers

  1. $\sum_{i=1}^{n} i = \frac{n(n+1)}{2}$ (the triangular-number closed form from Chapter 11), which is $O(n^2)$. 2. Length $k+1$ (the elements $2^0, 2^1, \dots, 2^k$, all comparable since each divides the next) — it is a chain. 3. $A = B$ iff $A \subseteq B$ and $B \subseteq A$ (set equality by mutual inclusion). 4. $|\mathcal{P}(S)| = 2^3 = 8$ elements; bottom $0 = \emptyset$, top $1 = S = {a,b,c}$.

What's Next

You can now rank things (partial orders), flatten a ranking into a sequence (topological sort), combine things (lattices), and you've seen the one algebraic structure — Boolean algebra — that ties this book's logic, sets, and hardware together. What we have not yet done is ask how expensive these operations are. The topo_sort you just wrote runs in time proportional to the number of nodes plus edges — but to say that precisely, and to compare it against alternatives, you need a language for the cost of an algorithm. Chapter 14 introduces that language: Big-O, Big-Ω, and Big-Θ, the asymptotic notation that lets us reason about running time independent of the machine. It is the analytical spine of everything that follows — every graph algorithm in Part V, every recurrence in Part III, is ultimately measured with the tools you'll build next.