Skip to main content
12 of 17
Fix a few typos.
wei2912
  • 1.3k
  • 8
  • 26

Algorithm

Your current method relies on iterating through all numbers and checking if they're 1) a factor 2) prime.

With the new algorithm, not only are we able to skip the expensive primality check, but we're also able to reduce our search space massively (the upper bound is the square root of the number).

public static long maxPrimeFactor(long n) {
    long factor = -1;
    for (int i = 2; i <= (long) Math.sqrt(n); i++) {
        if (n == 1) { break; }
        if (n % i != 0) { continue; }
        factor = i;
        while (n % i == 0) {
            n /= i;
        }
    }
    return n == 1 ? factor : n;
}

This algorithm is a slightly optimized version of Martin R's answer.

We first "trim" down a number by repeatedly dividing it by factors of itself which are smaller or equal to its square root. Here're some examples

[3] (no factors <= sqrt(3))
[4 -> 2 -> 1] (factors: 2)
[200 -> 100 -> 50 -> 25] --> [5 -> 1] (factors: 2, 5)
[7007 -> 1001 -> 143] --> [13] (factors: 7, 11. 13 > sqrt(143), hence it stops)

The square brackets represent groups of "divisions" where a number is repeatedly divided by the factor.

Most composite numbers will be reduced down to 1. This is because all prime factors of a composite number will always be lower than or equal to its square root. However, note that as an optimization, we trim down the upper bound to the square root of the new number upon every iteration. This results in a few cases where we stop at a prime number. In that case, we return that number, which is the largest prime factor.

Note that we only need to iterate through primes. Since a prime sieve is too expensive, iterating through {2, 3, 5, 7, ..., sqrt(n)} works well enough for our scenario. For clarity, my code iterates through all integers from 2 to sqrt(n); I'll leave this to the OP as an exercise.


Benchmarks

In these benchmarks, my algorithm refers to the optimized version of the algorithm, as mentioned in the previous paragraph.

OP's algorithm:

Result: 6857
Time required to calculate in nanoseconds: 870707572

h.j.k.'s algorithm:

Result: 6857
Time required to calculate in nanoseconds: 864074780

Slight improvement there, but not much.

JS1's algorithm:

Result: 6857
Time required to calculate in nanoseconds: 10038730

A definitely massive improvement, but is still a bit slow since the primality tests are too slow.

thepace's algorithm:

Result: 6857
Time required to calculate in nanoseconds: 179319

An even more massive improvement thanks to the primality test.

My algorithm:

Result: 6857
Time required to calculate in nanoseconds: 118748

Looks like it's faster than thepace's algorithm on first glance, but since these benchmarks are runned only once, it's hard to tell.

Running 1000 trials:

Let's try running 1000 trials with a multiple of 2 large primes:

$$3333593 \times 6882791 = 22944423898063$$

thepace's algorithm:

Result: 6882791
Time required to calculate in nanoseconds: 31844297317

My algorithm:

Result: 6882791
Time required to calculate in nanoseconds: 19238405860

Ooh, that seems like a massive improvement over thepace's algorithm.

Now, we'll try a large number of small primes multiplied together, using 1000 trials once again.

$$2^2 \times 3^9 \times 7^7 \times 13^2 = 10957822683444$$

thepace's algorithm:

Result: 1
Time required to calculate in nanoseconds: 8953229

Well, it broke... As it turns out, there're 2 errors in thepace's algorithm. Firstly, the isprime function does not detect 2 and 3 as primes. Secondly, the algorithm cannot handle repeated prime factors. This is why the algorithm works for our case in Project Euler but not in this case.

Here's my algorithm:

Result: 13
Time required to calculate in nanoseconds: 1589141

This one is pretty quick because my algorithm works better for small primes.

Testing product of first 15 primes which is:

$$2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17 \times 10 \times 23 \times 29 \times 31 \times 37 \times 41 \times 43 \times 47 = 614889782588491410$$

thepace's algorithm:

Result: 47
Time required to calculate in nanoseconds: 7346640

My algorithm:

Result: 47
Time required to calculate in nanoseconds: 2438296

Once again, my algorithm beats thepace's algorithm.


My original solution in Haskell is as follows:

module Main where

intSqrt :: Integer -> Integer
intSqrt = floor . sqrt . fromIntegral

maxPrimeFact :: Integer -> Integer
maxPrimeFact n = go n 1 (2 : [3, 5..intSqrt n])
  where
    go 1 maxPrime _ = maxPrime
    go n _ [] = n
    go n maxPrime wheel@(w:ws)
        | m == 0 = go d w $ takeWhile (<= intSqrt d) wheel
        | otherwise = go n maxPrime ws
        where
            (d, m) = n `divMod` w

problem3 :: Integer -> Integer
problem3 = maxPrimeFact

main :: IO ()
main = do
    _ <- getLine
    contents <- getContents
    let cases = map read $ lines contents
    let results = map problem3 cases
    mapM_ print results

Credit must go to Martin R for the original solution.

wei2912
  • 1.3k
  • 8
  • 26