Burnikel-Ziegler division in Python
Recursive division that reduces n-bit division to multiplications, giving subquadratic time.
- File
burnikel_ziegler_division.py, 108 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/burnikel-ziegler-division/. The authors are listed in prov/V436BS.jsonl.
"""Burnikel-Ziegler recursive division on numbers of limbs in base 2^w.
Numbers are Python ints read as limb arrays in base beta = 2^w; limbs are
taken out with shifts and masks. Products of big numbers are delegated to
Python multiplication, which plays the role of M(n), and every such product
is recorded in Stats so that the cost recurrence can be checked.
"""
from dataclasses import dataclass, field
@dataclass
class Stats:
"""Counters: products by size (in limbs) and correction steps."""
products: dict[int, int] = field(default_factory=dict)
corrections: int = 0
max_corrections: int = 0
basecase_calls: int = 0
def limbs(x: int, w: int) -> int:
"""The number of base-2^w limbs of x (0 has 0 limbs)."""
return (x.bit_length() + w - 1) // w
def _record(st: Stats, size: int) -> None:
st.products[size] = st.products.get(size, 0) + 1
def schoolbook_divrem(a: int, b: int, n: int, m: int, w: int, st: Stats) -> tuple[int, int]:
"""Long division of a < beta^m * b by a normalized n-limb b (Knuth's D)."""
st.basecase_calls += 1
beta = 1 << w
top = b >> (w * (n - 1))
q = 0
for j in range(m - 1, -1, -1):
hi = (a >> (w * (n + j - 1))) & ((1 << (2 * w)) - 1)
qj = min(hi // top, beta - 1)
a -= (qj * b) << (w * j)
while a < 0:
qj -= 1
a += b << (w * j)
q = (q << w) | qj
return q, a
def div_2n_1n(a: int, b: int, n: int, w: int, st: Stats, n0: int = 2) -> tuple[int, int]:
"""Divide 0 <= a < beta^n * b by a normalized n-limb b."""
if n % 2 == 1 or n <= n0: # @line L1
return schoolbook_divrem(a, b, n, n, w, st)
h = n // 2 # @line L2
mask = (1 << (w * h)) - 1
a4 = a & mask
a123 = a >> (w * h)
q1, r = div_3n_2n(a123, b, h, w, st, n0) # @line L3
q2, s = div_3n_2n((r << (w * h)) | a4, b, h, w, st, n0) # @line L4
return (q1 << (w * h)) | q2, s # @line L5
def div_3n_2n(a: int, b: int, h: int, w: int, st: Stats, n0: int = 2) -> tuple[int, int]:
"""Divide 0 <= a < beta^h * b by a normalized 2h-limb b = b1 beta^h + b2."""
mask = (1 << (w * h)) - 1
b1, b2 = b >> (w * h), b & mask
a12, a3 = a >> (w * h), a & mask
a1 = a12 >> (w * h)
if a1 < b1: # @line L6
q, r1 = div_2n_1n(a12, b1, h, w, st, n0)
else: # @line L7
q = mask
r1 = a12 - q * b1
_record(st, h)
r = (r1 << (w * h)) + a3 - q * b2 # @line L8
k = 0
while r < 0: # @line L9
q -= 1
r += b
k += 1
st.corrections += k
st.max_corrections = max(st.max_corrections, k)
return q, r # @line L10
def bz_divmod(a: int, b: int, w: int = 4, n0: int = 2, st: Stats | None = None) -> tuple[int, int]:
"""Return divmod(a, b) for a >= 0 and b > 0 by Burnikel-Ziegler.
Normalizes b so that its top limb has its high bit set, pads it to n
limbs, and divides a block of n limbs at a time from the top.
"""
if b <= 0 or a < 0:
raise ValueError("need a >= 0 and b > 0")
st = st if st is not None else Stats()
n = max(limbs(b, w), 1)
shift = n * w - b.bit_length()
bn, an = b << shift, a << shift
blocks = max((limbs(an, w) + n - 1) // n, 1)
if (an >> (w * n * (blocks - 1))) >= bn:
blocks += 1
mask = (1 << (w * n)) - 1
q, r = 0, 0
for i in range(blocks - 1, -1, -1):
chunk = (an >> (w * n * i)) & mask
qi, r = div_2n_1n((r << (w * n)) | chunk, bn, n, w, st, n0)
q = (q << (w * n)) | qi
return q, r >> shift
test_burnikel_ziegler_division.py 86 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/burnikel-ziegler-division/. The authors are listed in prov/V436BS.jsonl.
import random
import pytest
from burnikel_ziegler_division import Stats, bz_divmod, div_2n_1n, div_3n_2n
def test_example_base16():
# w = 4: beta = 16, b = 0xC3A5 has n = 4 limbs and is normalized.
a, b = 0x9F2E_71B4, 0xC3A5
st = Stats()
q, r = div_2n_1n(a, b, 4, 4, st, n0=1)
assert (q, r) == divmod(a, b) == (0xD049, 0x97A7)
# the second 3n/2n step needs exactly one correction (line L9)
assert st.corrections >= 1 and st.max_corrections <= 2
def test_exhaustive_small_base2():
for b in range(1, 64):
for a in range(0, 1 << 9):
assert bz_divmod(a, b, w=1, n0=1) == divmod(a, b)
def test_random_many_sizes():
rng = random.Random(1998)
for _ in range(600):
w = rng.choice((1, 2, 4, 8, 16))
nb = rng.randint(1, 48)
b = rng.getrandbits(nb * w) | 1
a = rng.getrandbits(rng.randint(0, 3 * nb * w))
st = Stats()
assert bz_divmod(a, b, w, n0=rng.choice((1, 2, 4)), st=st) == divmod(a, b)
assert st.max_corrections <= 2
def test_adversarial_top_limbs():
rng = random.Random(7)
for w in (2, 4, 8):
beta = 1 << w
for n in (2, 4, 8, 16, 32):
for _ in range(20):
b = (beta >> 1) << (w * (n - 1)) | rng.getrandbits(w * (n - 1))
q = rng.getrandbits(w * n)
r = rng.randint(0, b - 1)
a = q * b + r
st = Stats()
assert div_2n_1n(a, b, n, w, st, n0=1) == (q, r)
assert st.max_corrections <= 2
top = (1 << (w * n)) - 1
assert div_2n_1n(top * top, top, n, w, Stats(), n0=1) == (top, 0)
def test_product_counts():
# D(n) = 2 D3(n/2) and D3(h) = D(h) + one h x h product: for n = 2^k and
# n0 = 1, 2^j products of size n / 2^j occur at each level, j = 1..k.
rng = random.Random(3)
w = 4
for k in range(1, 7):
n = 1 << k
b = (1 << (w * n - 1)) | rng.getrandbits(w * n - 1)
a = rng.randint(0, (b << (w * n)) - 1)
st = Stats()
assert div_2n_1n(a, b, n, w, st, n0=1) == divmod(a, b)
expected = {n >> j: 1 << j for j in range(1, k + 1)}
assert st.products == expected
def test_3n_2n_cap_branch():
# a1 == b1 forces the branch Q = beta^h - 1 (line L7).
w, h = 4, 2
b = 0xC3A5
b1 = b >> (w * h)
a = (b1 << (2 * w * h)) | 0x0FFF
assert a < (b << (w * h))
assert div_3n_2n(a, b, h, w, Stats(), n0=1) == divmod(a, b)
def test_errors():
with pytest.raises(ValueError):
bz_divmod(5, 0)
with pytest.raises(ValueError):
bz_divmod(-1, 3)
assert bz_divmod(0, 7) == (0, 0)
What the next level needs
- the tests did not run in this build