NumPy
NumPy is what I reach for when a problem's work is uniform — the same arithmetic applied to every integer in a dense range, the same update applied to every cell of a table. Python's per-element cost is an interpreter dispatch, an object allocation and a reference count; NumPy's is a machine instruction on a packed buffer. Nothing about the algorithm changes. What changes is where the innermost loop runs, and that is worth one to two orders of magnitude on exactly the kind of loop these problems are made of.
The idea
An ndarray is one
contiguous block of memory plus a
dtype and a shape. A
million np.int64 entries are eight megabytes of packed integers — not a million
PyObject pointers to a million heap-allocated boxes. Every operation you write on
that array (a + b, a % m, a > k, a[::p] = 0) is a
universal function that loops
over the buffer in compiled C. Your Python code stops describing each step and
starts describing the whole step.
Across these problems that collapses into a handful of recurring shapes.
A strided slice is a sieve. flags[p*p::p] = False marks every multiple of $p$
in one C loop. The Python-level loop that remains runs only $\pi(\sqrt{n})$ times, so
the interpreter is touched $O(\sqrt{n}/\log n)$ times to do $O(n \log \log n)$ work.
The same shape carries a divisor-sum sieve, and a
Möbius sieve where the strided
slices are mu[p::p] *= -1 followed by mu[p*p::p] = 0. A strided slice also works as
a target: 407 takes view = best[s*(s+1)::s] (every multiple of
the small factor $s$) and folds a whole array of candidates into it with
np.maximum(view, cand, out=view), so one Python iteration per $s \le \sqrt{N}$
updates every modulus that $s$ divides. 668 runs the
Lucy_Hedgehog prime count the same
way — a Python loop over the primes up to $\sqrt{n}$, each step a couple of strided
slice subtractions over the table of $\pi(\lfloor n/k \rfloor)$. 521
carries a prime-sum table alongside the count and shows where the slice stops
working: the entries $\lfloor n/(ip) \rfloor$ that still index the large table are a
strided slice of it, and past a split index they fall into the small table and become a
gather. Because the split is one computed index rather than a per-entry test, each prime's
update is two contiguous pieces, with no mask.
The slice's start is the only thing that has to be computed, and two generalisations
follow from that. A segmented sieve over the block $[lo, hi)$ starts each prime at
-lo % p, the first multiple inside the block, so 632 can sieve
$d \le \sqrt{n}$ a few million at a time in bounded memory. It also multiplies each
small prime into a per-entry product array as it goes: sieving only to $d^{1/2}$,
an entry with product < d has exactly one prime factor left over, which is all its
prime-factor count needs, so the large primes never have to be sieved at all. A
polynomial sieve walks the values of $f(k)$ instead of $k$: the $k$ with
$p \mid f(k)$ are the roots of $f$ mod $p$, which form residue classes, so each root
is one strided slice. 659 sieves $4k^2 + 1$ this way, and it
divides rather than marks (values[k::q] //= p), stepping $q$ through
$p, p^2, \dots$ with each root
Hensel-lifted to the next power.
Once every prime up to the bound has been divided out, whatever is left in each entry
is 1 or a single large prime, and one np.where picks the largest factor for every
$k$.
A slice costs one Python call however few elements it touches, and a polynomial sieve
can make that the dominant cost. 291 sieves $2n^2 + 2n + 1$, whose
values grow quadratically, so its sieving primes run past the end of the index range.
Two million primes with two residue classes each would be four million slices, most of
them striking a handful of entries or none. So the loop is transposed. Think of the hits
as a table of (prime, hit number): small primes have many hits, so the Python loop runs
over primes and each slice takes one row; large primes have few hits, so the Python loop
runs over the hit number and each step takes one column. Primes above $N/64$ are
grouped into doubling bands $[lo, 2lo)$, where no prime has more than $N/lo + 1$ hits
per class, and step $k$ strikes every prime's $k$-th hit at once with
flags[start + k * q] = False. The hits are the same hits, so the work is unchanged,
but the band loops run a few hundred times rather than millions, and the scatter may
collide freely because every write is the same False. The result takes only about
twice as long as C's plain stride loops: what remains is memory traffic, not the
interpreter. Run the Python loop along whichever axis is short.
A comparison is a mask, and a mask is an index. div_sums > np.arange(limit + 1)
yields a boolean array;
np.flatnonzero
turns it into the positions that passed, and the array itself can index another array
to select those entries. Non-Abundant Sums — public, so here is
the real code — is that pipeline end to end, with
broadcasting doing the
pairwise sums:
div_sums = np.zeros(limit + 1, dtype=int)
for i in range(1, limit // 2 + 1):
div_sums[2 * i: limit + 1: i] += i # strided sieve
abundant = np.flatnonzero(div_sums > np.arange(limit + 1)) # mask -> indices
is_sum = np.zeros(limit + 1, dtype=bool)
for i in range(len(abundant)):
sums = abundant[i] + abundant[i:] # one scalar + a whole tail
is_sum[sums[sums <= limit]] = True # fancy-index assignment
arange plus arithmetic is a closed form applied to a range. Where the maths
gives a formula in $d$, evaluating it over $d = 1 \dots N$ is one expression:
counts = upper // (d * d), then a dot product against a sieved table. That is the
whole inner loop of the squarefree count in 193, and the
divisor-block sums in 944 are the same move over the
$O(\sqrt{n})$ distinct values of $\lfloor n/x \rfloor$. The same holds when the formula
is a recursion: 625 computes the totient summatory function
$\Phi(\lfloor N/k \rfloor)$ by Du's sieve,
and each $v = \lfloor N/k \rfloor$'s explicit divisor range is one arange. Some terms
$\Phi(\lfloor v/d \rfloor)$ are large values already memoised, others fall in the sieved
small table, and the mask k * d <= k_max routes each to its table — two gathers, no
branch. The range need not be the
answer's index: 257 parametrises its triangles by a fraction
$p/q$, and for each $q$ the admissible $p$ are one arange, filtered to the coprime
ones by np.gcd(p, q) == 1, with every primitive shape's scalings counted by one
floor division — the Python loop runs over $q$ only.
A table indexed by an array is a vectorised branch. A case analysis on a residue
is a lookup: 274 needs a multiplier that depends on $p \bmod 10$,
so it stores the four cases in a ten-entry array and writes
TABLE[primes % 10] * primes — one gather, no if. Two-dimensional tables work the
same way: 315 precomputes a saving for every (last two digits,
digit sum) pair and reads it for every prime at once as first[big % 100, sums].
A binomial coefficient is three gathers from the factorial tables:
517 evaluates a whole row of
$\binom{m+j}{j}$ terms as fact[m + j] * inv[j] % M * inv[m] % M.
A table can replace a loop as well as a branch. 921 needs
$\beta^x$ for over a million exponents $x$ below $2^{29}$, all with the same base
$\beta$, an element of $\mathbb{Z}[\sqrt 5]$ modulo a prime. A vectorised
square-and-multiply would cost about 29 squarings over the whole array. Because the base
is fixed, I can split each exponent instead: $\beta^x = \beta^{x_{hi} 2^{15}} \cdot \beta^{x_{lo}}$,
the baby-step giant-step shape
applied to fixed-base exponentiation.
Two tables of powers, about 45,000 entries between them, are built once in a short
scalar loop, and every exponent then costs two gathers and one multiply:
high[x >> 15] * low[x & 0x7fff], with that product being a ring multiply over
coordinate arrays. The condition is the shared base. Where each element has its own base,
as in 421 below, there is no table to share and the ladder stays.
The same split also gives every power in order. 995 needs the
multiplicative order of every residue mod $p$. With $g$ a primitive root and
$t = \lceil\sqrt{p-1}\rceil$, (big[:, None] * small[None, :] % p).ravel() is the
$t \times t$ outer product of $g^{tj}$ and $g^i$ read row by row, which is
$g^0, g^1, \dots, g^{p-2}$. The powers of a generator are a permutation of the units, so
the scatter orders[powers] = n // np.gcd(np.arange(n), n) cannot collide, and it writes
$\operatorname{ord}(g^i) = n/\gcd(i, n)$ for every residue at once. That is a discrete-log
table built from about $2\sqrt{p}$ scalar steps.
An index map is a transition, and a scatter must not collide. When a DP keeps a
distribution over states as a dense vector, a deterministic transition is just
an index array: dst[to] += src[from]. 400 pushes its weights
over Grundy values
down the tree with to = (idx ^ mask) - 1, sizing the vector to a power of two so
that XOR by a constant permutes the indices. That permutation is what makes the
one-line scatter correct. Fancy-index += reads once and writes once, so if two
sources land on the same target only one of them sticks, and nothing warns you. An
injective map, like this one, is safe; a many-to-one map needs
np.add.at
or np.bincount(to, weights=src). Plain assignment is the exception: a[idx] = True
lands the same value however often an index repeats, so a scatter of flags may collide
freely. 782 is built on that: it marks which counts $k$ are reachable
by scattering the values of about a dozen quadratic families into one $n^2$-entry flag
array, with no thought for duplicates. The problem is symmetric under $k \mapsto n^2 - k$,
and a reversed view is that map, so flags |= flags[::-1] closes the set under
complement in one pass. Source and target overlap there, and NumPy detects it and buffers
the read, just as it does for 249's knapsack slice below.
Push, don't pull — and argmin is a mex. A Grundy value
is the smallest value not among a position's options, and computing it by pulling —
gather every predecessor, build a set, scan for the gap — is a Python loop per
position. 310 reverses the direction. It keeps a boolean table
reach[heap, value], and the moment a heap's value is known it pushes it forward to
every heap a square-number move leads from, reach[heap + squares, value] = True — the
idempotent scatter above. When the loop reaches a heap, its row is complete, and
np.argmin of a boolean row is the first False: the mex, in one call. The table's
width is bounded because a mex never exceeds the number of options, here about
$\sqrt{N}$. The count of zero-XOR triples then reuses 400's trick: bincount the
Grundy values into a histogram sized to a power of two, and for each $x$,
np.dot(count, count[values ^ x]) is one term of the
XOR convolution — the gather
permutes, so the dot product pairs every $g(a)$ with the $g(b)$ that cancels it.
A matrix product enumerates linear combinations. If each candidate is a fixed
linear function of some per-element features, put the features in rows and the
functions in columns, and @ evaluates every candidate for every element in one
call. 637 inserts plus signs between digits: each placement is a
weight vector (a digit is worth base to the power of its distance from the end of its
block), so digits @ weights gives every split sum of every number at once. For set
intersection the same problem packs booleans into words:
np.packbits(bits, axis=1).view(np.uint64) turns a 512-wide membership row into
eight integers, and "do these two sets meet?" becomes (a & b).any(axis=-1)
broadcast over every (high half, low half) pair. That is a
meet-in-the-middle split
of each number's digits, with 64 set members tested per machine AND.
Loop over the digits, not the numbers. Digit work on a million integers is a
handful of passes, not a million calls: rem, d = np.divmod(rem, 10); sums += d
until rem is zero computes every digit sum in 315 in at most
ten array operations. 348 tests palindromes the same way, one
digit pair per pass from the outside in, and keeps only the survivors each time —
a cheap check first that shrinks the array
roughly tenfold per pass, so the whole filter costs barely more than its first
comparison. np.unique(..., return_counts=True) then tallies how many ways each sum
was reached.
The digits can also become the array. 705 needs the
concatenation of every prime's decimal string, so it writes the digits into a
(primes × width) uint8 table, one column per place value, and ravel() reads it
row-major — the concatenated string, with the leading-position zeros dropped by the
same flat != 0 filter that drops the real ones. Counting ordered pairs (positions
$i < j$ holding digits $x$ then $y$) is then one pass per $x$: np.cumsum(digits == x)
counts the $x$'s up to each position, and summed over the positions of $y$ it counts
the pairs. A stable argsort groups positions by digit so each $y$ is one contiguous
slice. That is a vectorised
inversion count, and
it is linear only because the alphabet has nine letters — nine prefix sums, not a
merge sort.
A recursion that only branches can run breadth-first. Enumerating combinatorial
objects is usually a recursive generator, one Python call per node. If nothing prunes
the tree, it can be built a layer at a time instead, with the whole frontier as one
array. 749 wants every digit-count vector $(c_0, \dots, c_9)$
summing to $d$, about two million of them at $d = 16$. A partial vector with $r$ digits
left has $r + 1$ children, so the child counts differ per row, and the layer needs an
arange of a different length for each parent. That has a loop-free form:
starts = np.cumsum(sizes) - sizes # each parent's first child
child = np.arange(sizes.sum()) - np.repeat(starts, sizes) # 0, 1, .. restarting per parent
sizes = [3, 1, 2] gives [0, 1, 2, 0, 0, 1], and np.repeat(rows, sizes, axis=0)
lines the parents up against it, so nine passes build the whole table. The price is that
the frontier lives in memory, about 160 MB of int64 here, and that every node gets
built. A search that prunes as it goes wants the recursion, which is why
983 below keeps its own.
Vectorise across instances, not just across a range. Nothing requires the
elements to share a modulus. 421 runs square-and-multiply on an
array of primes, each entry reduced by its own prime: np.where(odd, r * f % p, r)
is the conditional multiply, and the exponent array shifts right until it is zero.
Even a retry loop vectorises — searching for a base whose power is not 1, it keeps a
pending index array of the primes still unsatisfied and shrinks it each round, so
the work per round is only the stragglers. The exponent can differ per element too:
455 iterates $x \leftarrow n^x \bmod 10^9$ for every $n$ up to a
million at once, each element raised to its own current $x$, and stops when
np.array_equal says the whole array has reached its fixed point — a dozen rounds,
not a million separate iterations. Even the modulus an exponent is reduced by can be
chosen per element: 492 reduces a tower of 2s mod $p - 1$ or
$p + 1$ for each prime, picked by a quadratic-residue mask, and then runs a
Lucas-sequence ladder in which each
prime reads its own exponent bit. The element need not even be a number:
721 exponentiates $c + \sqrt{a}$ in the ring
$\mathbb{Z}_p[\sqrt{a}]$, holding each ring element as two arrays $(u, v)$ — a
struct of arrays — with a ring multiply written once over whole arrays and each $a$'s
own exponent bit applied as a mask. 437 uses the same struct of
arrays in $\mathbb{Z}_p[x]/(x^2 - x - 1)$ and adds what a struct of arrays costs when
lanes drop out. Every prime carries five parallel arrays (the prime, its unfactored
$p - 1$, and a remembered "dead root" held as a fraction), and each time a factor rules some
primes out, one keep mask compacts all five together. Filter one array and not the others
and the lanes silently stop lining up. The remainder left after trial division up to
$\sqrt{N}$ is the single large factor, and it gets one last vectorised pass, which is the
leftover-factor argument again.
Lanes that drop out on a known schedule need no compaction at all. Each row in
749 stays live only while its largest digit's $k$-th power is within
range, and that ceiling is known before the loop starts. So I sort the rows by ceiling
once, descending, and the rows live at exponent $k$ are always a prefix: one
searchsorted finds its length and counts[:live] is a view, where a mask would copy
millions of rows on every pass. The same ordering settles an overflow. A digit too big
for this $k$ cannot occur in any live row, so its entry in the power table is set to 0
rather than to a value int64 cannot hold, and counts[:live] @ powers never reads it
with a non-zero count.
Instances of different sizes still fit one array if you pad them to a rectangle, and the
padding value is chosen so that it does nothing. 564 averages over every
partition of $n - 3$, each a ragged list of (side, multiplicity) pairs, so it stores them as
two zero-padded matrices. Each partition's circumradius solves a monotone equation in a sum of
mult * np.arcsin(side * x) terms, and a padding cell has multiplicity 0 and $\arcsin 0 = 0$,
so it adds nothing to any row's sum. The solve is then a
bisection on every row at once:
64 rounds of hi = np.where(above, mid, hi) and its mirror. Which of the two equations a row
obeys (circumcentre inside the polygon or not) is a mask fixed before the loop, and the
residual is one np.where. A fixed round count means no lane ever drops out, so nothing needs
compacting. 975 pads ragged tables of extremal heights in the same way,
but its lanes walk a path through their own table, each holding its own cell $(i, j)$ and
reading fv[rows, i] with one paired gather per step. Walks end at different times, so it
compacts with a keep mask as 437 does, and the loop runs as long as the longest walk, not
once per walk.
A walk of fixed length needs no compaction, and on a small map it finds cycles without
looking for them. 971 reduces each prime $p \equiv 1 \pmod 5$ to a map on
five classes, a functional graph
on five nodes, and needs to know how many of them lie on a cycle. On $k$ nodes, $k$ steps
carry every node onto its cycle, and a cycle maps onto itself, so the image of $f^5$ is
exactly the periodic set. The state is a (primes × 5) array with arange(5) in every row.
Five paired gathers, cur = (cur + shift[rows, cur]) % 5, advance every node of every
prime, then scattering True at [rows, cur] and summing each row counts its cycle nodes.
That is cycle detection for millions of graphs with no visited set and no branch.
The axis can also run the other way: one instance, many candidate parameters. 986 asks for the largest $M$ for which a forced recurrence started at $M$ dies out. The answer is monotone in $M$, so a bisection would find it, but each probe is a long simulation whose cost is almost all per-step Python overhead. So 64 candidates run side by side, one per column of the state buffer, for about the price of one, and each round narrows the bracket 64-fold instead of 2-fold. The rounds are allowed to stop early, leaving slow columns undecided, because the final pair $M, M + 1$ is certified with an exact run. Wherever the per-step overhead dominates, a $k$-ary search costs about the same per round as a binary one and needs far fewer rounds.
A scan with no ufunc folds into a grid. A running product mod $p$ has no
vectorised form: np.multiply.accumulate cannot reduce between steps, so it overflows
uint64 after a couple of terms. 739 needs exactly such prefix and
suffix products over a range of $n$ factors, and gets them by reshaping each
chunk into a $k \times k$ grid: a Python loop over the $k$ columns advances all $k$
rows' scans together, then a $k$-step scalar carry stitches the rows into one
sequence. The Python loop count drops from $k^2$ to $2k$. A plain product reduces as a
pairwise tree, arr = arr[0::2] * arr[1::2] % p, in $\log_2$ of the length steps, and
even a linear recurrence fills the grid row-wise: an index-shift identity jumps the
recurrence by a whole row, so each row's start is one scalar step and its contents one
broadcast. 754 runs the grid scan inside a segmented
Möbius sieve and adds two moves.
A product over a subset is a full prefix product of np.where(mu == 1, d, 1) —
the identity fills the gaps. And each segment's scan starts from the previous segment's
last value, so the scan covers $1 \dots N$ in bounded memory while one fancy index per
segment keeps only the $O(\sqrt{N})$ prefixes the formula reads, at the quotients
$\lfloor N/k \rfloor$. Its sieve also tracks rad, the product of each entry's small
primes, so mu[rad != d] *= -1 accounts for the one prime above $\sqrt{N}$ that the
sieve never visited — the same leftover-factor argument as 632. 423
uses the grid scan to build the factorial tables, and the inverse factorials need only
one modular inverse: the scan of $n, n-1, \dots, 1$, read backwards, gives
$n!/i!$ for every $i$, and one multiply by $(n!)^{-1}$ turns that whole array into
$1/i!$.
Some recurrences do have a ufunc once you change variables.
718 computes the
Apéry set of a three-generator
semigroup by walking the residue cycle of $z$ modulo $x$. Each class's least
representable value obeys $w_i = \min(b_i,\; w_{i-1} + z)$, a min-plus recurrence with
no NumPy form as written. Subtracting $iz$ from both sides gives
$w_i - iz = \min(b_i - iz,\; w_{i-1} - (i-1)z)$, which is a running minimum, so the
whole recurrence is np.minimum.accumulate(base - i*z) + i*z. Done in chunks, each
chunk's first entry is folded with the previous chunk's last minimum, so the scan
crosses chunk boundaries with a single scalar of carry. The general rule: a recurrence
$s_i = s_{i-1} \oplus a_i$ vectorises exactly when $\oplus$ is a ufunc with an
accumulate, and a shift by a known sequence can often make it one.
Broadcasting builds the pairwise table. xs[None, :] - xs[:, None] is every
difference vector between $n$ points as one $n \times n$ array. 252
reduces each by np.gcd so that exactly collinear directions become equal integers
(floats only order them, never decide equality), sorts every row by angle with
argsort(axis=1), turns the sorted order into ranks with cumsum and
put_along_axis, and decides which triangles are empty with
np.maximum.accumulate — a running maximum along each row in place of a scan over
third points. When the objects are points but the tool wants scalars, pack them:
983 encodes a lattice point as one int64 key $xB + y$, after
which np.unique, np.isin and searchsorted handle sets of points as sets of
integers. The pairwise table need not be square or symmetric: for each $q$,
518 takes the multipliers $k$ that already passed one primality
test and the coprime $p < q$, and np.outer(ks, ps * ps) lays out every candidate
$(k, p)$ as a grid. Two gathers from the primality table give a boolean grid, and
np.nonzero returns the coordinates of its True cells, rows and cols, which
index back into ks and ps to weight each hit. The outer loop runs over $q$ only;
everything inside it is one grid.
searchsorted is a vectorised binary search. Given a sorted array of primes,
np.searchsorted
evaluates $\pi(x)$ for a whole array of $x$ at once. 187 counts
semiprimes as a sum of prime-counting differences and never writes a lookup loop at
all — the table is the array's own index, and the queries are one call. The same
call locates the piece of a piecewise formula. 341 evaluates
Golomb's sequence at a million cubes up to $10^{18}$ by
inverting its run-length structure. Inverted three times, the structure leaves a table
of only a few tens of thousands of block boundaries. Built in exact Python ints, which
are cheap at that size, it is converted to int64 once. searchsorted then finds every
query's block, and inside a block the sequence's partial sum is a quadratic, so one
vectorised quadratic solve finishes the job. The table is small and the queries are
many, so the binary search is what joins the two.
It is also NumPy's set lookup with a payload. np.isin says whether a value is present;
searchsorted says where, so a hit can carry data. 678 counts sums
$a^e + b^e$ that land on a perfect power $c^f$. The targets go through
np.unique(..., return_counts=True) into a sorted table with each value's multiplicity, and
a whole row of candidate sums is located with one searchsorted. An index past the end is
reset to 0, values[idx] == sums is the hit mask, and multiplicity[idx[hit]].sum() counts
each hit as many times as its target occurs.
Sort, then compare neighbours: that is a group-by. NumPy has no hash table, but a sort
does the same job for counting. 972 counts collinear triples in the
Klein model, where geodesics
are straight lines. For each anchor point, the line to every other point is a cross product
that one broadcast computes. The line is then made canonical: divide by the gcd and fix the
sign, so that two points on the same line produce equal integer pairs (252's lesson again).
After np.lexsort on that key, a group starts wherever a key differs from its predecessor:
np.flatnonzero of that comparison gives the starts, np.diff gives the group sizes, and
$\sum s(s-1)/2$ is the number of pairs on a common line. That is a
run-length encoding of sorted data, and
np.unique(keys, return_index=True) is the shortcut when you need only the first member of
each group: it is how 995 finds the least prime of every order in a
chunk.
Groups of unequal size are ragged again, and 998 handles them by
bucketing by size. It wants every pair of Pythagorean triples that share a leg $s$. With
the triples sorted by leg, np.bincount gives each leg's count and np.cumsum its offset (a
CSR index, as in 425). Every leg with exactly $k$ triples then stacks into one
(legs × $k$) matrix, np.triu_indices(k) lists the pairs $i \le j$ within a row, and the
pair tests run over the whole bucket at once. The Python loop runs over the distinct group
sizes, which are few, rather than over the legs. Building the triples needs the same
ragged expansion as 749: each primitive triple repeats $\lfloor n/\text{long leg} \rfloor$
times, and np.repeat with the restarting-arange trick produces all its multiples.
Sort by a key, and the rank is the position. Sometimes the order is easy to decide
even though the positions are hard to compute. 941 needs the place of
each of ten million 12-digit windows in a
de Bruijn sequence built by
concatenating Lyndon words in lexicographic order. Computing any single place is awkward. But
that sequence walks the necklaces in order, so one window comes before another exactly when
its (necklace, offset in that necklace's block) pair is smaller. The least rotation and the
period come from $n$ array passes, each rotation one line of digit arithmetic,
(w % 10**(n - s)) * 10**s + w // 10**(n - s), with a mask updating the running minimum.
The pair is packed into one int64 key, and argsort turns keys into ranks without any
position ever being computed. The few windows that straddle a block boundary need a different
key. One mask finds them and a scalar loop re-keys them: 558's split between the common case
and the rare one. Even the input is vectorised: the generator is a linear congruential
recurrence, so one row is stepped in Python and each later row is the previous one pushed
through the map composed block times, which is 739's row jump.
cumsum is the prefix sum, bincount the histogram. The
Mertens function is literally
np.cumsum(mu); the count of squarefree numbers up to each bound is
np.cumsum(mu != 0). Both fall out of a Möbius sieve for free in
464, which then needs a
Fenwick tree — and deliberately does
not build it in NumPy (see below).
A prefix sum also answers a threshold question for every value at once.
949 counts $k$-tuples whose sum is below $T$, by
meeting in the middle. The
distributions of the $h$-fold and $(k-h)$-fold sums are dense arrays built by shifted slices
(996's polynomial move, below). The pairs with $x + y < T$ then number
$\sum_x \mathrm{lo}[x] \cdot \#\{y < T - x\}$, and the second factor is cumsum(hi) read
at an index array, $T - x$ shifted to the table's offset. Indices past either end are
clipped, with the low side masked to zero by np.where, so each threshold costs one
gather and one dot product.
Shifted slices are stencils and DP layers. A grid diffusion step is four
boundary-clipped additions of offset slices — nxt[:, 1:, :] += share[:, :-1, :] and
its three mirrors — which is how 213 advances every flea's random
walk simultaneously instead of one walk at a time. The subtlest instance is the
knapsack recurrence in
249: counts[prime:] += counts[:-prime] is the whole
"add this item to every reachable subset sum" pass. The two slices overlap, and
NumPy buffers the right-hand read before writing — which is exactly the
descending-index update the recurrence requires, and exactly what a hand-written
ascending Python loop would get wrong. The same slice is polynomial arithmetic: hold a
generating function as its coefficient array and multiplying by $y^s$ is
out[s:] = p[:-s]. 996 advances a whole transfer recurrence of
polynomials that way — each step a few shifted sums, every coefficient updated
together.
The same buffered read multiplies a Dirichlet series
by an Euler factor. 883 needs $f * \mu$, which is $f$ times
$\prod_p (1 - p^{-s})$, and one factor is one line: out[p::p] -= out[1:limit // p + 1].
Every multiple $n$ of $p$ loses the value at $n/p$. Target and source overlap, and the
buffered read supplies the values from before this prime's pass, which is the single
factor the product asks for. An ascending Python loop would subtract entries it had already
updated and compute a different series. The whole Möbius inversion is one slice per prime, so
it costs the same as a sieve.
A shift can carry a per-cell weight, and then the question is what to do with the cells
where that weight means nothing. 475 counts 0/1 matrices row by row
over a 2D table of column-fill counts: each of the 15 ways to place a row's four ones is
one shifted add of table * weight, the weight being a product of binomials gathered at
each cell's own counts. Those counts come from invariants and go negative in
cells no real state can occupy, about three quarters of the grid on average, and a
negative index does not fail: it silently wraps to the far end of the table. np.clip
keeps the gather in bounds and the weight it reads there is nonsense, but it does not
matter, because the state count in those cells is 0 and multiplies it away. A placement
asking for more columns than a cell has reads $\binom{c}{k} = 0$ the same way. Zero is
the validity mask, and there is no if anywhere. The cost of the dense form is that
every cell is computed. Only about 6% of this grid is reachable on average, and the C
version, which skips the zero cells, runs about 36 times faster, against the factor of
two in 291. A dense table buys simplicity, and it is also fast only while most of it is
occupied.
A DP whose recurrence is inherently sequential along one axis can still be a vector along the
other. 1001 runs an interval DP: for each right end $r$, a column
$C_r[l]$ is filled from $l = r$ downwards, and every step depends on the step before. But
columns depend on each other only through values of shorter intervals, which are already
known, so 512 columns can be swept together. One Python iteration per row $l$ updates the
whole block as cur[k:] = (cur[k:] + g * snapshot) % MOD. Only the columns with $r \ge p$
(where $p$ is the partner of $l$) take the term, and because the columns are sorted that is a
suffix found by one bisect, so there is no mask, as in 521's split index. The cost is
bookkeeping. The update needs the block as it stood at row $p + 1$, which the downward pass
has since overwritten, so the pass copies a snapshot when it reaches that row and pops it
when $l$ arrives. The Python loop then runs about once per (row, block) rather than once per
DP cell.
How to reason about it
Fixed-width dtypes are the price of the speed — watch the integer width. Python's int is arbitrary
precision; np.int64 wraps silently at $2^{63}$ and tells you nothing. Every
vectorised modular computation therefore needs an overflow argument before it needs a
correctness argument. 944 does square-and-multiply on arrays with
the invariant that every partial product stays under $m^2 < 2^{63}$;
249 knows each pass at most doubles a count, so it can run ten
passes before reducing mod $10^{16}$, and sums a uint64 array in chunks small
enough that no partial sum wraps. 421 does both halves of the
job: it refuses inputs past $2^{32}$, where a product of two residues would leave
uint64, rather than wrap; and since its final sum can pass $2^{64}$ even though no
term does, it sums the high and low 32-bit halves separately and rejoins them as
Python ints. 348 caps its digit bands at the length whose sums
still fit. 401 needs $x(x+1)(2x+1)/6 \bmod m$ for $x$ up to
$10^{15}$, where the raw product is far past $2^{63}$; rather than reach for a modular
inverse of 6, it divides the 2 and the 3 out of whichever factor they divide — chosen
per element by masks on x & 1 and x % 3 — and only then reduces and multiplies. If you cannot state the bound, you do not have a correct program — you
have one that happens to work on the test case.
Floats estimate; integers decide. np.sqrt on an int64 array goes through
float64, which has 53 bits of mantissa, so past $2^{53}$ the truncated root can be
off by one either way. 331 takes the float root as a guess and
corrects it with exact integer comparisons — root -= root * root > v, then the
mirror step up — a vectorised integer square root;
721 uses the identical two lines to decide which $a$ are perfect squares.
341 rounds its per-element quadratic root the same way.
252 applies the same division of labour to angles: arctan2
sorts directions, but equality is decided on gcd-reduced integers.
A float can decide, but only where a bound proves it exact. 767 needs
polynomial products modulo $10^9 + 7$, and np.fft gives an $O(n \log n)$
convolution only in float64. Products
of 30-bit residues summed over a long convolution would need far more than the 53 bits a
double has. So each coefficient is split into three 10-bit limbs. A limb product is below
$2^{20}$, a sum of up to $2^{17}$ of them stays below $2^{37}$, and the FFT's rounding error
on values that size is far below $\tfrac12$, so np.rint of the inverse transform returns
the exact integer. Six forward transforms give the five limb-pair spectra, and the five
results are recombined mod $p$ in exact integers. The FFT does all the heavy work, and the
limb width is what makes its output exact. When the polynomials are short enough for direct
convolution, the float drops out entirely: 923 multiplies with
np.convolve on int64, which is exact as long as nothing wraps, so its 15-bit halves are
purely an overflow argument — four convolutions, recombined by shifts mod $p$.
Equality can come from a float too, by rounding onto a grid. 842 counts
the points where diagonals of a regular polygon cross, and several diagonals may pass
through one point. Every one of the $\binom{n}{4}$ crossings is computed at once in
float64 by Cramer's rule, snapped to a $10^{-9}$ grid, and packed into one int64 key
(983's packing, above), so np.unique(keys, return_counts=True) groups the coincident ones.
The rounding is sound only if distinct crossings lie much further apart than the float
error, and that is a claim about geometry, not something the code can prove. So the code
checks the one consequence it can test: $k$ chords through a point make $\binom{k}{2}$
crossing pairs, so every group size must be triangular, and a size that is not raises an
error instead of returning a wrong count.
558 pushes the division of labour furthest. It expands every square
greedily in powers of the real root $r$ of $x^3 = x^2 + 1$. Each remainder is held
exactly, as integer coefficients $(a, b, c)$ of $a + br + cr^2$ in three int64
arrays, and the question "is the remainder at least $r^k$?" is answered by a fixed-point
evaluation $a \cdot 2^P + b\hat{r} + c\widehat{r^2}$. That evaluation is
allowed to wrap. Integer multiplication and addition are exact modulo $2^{64}$, so as
long as the true result fits in int64, the wrapped intermediate terms do not matter.
The rounding in $\hat r$ gives a known error bound, and the comparison splits the lanes
three ways: clearly yes, clearly no, and a thin ambiguous band. Only the ambiguous lanes
leave NumPy, for an exact sign test (the field
norm) in Python ints, and they are rare
enough that the exact path costs nothing. The lanes that pass then apply their
subtraction in place with np.copyto(a, a - pa, where=take). Vectorise the decision
that is almost always easy, and pay for exactness only where it is actually in doubt.
The dtype is also the memory budget. A Möbius array only ever holds $-1, 0, +1$,
so int8 costs one byte per entry and a sieve to $2^{25}$ fits in 32 MB instead of
256 MB. When the range genuinely does not fit, chunk it: build arange a few million
at a time and accumulate, which keeps the vectorisation and bounds the footprint —
401 walks its $\sqrt{n} \approx 3 \times 10^7$ hyperbola terms in
chunks of $2^{20}$. The dtype can even be chosen at run time:
986 simulates in int32 whenever its starting values fit, falling back to
int64 otherwise, and keeps its state in a short ring buffer, updated in place with
np.add(..., out=block) and np.right_shift(..., out=block). The working rows stay in cache
and no temporaries are allocated, which matters when the arithmetic per step is a single add
and shift.
Vectorising means no Python per element, not NumPy somewhere.
np.vectorize
is a convenience wrapper around a Python loop and buys no speed — the docs say so
themselves. Indexing an array element-by-element in a Python loop is slower than a
list, because each read builds a fresh scalar object. This cuts both ways: when the
algorithm genuinely is scalar and branchy, the right move is .tolist() once and
loop over plain Python. 464 does exactly that — NumPy for the
sieve, the prefix sums and the sort, then a hand-written Fenwick tree over Python
lists, because a pointer-chasing $O(\log n)$ update has nothing to vectorise.
425 draws the same line through a graph. Building the edges is
uniform work: for each digit position and each decrement, one masked subtraction and
one is_prime gather produce every edge of that kind for all primes at once. The edges
are concatenated, sorted by their larger endpoint with argsort, and two
searchsorted calls give every prime its slice of that list — a
CSR
adjacency, with np.cumsum(is_prime) - 1 as the value-to-index map. The
union-find that walks it
is path-chasing, so it runs over .tolist() copies in plain Python.
The converse split works too: 983 is a
depth-first search, and the recursion stays
plain Python, but each node carries the table of every signed sum of its chosen
vectors as an array (tripled per level with one concatenate), so the pruning test
at each node is a couple of np.isin calls rather than a nested loop.
850 splits a depth-first search at its last level instead. It recurses
over powerful numbers by increasing prime,
but below a node with quotient $x$, a prime with $p^4 > x$ can only end the chain, as $p^2$
or $p^3$, and those children's weights are closed forms in $p$. Two searchsorted calls on
the prime array bound the slice $x^{1/4} < p \le \sqrt{x}$, and that node's whole fringe of
leaves is one expression such as ((a - 1) * (x // (a * a))).sum(). Python visits only the
inner nodes, and the leaves, which are most of the tree, are array elements.
It is a constant factor, not a complexity class. A $50\times$ speedup never rescues the wrong exponent; see algorithm beats language. Fix the asymptotics first, then vectorise the inner loop that survives. Conversely, once the algorithm is right, NumPy is often the cheapest remaining win — it keeps the solution in Python, readable and a dozen lines long, at a speed that would otherwise mean rewriting in C.
Build the arrays inside solve(). The benchmark harness times repeated calls, so
a table built at module import is free on every run after the first and the reported
cost is a fiction. Size the sieve to the actual input and build it per call.
When it does not apply: irregular control flow, data-dependent recursion,
pointer-chasing structures, and heavy exact arithmetic past 64 bits, where Python's
own bignums are the tool. A dtype=object array holds Python ints and keeps the
slicing and broadcasting, but its arithmetic runs at interpreter speed — useful as a
switch, not as a speed-up: 996 writes its recurrence once and
picks int64 when the input supplies a modulus and object when it asks for the
exact value, so one body of code serves both the small exact examples and the large
reduced one. NumPy wants the same operation, over a dense
contiguous range, with the branches expressed as masks rather than ifs. When the
work has that shape, reach for it; when it does not, forcing it produces code that is
both slower and harder to read.
Problems105 / 105
- ● 0023 — Non-Abundant Sums
- ● 0056 — Powerful Digit Sum
- ● 0128 — Hexagonal Tile Differences
- ● 0133 — Repunit Nonfactors
- ● 0134 — Prime Pair Connection
- ● 0135 — Same Differences
- ● 0136 — Singleton Difference
- ● 0149 — Maximum-sum Subsequence
- ● 0150 — Sub-triangle Sums
- ● 0154 — Exploring Pascal's Pyramid
- ● 0159 — Digital Root Sums of Factorisations
- ● 0165 — Intersections
- ● 0177 — Integer Angled Quadrilaterals
- ● 0187 — Semiprimes
- ● 0193 — Squarefree Numbers
- ● 0196 — Prime Triplets
- ● 0210 — Obtuse Angled Triangles
- ● 0211 — Divisor Square Sum
- ● 0213 — Flea Circus
- ● 0214 — Totient Chains
- ● 0216 — The Primality of $2n^2 - 1$
- ● 0229 — Four Representations Using Squares
- ● 0231 — Prime Factorisation of Binomial Coefficients
- ● 0249 — Prime Subset Sums
- ● 0252 — Convex Holes
- ● 0257 — Angular Bisectors
- ● 0274 — Divisibility Multipliers
- ● 0291 — Panaitopol Primes
- ● 0304 — Primonacci
- ● 0310 — Nim Square
- ● 0315 — Digital Root Clocks
- ● 0319 — Bounded Sequences
- ● 0331 — Cross Flips
- ● 0341 — Golomb's Self-describing Sequence
- ● 0348 — Sum of a Square and a Cube
- ● 0357 — Prime Generating Integers
- ● 0365 — A Huge Binomial Coefficient
- ● 0381 — $(\text{prime}-k)$ Factorial
- ● 0400 — Fibonacci Tree Game
- ● 0401 — Sum of Squares of Divisors
- ● 0407 — Idempotents
- ● 0420 — $2 \times 2$ Positive Integer Matrix
- ● 0421 — Prime Factors of $n^{15}+1$
- ● 0423 — Consecutive Die Throws
- ● 0425 — Prime Connection
- ● 0429 — Sum of Squares of Unitary Divisors
- ● 0437 — Fibonacci Primitive Roots
- ● 0455 — Powers with Trailing Digits
- ● 0461 — Almost Pi
- ● 0464 — Möbius Function and Intervals
- ● 0475 — Music Festival
- ● 0478 — Mixtures
- ● 0487 — Sums of Power Sums
- ● 0492 — Exploding Sequence
- ● 0501 — Eight Divisors
- ● 0517 — A Real Recursion
- ● 0518 — Prime Triples and Geometric Sequences
- ● 0521 — Smallest Prime Factor
- ● 0549 — Divisibility of Factorials
- ● 0558 — Irrational Base
- ● 0564 — Maximal Polygons
- ● 0565 — Divisibility of Sum of Divisors
- ● 0625 — Gcd Sum
- ● 0632 — Square Prime Factors
- ● 0635 — Subset Sums
- ● 0637 — Flexible Digit Sum
- ● 0650 — Divisors of Binomial Product
- ● 0659 — Largest Prime
- ● 0668 — Square Root Smooth Numbers
- ● 0678 — Fermat-like Equations
- ● 0688 — Piles of Plates
- ● 0705 — Total Inversion Count of Divided Sequences
- ● 0718 — Unreachable Numbers
- ● 0720 — Unpredictable Permutations
- ● 0721 — High Powers of Irrational Numbers
- ● 0726 — Falling Bottles
- ● 0739 — Summation of Summations
- ● 0749 — Near Power Sums
- ● 0754 — Product of Gauss Factorials
- ● 0757 — Stealthy Numbers
- ● 0764 — Asymmetric Diophantine Equation
- ● 0767 — Window into a Matrix II
- ● 0782 — Distinct Rows and Columns
- ● 0804 — Counting Binary Quadratic Representations
- ● 0842 — Irregular Star Polygons
- ● 0850 — Fractions of Powers
- ● 0883 — Remarkable Triangles
- ● 0921 — Golden Recurrence
- ● 0923 — Young's Game B
- ● 0934 — Unlucky Primes
- ● 0938 — Exhausting a Colour
- ● 0939 — Partisan Nim
- ● 0941 — de Bruijn's Combination Lock
- ● 0944 — Sum of Elevisors
- ● 0949 — Left vs Right II
- ● 0971 — Modular Polynomial Composition
- ● 0972 — Hyperbolic Plane
- ● 0975 — A Winding Path
- ● 0980 — The Quaternion Group I
- ● 0983 — Consonant Circle Crossing
- ● 0986 — Another Infinite Game
- ● 0995 — A Particular Pair of Polynomials
- ● 0996 — Overtakes
- ● 0998 — Squaring the Triangle
- ● 1001 — Connections I