Sources: This repo contains a Java implementation, a Kotlin implementation is here: https://github.com/sorokod/All-Armstong-numbers
Briefly, an Armstrong number "is a number that is the sum of its own digits each raised to the power of the number of digits".
For example 8208
is an Armstrong number because 8208 = 8^4 + 2^4 + 0^4 + 8^4
. We will say that
8208
is a level-4 A-number and call the power sum of its digits, an aSum
. In other words, N is an Armstrong number
if aSum(N) = N
.
Single digit numbers are obviously (level-1) Armstrong numbers , all the A-numbers up to level 4 are:
1 2 3 4 5 6 7 8 9
153 370 371 407
1634 8208 9474
There is a finite number of A-numbers, this is because the magnitude of a number grows quicker then
it's aSum
so that after certain point, the aSum
falls behind its number. The largest A-number is
a level-39ner
The code calculates all A-numbers up to level-39 with the following timings:
Level-15 0 sec. 41 numbers
Level-18 2 sec. 46 numbers
Level-20 6 sec. 51 numbers
Level-21 11 sec. 53 numbers
Level-22 15 sec 53 numbers
Level-23 21 sec. 58 numbers
Level-25 46 sec 66 numbers
Level-39 3518 sec. (58 min) 88 numbers
* On i7-4790K CPU @ 4.00GHz
* Run as: java -Xmx5g -Xms5g -cp target/armstrong-1.0-SNAPSHOT.jar armstrong.Runner <LEVEL>
The following optimizations, listed in the order of their impact, are used.
-
A neat insight from this stackoverflow discussion, is best demonstrated by an example. Looking at
8208 = aSum(8208) = 8^4 + 2^4 + 0^4 + 8^4
, it is clear that the value ofaSum
doesn't depend on the order of the digits. In other words, all numbers comprised of the digits {0,2,8,8} will have the sameaSum
. For any representative N from the set of numbers comprised of digits {0,2,8,8}, N is A-number if and only ifaSum(N) = aSum(aSum(N))
. Therefore, generally, we don't need to examine all numbers up to a givenlevel
but only one representative from every multiset of digits. This reduces the search space from10^39
to about10^10
. Here is a more formal description [1]. -
It doesn't really matter which representative we pick, the one with digits in descending order is reasonably easy to work with, e.g. the representative of
{0,2,8,8}
is8820
. Instead of filtering all10^39
values for representatives, the code builds and discards the representatives dynamically so that only relevant values need to be examined with modest memory requirement ( in fact we need to examine less than10^9
values, more details here ). -
Obviously the problem can be broken down into parts that can be executed in parallel.
-
Datatypes. The representatives are modeled as byte arrays so that
8820
isbyte[] {8,8,2,0}
(BigInteger
s are used to calculateaSum
s ). -
Various. For the calculation of
aSums
the powers of{1..9}
are precomputed and cached. Instead of checking thataSum(N) = aSum(aSum(N))
it is cheaper to verify thataSum(N)
is a permutation of the digits ofN
which saves us an extra call toaSum
.
[1] For any integer N
, A(N)
will be the multiset of its digits, e.g: A(9474) = {9,7,4,4}
. Any integer that
is composed of digits in A(N)
is a representative
of A(N)
.
Lemma: If aSum(N)
is a representative
of A(N)
then aSum(aSum(N))
is an Armstrong number.
Proof: By the definition of aSum()
, for any K
and M
representatives
, aSum(K) = aSum(M)
. Since we assume
that aSum(N)
itself is a representative
we have that aSum(aSum(N))
is an Armstrong number.