Factorials and SIRPs

Go back


Introduction

This page brings up the topic of factorials, factorials over intervals, k-factorials and SIRPs; how to compute them and how to find some properties that about them, namely the logarithm and factorization of them. Let's start with some definitions.

Factorials are the product of all integers in the interval [1;s]. 5! = 1*2*3*4*5.

Interval factorials are the product of all integers in the interval [i;s]. intfact(4,10) = 4*5*6*7*8*9*10

K-factorials are a generalization of the factorials. It is the product of all the numbers in the interval [1;n] that are congruent to n mod k (the residue class). 3-factorial 7 is computed as 1 * 4 * 7 as 1,4,7 are all congruent to 1 mod 3. We show an example of a k-factorial function below


       k_fact(s,m) 
for i ∈ [1;s]
if i mod m == s mod m
product <- product*i
end if
end for
return product
Note that kfact(100,1) is identical to 100!

S{equential} I{nterval} R{esidue} P{roduct} is the product of every integer in a interval that is of the same residue class modulo n. this is a further generalization of the k-factorials which can be considered to be SIRPs in the interval [1;n] and of the residue class n mod k. So SIRP(1,100,1,0) would be the standard factorial 100! Likewise SIRP(1,100,5, 100 mod 5) is the 5-factorial 100.

Computation

There are some optimizations one can use to compute factorials, and k-factorials primarily relying on fast factorization of factorials and binary exponentiation. However for simplicity we are only showing an algorithm for the most general form. Below we show a naive Rust-lang code to compute SIRP in Radix-10^19 to arbitrary precision. Note that implementing the optimizations on the right, result in approximately 100 times faster code, but gives an output in Radix-2^64.
 
   fn sirp(infinum: u64, suprenum: u64, modulo: u64, residue: u64)-> Vec< u64 >{
  
     const RADIX : u128 = 0x8AC7230489E80000 ;  // 10^19 in hexadecimal form
      let product = Vec::new();
                                                                    // let mut acc = 1u64;        // Product accumulator for fewer array multiplications
     for i in infinum..suprenum+1{ // inclusive range 
     
       if i % modulo == residue { // if i is of the residue class modulo n then multiply  
       
        if product.len() == 0{
            product.push(1)
        }                                                         // if acc < 6981463658332{ acc*=i}  // Keep accumulating product if less than (2^64-1)^(2/3)
                                                                  // if acc > 6981463658332{          // execute array multiplication if greater than (2^64-1)^(2/3)
        let mut carry = 0u64;

          for j in product.iter_mut(){
          
            let total =  *j as u128 * i as u128 + carry as u128;
            
            *j = (total%RADIX) as u64;                           // to promote to Radix-2^64 replace line with   *j = total as u64; 
            
            carry = (total/RADIX) as u64;                        // likewise      carry = (total>>64) as u64;
            
          } // end for 

          if carry > 0 {
       
            product.push(carry)
      
          }                                                     // acc = 1 }    // resets accumulator to multiplicative identity and closes the previous conditional branch
       } // end if 
      
     } 
                                                              /* multiplies the last accumulation regardless of value
                                                                  let mut carry = 0u64;
                                                                   for j in product.iter_mut(){
                                                                     let total = *j as u128 * i as u128 + carry as u128;
                                                                     *j = total as u64;
                                                                     carry = (total>>64) as u64;
                                                                     }
                                                                     if carry > 0 { product.push(carry) }
                                                                   }
    product                                                          */
  }
  

Exact Evaluation of Logarithm and Factorization

Logarithm. One can compute the z-base logarithm of factorials exactly by summing the z-base logarithms of it's factors. A simple pseudocode is shown below for SIRP.
  
     sirplog(i,s,m,r,z)          
slog <- 0
for n in [i;s]
if n mod m == residue
slog = slog + log(n,z)
end if
end for
return slog
Factorization Like with logarithms before we can compute the prime-factorization of the SIRP by factoring each of the individual factors. For efficiency purposes it's preferable to find the number of factors of a set prime for each sirp-factor. We first start with the function to count the factors of sirp-factor.

    sirp_prime(i,s,m,r, factor)     
count <- 0
for n in [i;s]
if n mod m == residue
k <- i
while k mod factor == 0
k <- k/factor;
count <- count + 1
end while
end if
end for
return count
To perform the factorization for the entire number we simply check for every prime in the interval [1;s]

Fast Approximation of Logarithm and Factorization

Logarithm. While the linear time evaluation given before seems great, we can utilize a constant-time approximation to compute the z-base logarithm of the factorial
 
     zlog_fact(s,z)  ≈ (s * ln(s) - s + ln(s*(1 + 4*s*(1 + 2*s))) /6 + ln(3.14159265359)/2.0)/ln(z);
     
Computing the logarithm of interval-factorial over the range of [i;s]

    zlog_intfact(i,s,z) ≈ zlog_fact(s,z) - zlog_fact(i+1,z)
    
Computing the logarithm of k-factorials, this current approximation is fairly poor with an maximum error of around ln(n)/2

    zlog_kfact(s,m,z) ) ≈ zlog_fact(s,z)/m + (ln(s)/ln(10))/m
    
We can use a slightly better approximation for the double factorials Even form, where s ∈ 2Z

    zlog_twofact(s,z) ≈ zlog_fact(s,z)/2 + (0.249901354*log10(s))/z
    
Odd form, where s ∈ 2Z +1

    zlog_twofact(s,z) ≈ zlog_fact(s,z)/2 + (0.249901354*log10(s)/2)/z
    
Approximating the SIRP logarithm has the greatest error, usually less than log(s)

   zlog_sirp(i,s,m,r,z) ≈   (zlog(s,z) - zlog(i+1,z))/m
  
Factorization of factorials receives an even greater speed up than the logarithm approximation. Unfortunately outside of the 1-factorials the result is quite disastrous. To find the number of times that a number divides into factorial we first observe that we can naively compute it by summing up the quotients of s/factor successively. For example 100! has (100/2) + (50/2) + (25/2) + (12/2) + (6/2) + (3/2) = 97 factors of 2. By noting that each number is reduced by a constant factor 2, we see that you could use a geometric series to approximate it, it's only a matter of finding the coefficient and the number of terms. Fortunately this is trivial, the coefficient is 50 and the number of terms is the power of 2 required to reach 100 aka log2(100) = 6. Further generalization is seen to be count_factor(s,factor) ≈ ∑ (s/factor)(1/factor)floor(log(s,factor)). We shown a pseudocode implementation utilizing the geometric sum function.

    count_factor(s,factor)
        r <- 1/factor 
        p <- floor( log10(n)/log10(factor) )
    (s/factor) * floor( (1 - r^(p+1)) /(1-r))
  
To generalize this to SIRPs we can follow the same trick we did with the logarithm for all the factors.

  sirp_factor(i,s,m,r)
    primes <- [1;s] ∈ P
   
    for n in primes
      factors <- n 
      factors <- (count_factor(s,n) - count_factor(i+1,n))/m
    end for
  
Some adjustments can be made for a considerable improvement. If we find the number of times t the modulo m divides into factorial s we can multiply the prime-factorization of m by t and subtract it from the approximated factorization. We further note that these factors are present in the SIRP with the residue class of 0.