paint-brush
Daily Coding Problem: Building an Algorithm to Calculate Huge Factorials!by@nicolam94
317 reads
317 reads

Daily Coding Problem: Building an Algorithm to Calculate Huge Factorials!

by Nicola MoroAugust 16th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Algorithm to calculate factorials of big numbers
featured image - Daily Coding Problem: Building an Algorithm to Calculate Huge Factorials!
Nicola Moro HackerNoon profile picture

Welcome everyone with another problem to solve! Today we have some nice math to work with: factorials! We are going to build an algorithm to calculate huge factorials: we’ll see how fast they grow and how big they get, and finally, we will calculate the sum of their digits to solve our problem.


Today’s problems are given by ProjectEuler.net: a super cool website to look at if you are in the mood for some head-scratching! There are more than 800 math and coding problems to solve and a vast archive of discussions about them. It is literally the perfect tool to train your math skills and learn something new. Go check it out and try to solve your challenges too!


Enough talking: let’s see what today’s problem has to offer.


n! means n * (n-1) * … * 3 * 2 * 1.

For example, 10! = 10 * 9 * … * 3 * 2 * 1 = 3628800, and the sum of the digits of 10! is 3+6+2+8+8+0+0 = 27.

Find the sum of the digits in the number 100!


As we anticipated before, what we are asked to do is to calculate the digits of a number, in particular, the number 100! = 100 * 99 * … * 3 * 2 * 1 : pretty easy...right? Well, not quite!


Let’s put down some basics then and see how to manage such a big product.


Factorials!

The particular notation stated by the problem is a famous concept in math known as factorial. The factorial of a number is simply the product of all the natural numbers up until that number. It’s usually expressed with the expression mark after the number (n!) or, in symbols, as


Product from i = 0 to n of all the i


So, for instance, the factorial of the number 10, written as 10!, is 10! = 10 * 9 * … * 3 * 2 * 1 = 3628800.


What’s really cool about factorials is how fast they grow: since we are used to dealing with Big-O complexity notation, here’s a chart comparing some function’s growth rates. It’s pretty clear that the factorial growth is the worst: for an instance size of less than 10, the number of operations executed by a factorial growing function exceeds (by a lot) a thousand operations!


Credits to http://bigocheatsheet.com/


One simple way to see this behavior is to try with a calculator: if you still have your scientific calculator from high school, like the famous Casio or Sharpe, you can easily see that you can’t calculate over 69!: it gives you back a math error. This is because the number cannot be stored by the memory of the calculator, for it gets too big to address.


Some more recent calculators try to go over that bond: for example, the calculator of my phone (a Redmi 7) can calculate up to 170!, giving out the result with the exponential notation (7.25741562e³⁰⁶) if you try with 171! though, it just spits a disappointing ∞ … which is really not the case.


It’s also really cool to look at how many digits the result of a factorial has:


  • the number 100! has 158 digits

  • the number 500! has 1135 digits

  • the number 1000! has 2568 digits

  • the number 5000! has 16326 digits


The number 10000! has 35660 digits: it is so big that I could not fit it in a single image. Here’s a GIF with all the digits:


35000 digits of 10000!


But wait a minute: didn’t we just say that we could not calculate over the factorial of 70? There’s a trick to do so that we’ll see in a moment. But to understand how we can calculate arbitrarily big factorials, we first must go back to the basics of multiplication.


How we multiply

When you were kids, chances are that your teacher at school taught you to multiply in columns with a process similar to this:


Multiplication examples


On the left side, a simple multiplication between a three-digit number and a one-digit number, in which each digit from the right to the left gets multiplied by 9. At each multiplication, if the result is greater than 9, the remainder is written on top of the next digit (right to left) and added on the next step of the process; on the right side, a multiplication of a three-digit number and a two-digit number, where there are two “temporary” results in between of the process. Here, we previously wrote a 0 as the rightmost digit in the second temporary result and summed those to get the final result.


It’s really basic math, but this process works because we can decompose the starting number in units, tens, hundreds, and so on, and multiply each of those by the second number. After that, we just sum each individual partial result to obtain the final result. Something like this:


Decomposition of the product of 123 x 12


That is precisely what we are going to do on a much bigger scale. We are going to deal with very big numbers, so this process needs to be modified a bit to work with the integer’s memory limits built into the language we are using (Python, in this case). To be true, Python 2 used to have a size limit in it’s int type, but since Python 3 replaced it, the limit was somehow removed. Other languages do have it, though, so we build this approach to keep the algorithm portable to those languages too. Also, it will come in handy to multiply from the right to the left.


Coding the process

Basically, we first convert our number to a string and reverse it. After that, we multiply each digit for the second number, add the previous remainder, and store the result and the new remainder for the next calculation. Let’s see the process step by step first:


Multiplication process of two big numbers


