Problem 53 — Combinatoric Selections

solved Wed, 11 Dec 2019, 10:20 · level 0 · difficulty 3% projecteuler.net github

There are exactly ten ways of selecting three from five, 12345:

123, 124, 125, 134, 135, 145, 234, 235, 245, and 345

In combinatorics, we use the notation, $\displaystyle \binom 5 3 = 10$.

In general, $\displaystyle \binom n r = \dfrac{n!}{r!(n-r)!}$, where $r \le n$, $n! = n \times (n-1) \times ... \times 3 \times 2 \times 1$, and $0! = 1$.

It is not until $n = 23$, that a value exceeds one-million: $\displaystyle \binom {23} {10} = 1144066$.

How many, not necessarily distinct, values of $\displaystyle \binom n r$ for $1 \le n \le 100$, are greater than one-million?


Problem Analysis

This problem sits squarely in combinatorics, and its difficulty is not conceptual but arithmetic. The object of interest is the binomial coefficient C(n, r) = n! / (r!(n-r)!), which counts how many ways one can choose r items from n distinct items without regard to order. The naive route - computing the three factorials and dividing - is a trap: 100! has more than 150 decimal digits, so building it explicitly forces big-integer arithmetic and burns time dividing enormous numbers. The real skill here is finding a way to obtain the coefficients without ever touching a factorial.

Two structural facts about Pascal's triangle make this clean. First, each row - the values C(n, 0), C(n, 1), ..., C(n, n) - is symmetric, since C(n, r) = C(n, n-r), and it is unimodal: it rises monotonically to a single peak near r = n/2 and then falls. A unimodal sequence is one that increases up to one maximum and then decreases, so once a value crosses a threshold on the way up it stays above it until the peak. This means that as soon as I find the first r whose coefficient exceeds one-million, every entry from r through n-r also exceeds it, and I can count them all with a formula instead of evaluating them. Second, neighbouring coefficients in a row are related by the multiplicative recurrence C(n, r+1) = C(n, r) * (n - r) / (r + 1), so I can walk along a row one cell at a time using only modest-sized multiplications and an exact division. The worst-case complexity is O(max_n^2) - every row scanned to its midpoint - but the early exit trims most rows to a handful of steps, and with max_n capped at 100 the total work is trivial.

Solution Approach

Incremental Row Traversal with Symmetry Early-Exit

Both the C and Python files implement one and the same algorithm. I iterate over each row n from 1 to max_n. Within a row I initialise the running coefficient c to C(n, 0) = 1 and step r from 0 up to n/2 - only the left half, because symmetry will account for the right. At each step I test whether c already exceeds the threshold. If it does, I know from unimodality that every coefficient from index r up to the midpoint qualifies, and by symmetry their mirror images from the midpoint to n-r qualify too. The number of qualifying entries is therefore exactly n - 2*r + 1, the count of integers from r to n-r inclusive. I add that and break out of the inner loop immediately. If c has not yet crossed the threshold, I advance to the next cell with c = c * (n - r) / (r + 1) and continue.

The crucial arithmetic decision is multiplying before dividing in the recurrence. Because every binomial coefficient is a whole number, the product c * (n - r) is always exactly divisible by (r + 1), so doing the multiplication first guarantees the integer division is exact and loses nothing. The two implementations differ only in language detail. The C version reads max_n and threshold from the command line via parse_int, and accumulates the running coefficient in a long long so the intermediate values - which grow large as n approaches 100 - do not overflow. The Python version takes the same two inputs and relies on Python's arbitrary-precision integers, so overflow is simply impossible; it uses floor division // where C uses truncating /, but since the division is always exact the distinction never bites.

workbench

Test cases

categoryinputanswer
dev max_n = 100 threshold = 100 4724
main max_n = 100 threshold = 1000000 4075

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0053_s0 dev 4724 correct 0.000001 (63) 0.000017 (63)
p0053_s0 main 4075 correct 0.000002 (63) 0.000029 (63)

Files

Tags & blogs

Blogs