Skip to main content

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​

#TopicRatingStatus
1The four rightmost-bit identities, including x=0x = 010⬜
2▶ Re-derive Gosper; prove G3's division exact20⬜
3Per-byte counts; a 512512-bit block popcount22⬜
4▶ Count B(2,n)B(2,n) de Bruijn cycles; both B(2,3)B(2,3) by hand25⬜
5Median under two orders; symmetric ⇒\Rightarrow order-free15⬜
6▶ Canonicity for n=2n = 2: all 1616 functions and diagrams25⬜
7The ZDD skip lemma (factor 11) and adapted counting20⬜
8▶ Hidden weighted bit: exponential for every order30⬜

Problems​

1. The four rightmost-bit identities, including x=0x = 0 (rating 10 · cf. 7.1.3-warmup)​

Problem. Write a nonzero word as x=α 1 0ax = \alpha\, 1\, 0^a (aa trailing zeros, α\alpha 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 x=0x = 0.

Hint. Two facts do all the work: x−1=α 0 1ax - 1 = \alpha\, 0\, 1^a (borrow ripples through the trailing zeros), and −x=xˉ+1=αˉ 1 0a-x = \bar x + 1 = \bar\alpha\, 1\, 0^a (the carry stops at the lowest 11). Then AND/OR/XOR region by region.

Answer sketch. With x=α 1 0ax = \alpha\, 1\, 0^a and x−1=α 0 1ax - 1 = \alpha\, 0\, 1^a:

expressionresulteffect
x & (x-1)α 0 0a\alpha\, 0\, 0^adrop the rightmost 11
x | (x-1)α 1 1a\alpha\, 1\, 1^asmear the rightmost 11 downward
x ^ (x-1)0…0 1 1a0\ldots0\, 1\, 1^amask of the rightmost 11 and all below
x & (-x)0…0 1 0a0\ldots0\, 1\, 0^aextract the rightmost 11 (=2a= 2^a)

Hand-checked on x=01011000x = 01011000 (α=0101\alpha = 0101, a=3a = 3): drop =01010000= 01010000, smear =01011111= 01011111, xor =00001111= 00001111, extract =00001000= 00001000 — all reproduced by a REPL. Boundary x=0x = 0: extraction gives 0 & (−0)=0 & 0=00\, \&\, (-0) = 0\,\&\,0 = 0 (nothing to extract, a well-defined answer); the smear gives 0 ∣ (0−1)=0 ∣ (264−1)=0\,|\,(0 - 1) = 0\,|\,(2^{64}-1) = all ones (the borrow makes x−1x - 1 the full mask). Both follow from the wrapping mod-2642^{64} 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 x=α 0 1b 0ax = \alpha\, 0\, 1^b\, 0^a (rightmost run of b≥1b \ge 1 ones above a≥0a \ge 0 zeros). Show the successor is α 1 0a+1 1b−1\alpha\, 1\, 0^{a+1}\, 1^{b-1}, and prove that the division (x ^ v) / u in step G3 (with u=x & (−x)u = x\,\&\,(-x), v=x+uv = x + u) is always exact.

Hint. To grow xx minimally, set the lowest 00 that can be "paid for" — the 00 just above the run — then repack the leftover ones as low as possible. For the division, ask how many trailing zeros x⊕vx \oplus v has.

Answer sketch. Derivation. Any larger word must set some 00-bit; the cheapest choice is the 00 immediately above the rightmost run (setting a higher 00 costs more, a lower one gives y<xy < x). Setting it turns α 0 1b 0a\alpha\, 0\, 1^b\, 0^a into a word with one surplus 11 above; delete the run's bb ones and re-add b−1b - 1 ones packed at the very bottom, giving next=α 1 0a+1 1b−1\text{next} = \alpha\, 1\, 0^{a+1}\, 1^{b-1} — this is the minimal same-weight increase (an exchange argument: the forced high bit is fixed, the remaining b−1b - 1 ones sit as low as possible). The algorithm computes exactly this: u=x & (−x)=2au = x\,\&\,(-x) = 2^a isolates the run's low bit; v=x+uv = x + u carries through the run to α 1 0a+b\alpha\, 1\, 0^{a+b}; and x⊕v=0…0 1b+1 0ax \oplus v = 0\ldots0\, 1^{b+1}\, 0^a exposes the b+1b + 1 vacated/new ones over aa trailing zeros. Exactness: x⊕vx \oplus v ends in exactly aa zeros, and u=2au = 2^a, so (x⊕v)/u(x \oplus v)/u is an integer — it right-justifies to 1b+11^{b+1}; shifting right by 22 drops the two ones already accounted for by vv, leaving the owed 1b−11^{b-1}. Traced on x=01011100x = 01011100 (α=010\alpha = 010, b=3b = 3, a=2a = 2): u=00000100u = 00000100, v=01100000v = 01100000, x⊕v=00111100x \oplus v = 00111100, /u=00001111/u = 00001111, ≫2=00000011\gg 2 = 00000011, result 0110001101100011 — and enumerating this way produces all (123)=220\binom{12}{3} = 220 three-subsets of a 1212-set in increasing order (checked at a REPL).

3. Per-byte counts; a 512512-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 ν(x)\nu(x) (total 11-bits) of a 512512-bit block with one multiply per 6464-bit word.

Hint. After S3 each byte already holds its own count (each nibble is ≤4\le 4, so a byte sum is ≤8\le 8 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 512512-bit block = eight 6464-bit words w0,…,w7w_0, \ldots, w_7: run S1–S3 on each word, then the final multiply-and-shift per word yields that word's total ν(wi)\nu(w_i), and summing the eight totals gives the block popcount. (Alternatively, accumulate the eight post-S3 byte vectors with saturating-safe adds — each byte ≤8\le 8, eight of them ≤64<256\le 64 < 256, so no byte overflows — and do a single closing multiply.) Verified at a REPL: the SWAR routine agrees with a reference popcount on 10410^4 random words, and a random 512512-bit block totals correctly (e.g. 258258 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 B(2,n)B(2,n) de Bruijn cycles; both B(2,3)B(2,3) by hand (rating 25 · cf. 7.1.3-debruijn)​

Problem. A binary de Bruijn cycle B(2,n)B(2, n) is a cyclic sequence of 2n2^n bits in which all 2n2^n length-nn windows are distinct. The number of such cycles is 2 2n−1−n2^{\,2^{n-1} - n} (de Bruijn 1946). Verify the formula by hand for n=3n = 3 — it predicts 22, so exhibit both B(2,3)B(2, 3) cycles — and deduce how many usable 6464-bit ruler constants (a B(2,6)B(2, 6) beginning with six 00 bits) exist.

Hint. A valid ruler constant must start with six 00 bits so that left-shifts feed zeros into the low end and cleanly replay the cycle. Enumerate length-88 cyclic binary sequences whose eight 33-windows are all distinct.

Answer sketch. For n=3n = 3: 2 22−3=24−3=21=22^{\,2^{2} - 3} = 2^{4-3} = 2^1 = 2. Enumerating all length-88 cyclic sequences with eight distinct 33-windows (up to rotation) yields exactly two necklaces:

00011101and00010111.00011101 \qquad\text{and}\qquad 00010111.

The lesson's constant D=0x1D=00011101D = \texttt{0x1D} = 00011101 is the first; its left-shifts produce the eight distinct top-33-bit windows 000,001,011,111,110,101,010,100000, 001, 011, 111, 110, 101, 010, 100 — a perfect hash from shift amount to window, which is exactly why one table lookup recovers the trailing-zero count. For n=6n = 6: 2 25−6=232−6=2262^{\,2^{5} - 6} = 2^{32 - 6} = 2^{26} distinct B(2,6)B(2, 6) cycles; those beginning with six 00 bits number 2262^{26} as well, so there are 2262^{26} usable 6464-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 ⇒\Rightarrow order-free (rating 15 · cf. 7.1.4-median)​

Problem. Draw the ROBDD of the majority function ⟨x1x2x3⟩\langle x_1 x_2 x_3\rangle (true when ≥2\ge 2 inputs are true) under the order x1<x2<x3x_1 < x_2 < x_3 and again under x3<x1<x2x_3 < x_1 < x_2. Verify B(f)=6B(f) = 6 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 11, not which. A change of variable order just relabels the inputs.

Answer sketch. Under x1<x2<x3x_1 < x_2 < x_3, the reduced diagram has one x1x_1-node, two x2x_2-nodes, one x3x_3-node, and the two sinks — B=6B = 6 (the duplicate lower nodes merge under rule R2, as the lesson's median diagram shows). A REPL build confirms 66 nodes and 44 satisfying assignments (011,101,110,111011, 101, 110, 111). Order-independence: majority is symmetric — it equals 11 iff the input weight ν(x)≥2\nu(x) \ge 2, which is unchanged by any permutation π\pi of the inputs. Reordering the variables from x1x2x3x_1 x_2 x_3 to x3x1x2x_3 x_1 x_2 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, B=6B = 6 again. In general, for any symmetric ff and any two orders, the two ROBDDs are relabelings of one another, so B(f)B(f) 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 n=2n = 2: all 1616 functions and diagrams (rating 25 · cf. 7.1.4-canon)​

Problem. Make the canonicity theorem concrete for n=2n = 2: enumerate all 1616 Boolean functions of x0,x1x_0, x_1 (order x0<x1x_0 < x_1) and give each one's unique reduced ordered BDD, confirming the node count B(f)B(f). 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 1616 diagrams are distinct (that is the theorem for n=2n = 2), and they fall into four structural classes by node count (sinks included), reproduced by a REPL that computes B(f)B(f) for every truth table:

classfunctionsB(f)B(f)
constants⊥\bot (00000000), ⊤\top (11111111)11 (a bare sink)
single literalx0, ¬x0, x1, ¬x1x_0,\ \lnot x_0,\ x_1,\ \lnot x_133 (one branch ++ 22 sinks)
two-input, non-parity∧, ¬(∧), ∨, ¬(∨)\land,\ \lnot(\land),\ \lor,\ \lnot(\lor) and the four "one input negated" gates (e.g. ¬x0∧x1\lnot x_0 \land x_1)44 (two branches ++ 22 sinks)
parityx0⊕x1, ¬(x0⊕x1)x_0 \oplus x_1,\ \lnot(x_0 \oplus x_1)55 (three branches ++ 22 sinks)

That is 2+4+8+2=162 + 4 + 8 + 2 = 16 functions with node counts [1,4,4,3,4,3,5,4,4,5,3,4,3,4,4,1][1,4,4,3,4,3,5,4,4,5,3,4,3,4,4,1] across truth tables 00000000–11111111. Each row of the canonicity induction is visible here: constants are the base case (n=0n = 0); 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 1616 share a diagram, so equivalence testing is pointer comparison — exactly the payoff the theorem promises.

7. The ZDD skip lemma (factor 11) and adapted counting (rating 20 · cf. 7.1.4-count)​

Problem. In a BDD, a skipped level contributes a factor 2skip2^{\text{skip}} 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 11, not 22 — and adapt Algorithm C (counting) accordingly.

Hint. Compare the reduction rules. A BDD deletes nodes with LO == HI (the variable is irrelevant ⇒\Rightarrow free ⇒\Rightarrow two extensions). A ZDD deletes nodes whose HI edge points to ⊥\bot (the variable, if untested, is ⇒\Rightarrow absent ⇒\Rightarrow one extension).

Answer sketch. Let s(p)s(p) count the sets (families) accepted from node pp over the variables xℓ(p),…,xn−1x_{\ell(p)}, \ldots, x_{n-1}. In a ZDD, a node deleted by the zero-suppression rule is one whose HI child is ⊥\bot: taking that variable to be 11 leads to rejection, so every accepted set has that variable =0= 0 — the variable is forced absent, not free. Hence when an edge skips from level vv to a child at level w>v+1w > v + 1, each skipped variable xv+1,…,xw−1x_{v+1}, \ldots, x_{w-1} is pinned to 00 (absent), contributing a factor of 1 w−v−1=11^{\,w - v - 1} = 1 rather than 2 w−v−12^{\,w - v - 1}. The skip-weighted recurrence becomes

s(⊥)=0,s(⊤)=1,s(p)=1⋅s(LO(p))+1⋅s(HI(p)),s(\bot) = 0,\quad s(\top) = 1,\qquad s(p) = 1\cdot s(\mathrm{LO}(p)) + 1\cdot s(\mathrm{HI}(p)),

i.e. drop every 2skip2^{\text{skip}} 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 9595 of 100100 elements are absent" costs no nodes and no count factors, whereas the BDD would pay 2952^{95} 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 HWB(x)=xν(x)\mathrm{HWB}(x) = x_{\nu(x)} (with HWB(0)=0\mathrm{HWB}(0) = 0): the output is the input bit whose index equals the input's population count. Bryant (1991) proved that every variable order gives HWB\mathrm{HWB} 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 2k2^k pairwise-distinct residual functions and concluded ≥2k\ge 2^k nodes. For HWB\mathrm{HWB} the point is that no order lets a BDD level "forget" enough: after reading half the variables (in any order), the count ν(x)\nu(x) 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 MM pairwise-distinct residual subfunctions of the not-yet-read variables, then the BDD has ≥M\ge M 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 HWB\mathrm{HWB}, 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 ν(x)\nu(x) 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 2Ω(n)2^{\Omega(n)} nodes at the cut, for every order. Because the bound holds for all orders simultaneously, reordering heuristics (sifting) cannot rescue it — HWB\mathrm{HWB} 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 2k2^k-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.