Problem 17 — Number Letter Counts

solved Wed, 9 Oct 2019, 16:43 · level 2 · difficulty 8% projecteuler.net github

If the numbers $1$ to $5$ are written out in words: one, two, three, four, five, then there are $3 + 3 + 5 + 4 + 4 = 19$ letters used in total.

If all the numbers from $1$ to $1000$ (one thousand) inclusive were written out in words, how many letters would be used?


NOTE: Do not count spaces or hyphens. For example, $342$ (three hundred and forty-two) contains $23$ letters and $115$ (one hundred and fifteen) contains $20$ letters. The use of "and" when writing out numbers is in compliance with British usage.


Problem Analysis

This problem requires no number theory in the usual sense - there are no primes, divisors, or modular arithmetic to reason about. Instead the challenge is one of structured decomposition: converting an integer into its written English form and counting only the alphabetic characters. The central observation is that English number words are built hierarchically from a small, fixed vocabulary. Any number from 1 to 999 decomposes into at most three positional fragments - a hundreds part, a tens part, and a units part - and each fragment contributes a word whose length is fixed and known in advance. Because the letter count is fully determined by the digit structure rather than the numeric value, the whole problem reduces to assembling strings from a lookup table and tallying their letters.

The second key observation is that the vocabulary is finite and tiny: nineteen atomic words (one through nineteen), eight tens-words (twenty through ninety), plus "hundred", "and", and "thousand". Every number in the range is a combination of these. The only subtlety is the British convention requiring "and" between the hundreds and the remainder (e.g. "three hundred and forty-two"). Since the input range is bounded at 1000, the algorithm runs in linear time, O(N) where N = 1000, with each number requiring only a constant amount of work to build and measure. The problem is therefore dominated by correctness concerns - careful bookkeeping of the language rules - rather than by performance.

Solution Approach

Hierarchical Word Construction with Memoization

Both the C and Python files implement the same algorithm: build the English word representation of each number, then count its letters. I chose to construct the actual word strings rather than computing letter counts analytically, because building strings makes the language rules - especially the British "and" - explicit and easy to verify. Computing counts directly would save a little work but would obscure the logic and invite off-by-one errors.

The decomposition has two layers. The inner function, number_triplet_in_words, handles any value from 0 to 999. It first checks whether the number is directly representable - numbers below 20 and exact multiples of ten are served straight from the lookup table. For values from 21 to 99 that are not multiples of ten, it joins a tens-word and a units-word with a hyphen ("twenty-one"). For values of 100 and above, it prepends the hundreds word and "hundred", then recursively appends the remainder, inserting "and" only when the remainder is non-zero. This recursion is shallow - at most one level deep within a triplet - so it costs nothing.

The outer function, convert_number_to_words, layers the thousands scale on top. In the Python version this is handled generically: the number is formatted with comma separators via f"{number:,}", split on commas into triplet groups, and each group is paired with its scale suffix ("thousand", "million", and so on) from a pre-defined tuple. The British "and" prefix is then applied to the final group when appropriate. This design makes the Python version extensible to arbitrarily large numbers with no extra arithmetic. The C version handles 1000 as an explicit special case (it is the only four-digit value in range) but also includes a general thousands decomposition for robustness.

To avoid recomputing the same triplet repeatedly - the suffix "forty-two" appears in 42, 142, 242, and so on - both solutions cache triplet results. Python uses functools.lru_cache, which transparently memoizes the function. The C version uses a pair of parallel arrays: triplet_cache[1000][64] holds the strings and triplet_cached[1000] records which indices have been filled. Both caches are reset at the start of every solve() call - Python via cache_clear(), C via memset - so the timed benchmark loop measures the full word-construction cost on each run rather than reusing a cache left warm by a previous run. Letter counting is done by walking the string and skipping spaces and hyphens (C), or by stripping those characters with str.translate before measuring length (Python).

workbench

Test cases

categoryinputanswer
dev max_number = 5 19
main max_number = 1000 21124

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0017_s0 dev 19 correct 0.000001 (84) 0.000009 (84)
p0017_s0 main 21124 correct 0.000096 (84) 0.002029 (84)

Files

Tags & blogs

Blogs