pythonprimescompiler-optimization

Why is this Python code so slow? How can I make it more efficient?


I'm working on problem #69 from the Project Euler website. It regards Euler's Totient function. The definition of this function can be found here. Anyway, the problem asks to find the maximum value of n / Phi(n) , where Phi is the Totient function, over the integers 2 to 1,000,000. I know that my code works, as it correctly finds n=6 as a maximum when the search interval is 2 to 10. But, it is horrendously slow - using an upper limit of 1,000 extends the computation time to roughly 30 seconds, meaning that using an upper limit of 1,000,000 would take roughly at least 8 hours! On the Project Euler website it states that no single computation should take more than a minute on a moderately powered computer. My computer is plenty powerful so there is no deficiency in that regard. Perhaps the Jupyter Notebooks IDE I'm using has a particularly inefficient compiler? I'm not really sure. Help on this matter would be greatly appreciated. Below is my code:

def FactorsOf(n,include_ends=False): #Returns the factors of a positive integer in an array.
    factors = []                     #The include_ends param can be set to True if 1 & n are to be
    a=0                              #included in the returned array.
    b=0
    if (include_ends):
        a=1
        b=n+1
    else:   
        a=2
        b=n
    for k in range(a,b):
            if (n%k==0):
                factors.append(k)
    return factors
def AreRelativelyPrime(a,b):
    a_factors=FactorsOf(a,include_ends=True)
    b_factors=FactorsOf(b,include_ends=True)
    for i in range(1,len(a_factors)):           #Searches through both factor arrays to see if there
        for j in range(1,len(b_factors)):       #are any elements in common. Of course the first element,
            if (a_factors[i]==b_factors[j]):    # 1, is excluded.
                return False
    return True        
def Totient(n):
    totient=1                       #The Totient function's minimum value is 1.
    n_factors = FactorsOf(n)
    for m in range(2,n):
        if(AreRelativelyPrime(n,m)):     #Increments the Totient function every time we find a 
            totient+=1                   # number relatively prime to n.
    return totient
n_over_phi_MAX = 2
maxes = []
for n in range(2,1001):
    n_over_phi = n/Totient(n)
    if(n_over_phi > n_over_phi_MAX):
        n_over_phi_MAX = n_over_phi
        maxes.append(n)
print("The maxiumum value of n/phi(n) is " + str(n_over_phi_MAX) + " at a value of " + str(maxes[-1]))

Solution

  • Factorizing every integer from 2 to 1M will take a long time in any programming language no matter how you program it. You need to use a different algorithm altogether. A more efficient approach could be to leverage the fact that the phi function is multiplicative. Given that phi of the power of a prime is easy to compute: p**k - p**(k-1), you can generate primes (using the sieve of Eratosthenes) and just run through all multiples of the powers of these primes. Any multiplier of these prime powers (that is not itself a multiple of said prime) will have a gcd of 1 with the prime power. The multiplicative property of the phi function then allows computation of the phi value for that multiple based on previously calculated phi values (of the multiplier and of the prime power).

    This runs in 0.5 second on my laptop.

    Here's an example of the algorithm:

    N     = 1000000
    phi   = [0]+[1]*N  
    done  = [False]*(N+1)     
    prime = [True]*(N+1)
    
    for p in range(2,N+1):
        if not prime[p]: continue
        prime[p*p::p] = [False]*len(range(p*p,N+1,p)) # sieve of Eratosthenes
        n = p
        while n < N:                      # n is every power of prime p (in range of N)
            phi[n]  = n - n//p            # phi(n) for a power of a prime
            done[n] = True
            for m in range(2,N//n+1):     # Multipliers of n will have gcd(m,n) == 1
                if m%p == 0: continue     # if m not divisible by p
                if not done[m]: continue  # Expand from known phi(m)
                nm       = n*m
                phi[nm]  = phi[n]*phi[m]  # totient function is multiplicative
                done[nm] = True
            n *= p
    
    # once you have the phi values for all numbers in range, 
    # you can get the maximum ratio of n over Phi(n) 
    
    maxN2Phi = max(range(2,N),key=lambda n:n/phi[n])
    

    output:

    # print(f"Max n/phi(n) from 1 to {N}: {maxN2Phi} / {phi[maxN2Phi]} ({maxN2Phi/phi[maxN2Phi]})") 
    
    print("\nFIRST 143 phi(n):")
    for b in range(0,144,12):
        print(" ".join(f"{n or '':4}" for n in phi[b:b+12])) 
    
    
    FIRST 143 phi(n):
            1    1    2    2    4    2    6    4    6    4   10
       4   12    6    8    8   16    6   18    8   12   10   22
       8   20   12   18   12   28    8   30   16   20   16   24
      12   36   18   24   16   40   12   42   20   24   22   46
      16   42   20   32   24   52   18   40   24   36   28   58
      16   60   30   36   32   48   20   66   32   44   24   70
      24   72   36   40   36   60   24   78   32   54   40   82
      24   64   42   56   40   88   24   72   44   60   46   72
      32   96   42   60   40  100   32  102   48   48   52  106
      36  108   40   72   48  112   36   88   56   72   58   96
      32  110   60   80   60  100   36  126   64   84   48  130
      40  108   66   72   64  136   44  138   48   92   70  120
    

    Not printing the answer here in accordance with project Euler's rules

    Another alternative is to leverage the phi(n) = n * (1-1/p) * (1-1/p) * ... property. This is much simpler to implement but runs slower in my tests (still under 1 second though):

    N     = 1000000
    phi   = list(range(N+1))    
    prime = [True]*(N+1)
    
    for p in range(2,N+1):
        if not prime[p]: continue
        prime[p*p::p] = [False]*len(range(p*p,N+1,p))   # sieve of Eratosthenes
        phi[p::p]     = [ n - n//p for n in phi[p::p] ] # phi(n) = n*(1-1/p)*(1-1/p)...
    
    maxN2Phi = max(range(2,N),key=lambda n:n/phi[n])
    

    [EDIT] A much much faster solution

    By focusing closer to the objective (i.e. n/phi(n)) rather than producing the phi values, we can strategize that the largest ratio will be one where n is as large as possible and phi(n) is as small as possible.

    Given that phi(n) = n*(1-1/p)*(1-1/p)... each prime in n reduces the value of phi(n). Moreover, the smaller primes reduce it more than the larger ones. So, we need to have the greatest possible number of the smallest possible primes as factors of n. This points to selecting all the first primes until their product becomes greater than 1,000,000.

    Also, since we want n to be as large as possible, once we reach the maximum number of primes that will fit, we can further multiply the product of these primes by 2 or 3, or 4 ... as long as n remains below 1,000,000.

    This approach gives the solution directly with only a prime number generation up to a very small fraction of 1,000,000.

    The algorithm produces the solution in 0.00005 second (50 microseconds)

    here's the code:

    N     = 1000000
    prime = [True]*int(N**0.5) # largest prime used will be smaller than square root of N           
    n     = 1
    
    for p in range(2,len(prime)):
        if not prime[p]: continue
        prime[p*p::p] = [False]*len(prime[p*p::p]) # Eratosthenes
        if n*p < N:  n *= p                        # product of first primes
        else: break                                # while product fits within N
    
    n   = n*(N//n)                                 # multiply to maximize n within N
    phi = n                                        # compute phi(n)
    for f in range(2,p):
        if prime[f]: phi -= phi//f                 # n*(1-1/p)(1-1/p) ...
    
    if printIt:
        print(f"Max n/phi(n) from 1 to {N}: n={n} phi(n)={phi} ({n/phi})")