Your intuition on the second point is spot-on: if
What rescues the 64-bit hint is a see-saw trade-off between the bit-length of the dropped largest prime (uint64_t + uint16_t), the exact proof and construction of the 64-bit hint H given pushprime(p) / inc_last_prime_exponent(p) state machine.
In Scheme B, the 64-bit word W chains the odd prime factors 1 of 1 of
From W alone, two hardware instructions give us the exact endpoints of the chain:
where
To unpack W, the decoder only needs to know which integer partition of
was used. If the number of realizable partitions uint16_t shape_id uniquely indexes the partition inside bucket
Theorem 1: For every $S \in [1, 63]$ , the number of realizable prime step-partitions satisfies $N(S) < 2^{16} = 65,536$ .
Proof:
At first glance, the unrestricted partition number
For each integer step size
For small
$s = 1 \implies p_{\min}(1) = 3 \implies \delta_{\min}(1) = \log_2(3) - 1 \approx 0.58496\text{ bits}$ $s = 2 \implies p_{\min}(2) = 5 \implies \delta_{\min}(2) = \log_2(5) - 2 \approx 0.32193\text{ bits}$ $s = 3 \implies p_{\min}(3) = 11 \implies \delta_{\min}(3) = \log_2(11) - 3 \approx 0.45943\text{ bits}$ $s = 4 \implies p_{\min}(4) = 17 \implies \delta_{\min}(4) = \log_2(17) - 4 \approx 0.08746\text{ bits}$
For any partition
Now split any partition of
-
Rapid decay of
$p_{\ge 4}(m)$ : Removing parts$1, 2, 3$ collapses the partition function by nearly two orders of magnitude:
-
Tight excess cap on
$(c_1, c_2, c_3)$ : By the excess inequality,$(c_1, c_2, c_3)$ must satisfy:
Summing
-
When
$S$ is large ($58 \le S \le 63$ ):$64 - S \le 6$ forces$c_1 + c_2 + c_3$ to be very small, and$N(S)$ peaks at$S = 58$ with$N(58) \le 51,890 < 65,536$ . -
When
$S$ is medium ($45 \le S \le 57$ ): As$64 - S$ grows, every increase in$t = c_1 + 2c_2 + 3c_3$ shrinks the argument$m = S - t$ of$p_{\ge 4}(m)$ exponentially fast, keeping$N(S) < 54,000 < 65,536$ . -
When
$S$ is small ($S \le 42$ ): Even the unconstrained partition function satisfies$p(S) \le p(42) = 53,174 < 65,536$ .$\blacksquare$
(Note: Even if you ignored $\delta_{\min}(s)$ completely, whenever $S \le 63$, the top $63 - S$ bits of W above bit $P_r$ are unused zeros, giving $16 + (63 - S)$ header bits—which exceeds $\lceil \log_2 p(S) \rceil$ for every $S \in [1, 63]$ with room to spare.)
Now let us address your exact counterexample: What happens when
As you pointed out:
- For a semiprime
$n = q_1 q_2$ ($r = 2$ ),$q_1 \le \sqrt{n} < 2^{32}$ , so$s_1 \le 31$ and dropping$q_2$ saves at least$32$ bits. -
But for
$r = 8$ primes near$2^8$ (e.g.,$n = 131 \times 257^7 < 2^{64}$ ), dropping the single largest prime$q_8 = 257$ saves only$\approx 8$ bits! The remaining$7$ primes still have$\sum_{j=1}^7 s_j = 7 + 6 \times 8 = 55$ bits. If we stored a 55-bitWplus a 55-bit fencepost maskF, we would need$110$ bits—far beyond 64 bits.
The reason W took W kept the fencepost 1-bit at the boundary of each prime inside the data word itself.
When we instead separate the partition header from the prime bits, how many bits of actual prime data do we need for each odd prime
- Bit
$s_j$ (the MSB of$q_j$ ) is **always1**. - Bit
$0$ (the LSB of$q_j$ ) is **always1**(because$q_j \ge 3$ is odd). - Therefore,
$q_j = 2^{s_j} + 2u_j + 1$ is completely determined by only$m_j = s_j - 1$ interior bits:
Notice what happens when we count the total number of interior bits tzcnt(n)):
For every integer
Proof:
-
Saving from dropping the largest prime
$q_r$ : Because$q_r = \max_{1 \le j \le r} q_j \ge \left(\prod_{j=1}^r q_j\right)^{1/r}$ , the product of the first$r - 1$ odd primes satisfies:
Since
-
Saving from stripping both MSB and LSB of each of the
$r - 1$ odd primes: Each of the$r - 1$ primes has$s_j - 1$ interior bits, which subtracts an additional$(r - 1)$ bits:
-
Maximizing over all
$r \ge 2$ : By AM-GM (or setting$\frac{d}{dr}(64 - \frac{64}{r} - r) = \frac{64}{r^2} - 1 = 0$ ), the function$64 - \frac{64}{r} - r$ attains its global maximum at$r = \sqrt{64} = 8$ :
Look at the see-saw effect across
| Number of Odd Prime Factors ( |
Bits Saved by Dropping |
Bits Saved by Stripping LSBs ( |
Max Sum of Step Sizes ( |
Max Interior Bits ( |
Free Bits Left in 64-Bit H ( |
|---|---|---|---|---|---|
|
|
|
|
|
30 bits | 34 bits |
|
|
|
|
|
40 bits | 24 bits |
|
|
|
|
44 bits | 20 bits | |
|
|
|
|
48 bits | 16 bits | |
|
|
|
|
|
48 bits | 16 bits |
|
|
|
|
47 bits | 17 bits | |
|
|
|
|
41 bits | 23 bits | |
|
|
|
|
|
0 bits | 64 bits |
-
When
$r$ is small ($r = 2, 3, 4$ ): Dropping$q_r$ removes$16$ to$32$ bits. -
When
$r$ is large ($r \ge 10$ ): Dropping$q_r$ saves very little, but stripping the MSB and LSB from each of the$r - 1$ primes removes$r - 1$ bits! - Even at the exact worst-case peak
$r = 8$ (achieved by$n = 131 \times 257^7 < 2^{64}$ or the 8 distinct primes$131 \times 257 \times 263 \times 269 \times 271 \times 277 \times 281 \times 283 < 2^{64}$ , where $s = (7, 8, 8, 8, 8, 8, 8, 8)$), the interior bits$u_1, \dots, u_7$ take$6 + 6 \times 7 = \mathbf{48\text{ bits}}$ , leaving$64 - 48 = 16\text{ full bits}$ free inside the single 64-bit hintH!
Because
-
Bits
0 .. M_{r-1} - 1(at most 48 bits): Concatenated interior bits$u_1, u_2, \dots, u_{r-1}$ of the first$r - 1$ odd primes. -
Bits
48 .. 63(top 16 bits, or top 18 bits with prefix sharing): The prefix-partition indexshape_idof$(s_1 \le s_2 \le \dots \le s_{r-1})$ . Wait: how many valid prefix partitions$(s_1 \le s_2 \le \dots \le s_{r-1})$ exist across all$n < 2^{64}$ ? Because there is an omitted$r$ -th prime$q_r \ge q_{r-1}$ , the largest part$s_{r-1}$ in the prefix partition is counted twice in the bit budget:
This double weight on 46 and 47 of H are also free (giving 18 header bits), whereas for SHAPE_TABLE[H >> 46], where SHAPE_TABLE[H >> 46] returns
- Extract
$e_2 = \text{tzcnt}(n)$ and compute$n_{\text{odd}} = n \gg e_2$ . If$n_{\text{odd}} == 1$ , stop. - Fetch the step-size mask from
SHAPE_TABLE[H >> 46]. - Loop over the
$r - 1$ step sizes$s_1, \dots, s_{r-1}$ : slice$s_j - 1$ bits$u_j$ from the low bits ofH, reconstruct$q_j = (1\text{ULL} \ll s_j) \mid (u_j \ll 1) \mid 1$ , and accumulate the product$P \gets P \cdot q_j$ . - Recover the omitted largest prime via one 64-bit division:
Assume primes arrive in strictly increasing order pushprime(p) and inc_last_prime_exponent(p) run in
In the overlapped prime word W, appending another prime pushprime(p) or a repeated prime via inc_last_prime_exponent(p)) is the exact same operation, because the current top set bit of W is always
#include <stdint.h>
typedef struct {
uint64_t W; // Overlapped prime bits (initialized to W = 1 for n = 1)
uint64_t F; // Internal fencepost bitmask (initialized to F = 0)
} PackedFactor126;
static inline void append_factor_126(PackedFactor126 *st, uint64_t p) {
if (p == 2) {
// Since p1 < p2 < ..., p = 2 always arrives first when W = 2^e2 and F = 0
st->W <<= 1;
return;
}
int p_cur = 63 - __builtin_clzll(st->W); // Current highest fencepost bit
int s = 63 - __builtin_clzll(p); // Step size floor(log2(p))
st->W |= (p << p_cur); // Overlap p's LSB=1 with W's MSB=1
st->F |= (1ULL << (p_cur + s)); // Record new fencepost in F
}
// Both run in O(1) (~4 ALU instructions, 0 memory lookups):
static inline void pushprime_126(PackedFactor126 *st, uint64_t p) {
append_factor_126(st, p);
}
static inline void inc_last_prime_exponent_126(PackedFactor126 *st, uint64_t p) {
append_factor_126(st, p);
}Two things happen when we compress the fenceposts into shape_id and omit the largest prime H:
-
Why
shape_idupdates in$O(1)$ : Because the input primes arrive in strictly increasing order$p_1 < p_2 < \dots < p_k$ , their step sizes$s_j = \lfloor \log_2 q_j \rfloor$ arrive in non-decreasing order$s_1 \le s_2 \le \dots \le s_{r-1}$ ! Every valid sorted partition$\lambda = (s_1 \le \dots \le s_{r-1})$ is a node in a Directed Acyclic Graph (partition trie) rooted at the empty partition$\emptyset$ (shape_id = 0). Appending a new step$s \ge s_{\text{last}}$ is a single$O(1)$ state-machine transition:
-
How to omit the last prime when you don't know when the stream ends ("1-Element Delayed Commit"):
At any moment when
$r \ge 1$ odd primes have been added to$n$ ,Hholds only the first$r - 1$ odd primes$q_1, \dots, q_{r-1}$ , while the$r$ -th (most recent) odd prime$q_r$ is **held implicitly inn**(not yet committed toH).
- Look at what happens when
inc_last_prime_exponent(p)is called: Because we are incrementing the exponent of$p$ , the previously uncommitted last prime was$q_r = p$ , and the new last prime is also$q_{r+1} = p$ ! Since **pis passed directly intoinc_last_prime_exponent(p)**, we don't even have to recover$q_r$ —we simply commitpintoHright now, and multiplyn *= pso the new copy of$p$ remains the uncommitted last prime! - And what when
pushprime(p)is called for a new odd prime$p$ ? The previous prime$q_r$ is committed toH(either passed from a register in the builder or recovered in$O(1)$ via $q_r = (n \gg \text{tzcnt}(n)) / \text{product}(H)$), and the new prime$p$ becomes the new uncommitted last prime vian *= p!
typedef struct {
uint64_t n; // The number itself (initialized to 1)
uint64_t H; // 64-bit hint: [shape_id (top 18b) | interior bits U (low <=48b)]
} FactorHint64;
// Helper: commits an odd prime q into H in O(1) time
static inline void commit_odd_prime_to_H(FactorHint64 *st, uint64_t q) {
uint32_t shape_id = (uint32_t)(st->H >> 46);
uint8_t m_cur = SHAPE_INTERIOR_BITS[shape_id]; // Current bit offset M_{r-1}
int s = 63 - __builtin_clzll(q);
uint64_t u = (q >> 1) & ((1ULL << (s - 1)) - 1ULL); // s - 1 interior bits
uint32_t next_id = NEXT_SHAPE[shape_id][s]; // O(1) partition DAG transition
uint64_t low_mask = (1ULL << m_cur) - 1ULL;
st->H = ((uint64_t)next_id << 46) | (u << m_cur) | (st->H & low_mask);
}
// O(1) increment of the last prime's exponent:
static inline void inc_last_prime_exponent_64(FactorHint64 *st, uint64_t p) {
if (p > 2) {
// The previous copy of p was the uncommitted last prime; commit it now!
commit_odd_prime_to_H(st, p);
}
st->n *= p; // The new copy of p stays uncommitted in n
}
// O(1) push of a strictly larger prime p:
static inline void pushprime_64(FactorHint64 *st, uint64_t p, uint64_t prev_odd_p) {
if (p > 2 && prev_odd_p > 2) {
// Commit the previous largest odd prime before p becomes the new largest
commit_odd_prime_to_H(st, prev_odd_p);
}
st->n *= p;
}(If pushprime_64 is not passed prev_odd_p, it simply computes prev_odd_p = (st->n >> __builtin_ctzll(st->n)) / decode_prefix_product(st->H) in $O(1)$ time before calling commit_odd_prime_to_H.)
Use the interactive visualizer below to step through pushprime(p) and inc_last_prime_exponent(p) in real time, see how the "lazy last factor" commits into H, and inspect the 48-bit see-saw bound
Key takeaway: Dropping the largest prime
$q_r$ saves$\lfloor 64/r \rfloor$ bits (which dominates for small$r$ ), while stripping the fixed MSB1and LSB1from the remaining$r - 1$ odd primes saves$r - 1$ bits (which dominates for large$r$ ). Together, the interior bits of$q_1, \dots, q_{r-1}$ never exceed$64 - \lfloor 64/r \rfloor - r \le 48\text{ bits}$ , leaving at least$16$ bits in a singleuint64_thintHfor the partition state machine.