Exercises — Module 13 (Bitwise Tricks & BDDs)
Self-contained problems on this module's material — the rightmost-bit identities, Gosper's hack, sideways addition and de Bruijn cycles, BDD canonicity, model counting, the ordering problem, and ZDDs. You can work every one without the books: each states the problem in full, gives a hint to peek at when stuck, and a worked answer sketch to check against after you try. Computational answers here are reproduced by the code you write in the lab (or a few lines at a REPL).
Ratings follow Knuth's scale (00–50; M = needs mathematics, ▶ = especially
instructive). Exercise numbers follow the Vol. 4A first edition; where a printing
differs, match by content.
Tracking
| # | Topic | Rating | Status |
|---|---|---|---|
| 1 | The four rightmost-bit identities, including | 10 | ⬜ |
| 2 | ▶ Re-derive Gosper; prove G3's division exact | 20 | ⬜ |
| 3 | Per-byte counts; a -bit block popcount | 22 | ⬜ |
| 4 | ▶ Count de Bruijn cycles; both by hand | 25 | ⬜ |
| 5 | Median under two orders; symmetric order-free | 15 | ⬜ |
| 6 | ▶ Canonicity for : all functions and diagrams | 25 | ⬜ |
| 7 | The ZDD skip lemma (factor ) and adapted counting | 20 | ⬜ |
| 8 | ▶ Hidden weighted bit: exponential for every order | 30 | ⬜ |
Problems
1. The four rightmost-bit identities, including (rating 10 · cf. 7.1.3-warmup)
Problem. Write a nonzero word as ( trailing zeros,
the high bits). Prove the four identities of the lesson's table — the
binary form and effect of x & (x-1), x | (x-1), x ^ (x-1), and x & (-x) —
and state what each does when .
Hint. Two facts do all the work: (borrow ripples through the trailing zeros), and (the carry stops at the lowest ). Then AND/OR/XOR region by region.
Answer sketch. With and :
| expression | result | effect |
|---|---|---|
x & (x-1) | drop the rightmost | |
x | (x-1) | smear the rightmost downward | |
x ^ (x-1) | mask of the rightmost and all below | |
x & (-x) | extract the rightmost () |
Hand-checked on (, ): drop ,
smear , xor , extract — all reproduced by a
REPL. Boundary : extraction gives
(nothing to extract, a well-defined answer); the smear gives all ones (the borrow makes the full mask). Both follow
from the wrapping mod- arithmetic, which is why Rust makes you spell out
wrapping_neg / wrapping_sub.
2. ▶ Re-derive Gosper; prove G3's division exact (rating 20 · cf. 7.1.3-20)
Problem. Derive Gosper's hack — the next-larger word of the same weight —
from the picture (rightmost run of ones
above zeros). Show the successor is , and
prove that the division (x ^ v) / u in step G3 (with ,
) is always exact.
Hint. To grow minimally, set the lowest that can be "paid for" — the just above the run — then repack the leftover ones as low as possible. For the division, ask how many trailing zeros has.
Answer sketch. Derivation. Any larger word must set some -bit; the cheapest choice is the immediately above the rightmost run (setting a higher costs more, a lower one gives ). Setting it turns into a word with one surplus above; delete the run's ones and re-add ones packed at the very bottom, giving — this is the minimal same-weight increase (an exchange argument: the forced high bit is fixed, the remaining ones sit as low as possible). The algorithm computes exactly this: isolates the run's low bit; carries through the run to ; and exposes the vacated/new ones over trailing zeros. Exactness: ends in exactly zeros, and , so is an integer — it right-justifies to ; shifting right by drops the two ones already accounted for by , leaving the owed . Traced on (, , ): , , , , , result — and enumerating this way produces all three-subsets of a -set in increasing order (checked at a REPL).
3. Per-byte counts; a -bit block popcount (rating 22 · cf. 7.1.3-swar)
Problem. Adapt Algorithm S (sideways addition) to leave, in each byte, the population count of that byte — i.e. stop after step S3, before the final multiply. Then use it to compute (total -bits) of a -bit block with one multiply per -bit word.
Hint. After S3 each byte already holds its own count (each nibble is ,
so a byte sum is and fits). The final multiply
(x * 0x0101010101010101) >> 56 sums the eight byte-counts of one word into its
top byte.
Answer sketch. Steps S1–S3 unchanged:
x -= (x >> 1) & 0x5555555555555555; // pairs
x = (x & 0x3333333333333333) + ((x >> 2) & 0x3333333333333333); // nibbles
x = (x + (x >> 4)) & 0x0f0f0f0f0f0f0f0f; // each BYTE = its popcount
Stopping here gives eight independent byte-counts in one word. For a -bit
block = eight -bit words : run S1–S3 on each word, then the
final multiply-and-shift per word yields that word's total , and summing
the eight totals gives the block popcount. (Alternatively, accumulate the eight
post-S3 byte vectors with saturating-safe adds — each byte , eight of them
, so no byte overflows — and do a single closing multiply.) Verified
at a REPL: the SWAR routine agrees with a reference popcount on random words,
and a random -bit block totals correctly (e.g. set bits, matching a
naive bit count). About a dozen branchless instructions per word, no table, no
loop — the algorithm inside hardware POPCNT.
4. ▶ Count de Bruijn cycles; both by hand (rating 25 · cf. 7.1.3-debruijn)
Problem. A binary de Bruijn cycle is a cyclic sequence of bits
in which all length- windows are distinct. The number of such cycles is
(de Bruijn 1946). Verify the formula by hand for — it
predicts , so exhibit both cycles — and deduce how many usable
-bit ruler constants (a beginning with six bits) exist.
Hint. A valid ruler constant must start with six bits so that left-shifts
feed zeros into the low end and cleanly replay the cycle. Enumerate length-
cyclic binary sequences whose eight -windows are all distinct.
Answer sketch. For : . Enumerating all length- cyclic sequences with eight distinct -windows (up to rotation) yields exactly two necklaces:
The lesson's constant is the first; its left-shifts
produce the eight distinct top--bit windows — a perfect hash from shift amount to window, which is exactly why one table
lookup recovers the trailing-zero count. For : distinct cycles; those beginning with six bits
number as well, so there are usable -bit ruler constants —
0x03f79d71b4cb0a89 being one. Build the inverse table from the identity
table[(D << a) >> 58] = a; never copy a magic table you cannot regenerate.
5. Median under two orders; symmetric order-free (rating 15 · cf. 7.1.4-median)
Problem. Draw the ROBDD of the majority function (true when inputs are true) under the order and again under . Verify both times, and prove the general fact: a symmetric Boolean function has the same BDD size under every variable order.
Hint. A symmetric function's value depends only on how many inputs are , not which. A change of variable order just relabels the inputs.
Answer sketch. Under , the reduced diagram has one -node, two -nodes, one -node, and the two sinks — (the duplicate lower nodes merge under rule R2, as the lesson's median diagram shows). A REPL build confirms nodes and satisfying assignments (). Order-independence: majority is symmetric — it equals iff the input weight , which is unchanged by any permutation of the inputs. Reordering the variables from to is exactly applying such a permutation; the resulting function of the reordered inputs is the same symmetric function, so by the canonicity theorem its ROBDD is isomorphic to the first — identical node counts, again. In general, for any symmetric and any two orders, the two ROBDDs are relabelings of one another, so is order-invariant. (This is the rare happy case; the next problem's cousin — and §2.5's couples function — show how badly non-symmetric functions depend on order.)
6. ▶ Canonicity for : all functions and diagrams (rating 25 · cf. 7.1.4-canon)
Problem. Make the canonicity theorem concrete for : enumerate all Boolean functions of (order ) and give each one's unique reduced ordered BDD, confirming the node count . Group them by structure.
Hint. Build each function from its minterms and reduce with rules R1 (drop a node with LO HI) and R2 (merge duplicate triples). A function that ignores a variable has no node for it.
Answer sketch. All diagrams are distinct (that is the theorem for ), and they fall into four structural classes by node count (sinks included), reproduced by a REPL that computes for every truth table:
| class | functions | |
|---|---|---|
| constants | (), () | (a bare sink) |
| single literal | (one branch sinks) | |
| two-input, non-parity | and the four "one input negated" gates (e.g. ) | (two branches sinks) |
| parity | (three branches sinks) |
That is functions with node counts across truth tables –. Each row of the canonicity induction is visible here: constants are the base case (); the literals are functions where a cofactor is constant (Case 1, a variable drops out); the parity functions are the extreme Case 2 where both cofactors differ and depend on the deeper variable, forcing the full three-branch shape. No two of the share a diagram, so equivalence testing is pointer comparison — exactly the payoff the theorem promises.
7. The ZDD skip lemma (factor ) and adapted counting (rating 20 · cf. 7.1.4-count)
Problem. In a BDD, a skipped level contributes a factor to the model count, because a skipped variable is free. Zero-suppressed decision diagrams (ZDDs) reinterpret a skipped variable as absent from the set, not free. Prove the ZDD version of the skip lemma — a skipped level multiplies by , not — and adapt Algorithm C (counting) accordingly.
Hint. Compare the reduction rules. A BDD deletes nodes with LO HI (the variable is irrelevant free two extensions). A ZDD deletes nodes whose HI edge points to (the variable, if untested, is absent one extension).
Answer sketch. Let count the sets (families) accepted from node over the variables . In a ZDD, a node deleted by the zero-suppression rule is one whose HI child is : taking that variable to be leads to rejection, so every accepted set has that variable — the variable is forced absent, not free. Hence when an edge skips from level to a child at level , each skipped variable is pinned to (absent), contributing a factor of rather than . The skip-weighted recurrence becomes
i.e. drop every factor — a two-line change to Algorithm C, and
no top-of-root factor either. This is exactly why ZDDs compress families of sparse
subsets: "the other of elements are absent" costs no nodes and no count
factors, whereas the BDD would pay of free-variable weight to say the same
thing. (Ground it against §2.6: same mk/unique-table/memoized-apply machinery,
adjusted skip factor.)
8. ▶ Hidden weighted bit: exponential for every order (rating 30 · cf. 7.1.4-hidden)
Problem. The hidden weighted bit function is (with ): the output is the input bit whose index equals the input's population count. Bryant (1991) proved that every variable order gives an exponentially large BDD. Reproduce the core of the argument — the distinguishing-set idea from §2.5 — in your own words, and explain why this is the worst case that variable reordering cannot fix.
Hint. The §2.5 lower bound found pairwise-distinct residual functions and concluded nodes. For the point is that no order lets a BDD level "forget" enough: after reading half the variables (in any order), the count is still undetermined, so which bit is selected is still open.
Answer sketch. The general lower-bound engine (§2.5): if, cutting the variable order at some level, the function has pairwise-distinct residual subfunctions of the not-yet-read variables, then the BDD has nodes at that cut — because the node reached by each partial assignment determines its residual, so distinct residuals force distinct nodes (a distinguishing/fooling set, kin to Myhill–Nerode). For , Bryant shows that for any order there is a cut (around the middle) across which exponentially many assignments to the read variables leave genuinely different residual functions: the selected index can still land on either side of the cut depending on the unread bits, so the residual must "remember" both the partial popcount and the values of many read bits that might yet be selected — and these memories are pairwise distinguishable. That forces nodes at the cut, for every order. Because the bound holds for all orders simultaneously, reordering heuristics (sifting) cannot rescue it — is the standard witness that some functions have no good BDD order at all, the definitive limit on the whole representation. (This problem is a read-and-reproduce of Bryant's proof; the distinguishing-set skeleton above is the transferable core, grounded in §2.5's -residuals argument.)
Your solutions
Use this space to log your own work — a restated problem, your approach, and how it compared with the sketch above.
Exercise N (rating RR)
Approach. Answer / proof. Compared with the sketch: what you did differently, what you missed.