With this process, ideally, the only two limits we have on our products are:


  • The RAM limit: when the number we are trying to multiply gets too big, we’ll be thrown a MemoryError, for the interpreter has not enough memory to allocate the calculated number. Every modern computer has 8, 16, or 32Gb of RAM tough, so you should have plenty of memory to allocate before reaching the limit.

  • The time limit: as we’ll see better later, the time needed to complete the process grows pretty quickly. Even if it’s not a real limit (one can always wait, right?), the more the number grows, the more time you’ll need to wait for a result.


Our code will replicate this process step by step, returning the result. Here’s the code:

The function multiplication accepts two arguments: n , the number to multiply as string, and m , the other one as integer. First off, we reverse n and create two other variables:


  • newNumber , which will hold our new number digit by digit

  • remainder , which will keep track of the remainder of each operation. The remainder is initially equal to 0.


We loop over the reversed number and, at each step:


  1. We calculate the result of the digit, we are on times the second number m and add the remainder.
  2. We add the last digit of result to the newNumber ;
  3. If there is a remainder (when result > 9 ), we update the variable remainder with the new one. The remainder is composed by the digits of resultexcept the last one. If there is no remainder, we set remainder back to 0. This is really important to avoid bringing on remainders of previous multiplications!


We keep looping for each digit of reversed and finally, if there is a remainder after the last multiplication, we add it to newNumber . After that, we simply return our newNumber.


Calculating the factorial

Now that we have a method to multiply numbers of the size we want, we can apply it to calculate our factorials! Here’s the code:

It’s just a wrapper, really: all it does is set a variable out , initially equal to the string “1” and then range from 1 to n (n+1 really, for Python’s range stops at n-1) to apply our multiplication function, each time updating out with the new result.


For example, here’s the result of 500!:


1220136825991110068701238785423046926253574342803192842192413588385845373153881997605496447502203281863013616477148203584163378722078177200480785205159329285477907571939330603772960859086270429174547882424912726344305670173270769461062802310452644218878789465754777149863494367781037644274033827365397471386477878495438489595537537990423241061271326984327745715546309977202781014561081188373709531016356324432987029563896628911658974769572087926928871281780070265174507768410719624390394322536422605234945850129918571501248706961568141625359056693423813008856249246891564126775654481886506593847951775360894005745238940335798476363944905313062323749066445048824665075946735862074637925184200459369692981022263971952597190945217823331756934581508552332820762820023402626907898342451712006207714640979456116127629145951237229913340169552363850942885592018727433795173014586357570828355780158735432768888680120399882384702151467605445407663535984174430480128938313896881639487469658817504506926365338175055478128640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000

Summing up

Since our problem requires us to calculate the sum of the digits of 100!, we also need a function to sum all the digits, but it’s quite simple to build: you can do it directly in your main function. Here’s the main:

As in the other article where the problem was also given by ProjectEuler.net, I won’t give out the solution: you need to build this by yourself, at least, if you want to know it! ProjectEuler.net is really a lovely project: besides presenting math riddles and problems, their main focus is to provide a tool to start learning, thinking, and reasoning about math. And I love that: they are so committed that publishing results and guides for their problems is actually forbidden (this is one of the first 100 problems, so I understand it is permitted). Given this, I don’t feel it would be fair to just give out the solution to copy-paste and get the achievement.


Have a nice coding, guys! 😎


Time and complexity

It’s not that hard to evaluate the time complexity of this algorithm: the biggest amount of time is spent inside the multiplication function. Here, besides allocating some variables and modifying their values, the slowest part is the for loop, which will run len(n) times, depending on the length of the number we are multiplying. It is linear, since there are no nested loops to deal with.

When we wrap this function in hugeFactorial though, we must run that function n times: so the time complexity becomes quadratic, leaving us with O(n²) t.c.


Finally, summing up the digits it’s another linear time algorithm, but it’s only invoked after the factorial is calculated: for this reason, it won’t add any complexity, leaving us with a total complexity of O(n²).


Talking about execution time, here’s the time taken by my machine (AMD Ryzen 5, avg. ck. 2700 MHz) to calculate some results:


Time elapsed in seconds by the execution of the algorithm for n


With a simple regression, even if the data is clearly not enough, we can see that our prediction of a O(n²) time complexity holds true: the time taken actually grows like a parabola.


x = n! ; y = time (seconds); r coeff. = 0.99978; a = 0.95808; b = -0.00248; c = 0


I did not have much time to go over 10000!, so if you try it out, please let me know your results!


Conclusions

Here it is; this was my tackle! In all likelihood, there are better approaches to this kind of problem, but, as always, I wanted to share my try with you guys.


What do you think about this? Do you have a better solution or idea? Let me know in the comments!


I hope you liked the article and found something interesting in it! If so, please leave a like or share it with someone who could appreciate this kind of problem: that would really make my day!


Thanks for reading; as always