Project Euler presents the following Problem #13:
Work out the first ten digits of the sum of the following one-hundred 50-digit numbers.
37107287533902102798797998220837590246510135740250463769376774900097126481248969700780504170182605387432498619952474105947423330951305812372661730962991942213363574161572522430563301811072406154908250230675882075393461711719803104210475137780632466768926167069662363382013637841838368417873436172675728112879812849979408065481931592621691275889832738442742289174325203219235894228767964876702721893184745144573600130643909116721685684458871160315327670386486105843025439939619828917593665686757934951621764571418565606295021572231965867550793241933316490635246274190492910143244581382266334794475817892575867718337217661963751590579239728245598838407582035653253593990084026335689488301894586282278288018119938482628201427819413994056758715117009439035398664372827112653829987240784473053190104293586865155060062958648615320752733719591914205172558297169388870771546649911559348760353292171497005693854370070576826684624621495650076471787294438377604532826541087568284431911906346940378552177792951453612327252500029607107508256381565671088525835072145876576172410976447339110607218265236877223636045174237069058518606604482076212098132878607339694128114266041808683061932846081119106155694051268969251934325451728388641918047049293215058642563049483624672216484350762017279180399446930047329563406911573244438690812579451408905770622942919710792820955037687525678773091862540744969844508330393682126183363848253301546861961243487676812975343759465158038628759287849020152168555482871720121925776695478182833757993103614740356856449095527097864797581167263201004368978425535399209318374414978068609844840309812907779179908821879532736447567559084803087086987551392711854517078544161852424320693150332599594068957565367821070749269665376763262354472106979395067965269474259770973916669376304263398708541052684708299085211399427365734116182760315001271653786073615010808570091499395125570281987460043753582903531743471732693212357815498262974255273730794953759765105305946966067683156574377167401875275889028025717332296191766687138199318110487701902712526768027607800301367868099252546340106163286652636270218540497705585629946580636237993140746255962240744869082311749777923654662572469233228109171419143028819710328859780666976089293863828502533340334413065578016127815921815005561868836468420090470230530811728164304876237919698424872550366387845831148769693215490281042402013833512446218144177347063783299490636259666498587618221225225512486764533677201869716985443124195724099139590089523100588229554825530026352078153229679624948164195386821877476085327132285723110424803456124867697064507995236377742425354112916842768655389262050249103265729672370191327572567528565324825826546309220705859652229798860272258331913126375147341994889534765745501184957014548792889848568277260777137214037988797153829820378303147352772158034814451349137322665138134829543829199918180278916522431027392251122869539409579530664052326325380441000596549391598795936352974615218550237130764225512118369380358038858490341698116222072977186158236678424689157993532961922624679571944012690438771072750481023908955235974572318970677254791506150550495392297953090112996751986188088225875314529584099251203829009407770775672113067397083047244838165338735023408456470580773088295917476714036319800818712901187549131054712658197623331044818386269515456334926366572897563400500428462801835170705278318394258821455212272512503275512160354698120058176216521282765275169129689778932238195734329339946437501907836945765883352399886755061649651847751807381688378610915273579297013376217784275219262340194239963916804498399317331273132924185707147349566916674687634660915035914677504995186714302352196288948901024233251169136196266227326746080059154747183079839286853520694694454072476841822524674417161514036427982273348055556214818971426179103425986472045168939894221798260880768528778364618279934631376775430780936333301898264209010848802521674670883215120185883543223812876952786713296124747824645386369930090493103636197638780396218407357239979422340623539380833965132740801111666627891981488087797941876876144230030984490851411606618262936828367647447792391803351109890697907148578694408955299065364044742557608365997664579509666024396409905389607120198219976047599490197230297649139826800329731560371200413779037855660850892521673093931987275027546890690370753941304265231501194809377245048795150954100921645863754710598436791786391670211874924319957006419179697775990283006991536871371193661495281130587638027841075444973307840789923115535562561142322423255033685442488917353448899115014406480203690680639606723221932041495354150312888033953605329934036800697771065056663195481234880673210146739058568557934581403627822703280826165707739483275922328459417065250945123252306082291880205877731971983945018088807242966198081119777158542502016545090413245809786882778948721859617721078384350691861554356628840622574736922845095162084960398013400172393067166682355524525280460972253503534226472524250874054075591789781264330331690
❤️ Problem #13 is by far one of my favorite Project Euler problems to revisit. The official site ranks it as the 14th easiest problem at the time of writing, but even a less challenging problem can be memorable when it highlights the intricacies of different programming languages. We'll consider solutions in Kotlin, Python, and C++, as well as a more language-agnostic approach.
Kotlin
Kotlin's largest built-in numeric type, Long, represents a 64-bit signed integer and is far too small to hold this problem's 50-digit numbers. The largest supported integer, Long.MAX_VALUE, is only equal to a 19-digit number, 9_223_372_036_854_775_807.
Regardless, Kotlin's interoperability with Java means it has access to arbitrary-precision integers through the BigInteger class, which allows us to easily solve this problem with a single import statement:
1import java.math.BigInteger
2
3fun sliceSum(numbers: List<String>): String {
4 return numbers.sumOf { BigInteger(it) }.toString().take(10)
5}
kt
The BigInteger class is a useful tool to become quickly familiar with as it provides assistance for large number modular arithmetic operations and primality testing in several future Project Euler problems. While not proven to be a constraint in the first hundred problems, it must still be mentioned that the class does have its set limits:
... BigInteger constructors and operations throw ArithmeticException when the result is out of the supported range of −2Integer.MAX_VALUE (exclusive) to +2Integer.MAX_VALUE (exclusive).
Python
Python takes this problem and eats it like a piece of 🍰. As of Python 3, the language's only intrinsic numeric types are: integer, floating point number, and complex number, with the int type having no precision restriction. Handling 50-digit numbers is therefore as simple as reading, casting, and adding them, no import statement necessary:
1def slice_sum(numbers: list[str]) -> str:
2 return str(sum(map(int, numbers)))[:10]
py
📌 And Then There Were Three...
Python 2 had a fourth numeric type, long integer, with unlimited precision. Its int type was instead restricted depending on the current environment, with the largest positive supported value determined using sys.maxint.
Python 3 dropped long, as well as sys.maxint, and int no longer has a limit to the value of its integers, being only dependent on memory allowance.
Note that, while sys.maxint was removed, sys.maxsize can still be used to represent the largest possible size of a list or string index. On a 64-bit machine, this value corresponds to Kotlin's Long.MAX_VALUE.
C++
C++ numeric types include unsigned long long int, which represents a 64-bit unsigned integer with a 20-digit maximum of 18_446_744_073_709_551_615. For more precision, the boost multiprecision library does exist with various data type options.
As a learning opportunity, this problem became the motivation to implement my own custom BigInteger class tailored to the specific needs of this and future problems. Starting with the very minimum, Problem #13 only requires the implementation of constructors, a cast-to-string function, and the addition operator (as well as the addition assignment operator, based on design choice).
This custom class is then used with std::accumulate() from the numeric library and a binary operation function to sum up the input strings, in a manner similar to the previous two languages:
1#include <numeric>
2
3#include "pe-custom/big-int.h"
4
5std::string sliceSum(const std::vector<std::string>& numbers)
6{
7 return std::accumulate(
8 numbers.cbegin(),
9 numbers.cend(),
10 BigInt::zero(),
11 [](BigInt& acc, std::string num) {
12 return acc + BigInt {num};
13 })
14 .toString()
15 .substr(0, 10);
16}
cpp
🤔 So we've found a solution in three different languages, but why stop there when we can find common ground?
It is entirely possible to solve this problem using only the input strings by simulating the right-to-left technique one (potentially) uses when adding numbers manually. That is, each rightmost digit of each input string is summed and the rightmost digit of the result is stored, with the result divided by 10 pushed to a carry-over value. The cycle continues with a shift to the left and the new sum this time including the carry-over value.
In such a solution, the resulting output is stored as a string that is processed with each iteration to maintain the desired length of ten digits. The final output is returned as a reversed string:
1fun addInReverse(numbers: List<String>): String {
2 val n = numbers.size
3 if (n == 1) return numbers.single().slice(0..9)
4
5 var output = ""
6 var carryOver = 0
7 for (i in 49 downTo 0) {
8 val sum = numbers.sumOf { it[i].digitToInt() } + carryOver
9 output = (if (output.length == 10) output.drop(1) else output) + sum % 10
10 carryOver = sum / 10
11 }
12
13 while (carryOver > 0) {
14 output = (if (output.length == 10) output.drop(1) else output) + carryOver % 10
15 carryOver /= 10
16 }
17 return output.reversed()
18}
kt
Line 13: This accounts for any potential overflow remaining in carryOver after all the digits have been added.
This solution can be tailored heavily to fit personal preference. For example, the input strings could be read in reverse if it feels more natural to visualize the inner for loop moving left-to-right. The output could also be stored as a single growing unprocessed string, thereby removing the repeated conditional checks, with takeLast(10).reversed() chained to the final string.
Personally, the thought of repeatedly creating and processing new strings as dynamic storage was unappealing, so I originally wrote a different version that used a small class meant to mimic a bounded circular buffer. Using this mechanism abstracts away the data storage logic, so that new digits can be added without worrying about how they are being handled. My version inherits from Java's ArrayBlockingQueue class and overrides a single function:
1import java.util.concurrent.ArrayBlockingQueue
2
3class RollingQueue<E>(
4 capacity: Int
5) : ArrayBlockingQueue<E>(capacity) {
6
7
8
9
10
11
12
13
14
15 override fun add(element: E): Boolean {
16 return if (offer(element)) {
17 true
18 } else {
19 poll()
20 offer(element)
21 }
22 }
23}
kt
📍 A circular buffer only proves to be useful in two out of the first hundred Project Euler problems, so this tiny subclass currently suffices, but maybe future problems will warrant the implementation of a fully custom class.
Here's the original addInReverse(), which uses RollingQueue to store the final output:
1import util.custom.RollingQueue
2
3fun addInReverse(numbers: List<String>): String {
4 val n = numbers.size
5 if (n == 1) return numbers.single().slice(0..9)
6
7 val output = RollingQueue<Int>(10)
8 var carryOver = 0
9 for (i in 49 downTo 0) {
10 val sum = numbers.sumOf { it[i].digitToInt() } + carryOver
11 output.add(sum % 10)
12 carryOver = sum / 10
13 }
14
15 while (carryOver > 0) {
16 output.add(carryOver % 10)
17 carryOver /= 10
18 }
19 return output.reversed().joinToString("")
20}
kt
Here are the average execution speeds for the three Kotlin solutions mentioned above, using two test resources of N 50-digit numbers:
| N = 100 (ms) | N = 1000 (ms) |
|---|
| BigInteger | 7.17 | 18.30 |
| Manual | 1.42 | 8.76 |
| Manual + Queue | 6.86 | 15.96 |
Visit the following links to see the Kotlin, Python, and C++ source code and test suites in their respective repositories:
Problem content on the Project Euler site is licensed under a Creative Commons (CC) License: CC BY-NC-SA 4.0. Solutions to only the first hundred problems are encouraged to be distributed publicly.
Thanks for reading!🏝️➕☕🐍⚡