1

Problem 35 of Project Euler is as so:

The number, 197, is called a circular prime because all rotations of the digits: 197, 971, and 719, are themselves prime.

There are thirteen such primes below 100: 2, 3, 5, 7, 11, 13, 17, 31, 37, 71, 73, 79, and 97.

How many circular primes are there below one million?

My, rather crude, approach is to first generate all the primes below one million then filter out all the primes which contain even digits or the number 5 (as there will always be a non-prime permutation). Then, for each element in this reduced list of primes to return all the possible permutations of the number using the permutations() function in the itertools module, then to check if any of these permutations are not prime and if so to remove the element from the list of primes.

from itertools import permutations

def gen_primes(limit):
    D = {}
    q = 2
    while q <= limit:
        if q not in D:
            yield q
            D[q * q] = [q]
        else:
            for p in D[q]:
                D.setdefault(p + q, []).append(p)
            del D[q]       
        q += 1 

def odd_primes(limit):
    r = list(gen_primes(limit))
    for i in r[:]:
        for j in str(i):
            if any(int(j)%2 == 0 or int(j) == 5 for j in str(i)):
                r.remove(i)
                break
    r.extend([2,5])
    return r    

def circular_list():
    prime_list = odd_primes(1000000)
    for i in prime_list[:]:
        perm = [''.join(j) for j in permutations(str(i))]
        if any(int(j) not in prime_list for j in perm):
            prime_list.remove(i)
            break
    return prime_list

print len(circular_list)

The output yields a value which is incorrect by some margin. I've really been struggling to find the mistake in either the logic or code (or both). Is the permutations() function a viable approach?

I understand that there are more efficient approaches, but I'd be grateful if someone could point me in a direction to make this one work.

Community
  • 1
  • 1
ggordon
  • 259
  • 1
  • 3
  • 16
  • 3
    Not all permutations are rotations. – jwodder May 16 '16 at 21:48
  • Do you know that 'gen_primes' actually does what it says on the tin ? Also your code in 'odd_primes' looks strange to me ... i think it is trying to remove primes from the list which have either a 2 or a 5 in them - a quicker way might be to use 'in' (i.e. `if '2' in str(i) or "5" in str(i)`) – Tony Suffolk 66 May 16 '16 at 21:50

3 Answers3

1

First issue is you need rotations, not permutations. A deque can be rotated so we can substitute that. Second issue is that odd_primes() is a speed optimization that shouldn't be added until the basic code is working, so we'll leave it out for now and have circular_list() call gen_primes() directly. Of course, a prime generator isn't optimal, as you discovered, since we have to look back into the list of primes and a generator can only be iterated once.

So here's what is hopefully working code with a deque and sans odd_primes():

from collections import deque

def gen_primes(limit):
    D = {}
    q = 2
    while q <= limit:
        if q not in D:
            yield q
            D[q * q] = [q]
        else:
            for p in D[q]:
                D.setdefault(p + q, []).append(p)
            del D[q]       
        q += 1 

def circular_list(limit):
    circular = []

    primes = list(gen_primes(limit))

    for prime in primes:
        string = str(prime)
        digits = deque(string)

        for rotation in range(1, len(string)):
            digits.rotate(1)

            if int("".join(digits)) not in primes:
                break
        else:
            circular.append(prime)

    return circular

print(circular_list(1000000))

Which outputs:

[2, 3, 5, 7, 11, 13, 17, 31, 37, 71, 73, 79, 97, 113, 131, 197, 199, 311, 337, 373, 719, 733, 919, 971, 991, 1193, 1931, 3119, 3779, 7793, 7937, 9311, 9377, 11939, 19391, 19937, 37199, 39119, 71993, 91193, 93719, 93911, 99371, 193939, 199933, 319993, 331999, 391939, 393919, 919393, 933199, 939193, 939391, 993319, 999331]

If that's valid output, now go back and slip in odd_primes() to see if there are any tricks you can play to optimize the speed.

cdlane
  • 40,441
  • 5
  • 32
  • 81
1

Using the great code from cdlane and introducing the speed optimization based on that a circular prime with at least two digits can only consist of combinations of the digits 1, 3, 7 or 9, because having 0, 2, 4, 6 or 8 as the last digit makes the number divisible by 2, and having 0 or 5 as the last digit makes it divisible by 5 (from https://en.wikipedia.org/wiki/Circular_prime) you get:

from collections import deque
import re
import time

def gen_primes(limit):
    D = {}
    q = 2
    while q <= limit:
        if q not in D:
            yield q
            D[q * q] = [q]
        else:
            for p in D[q]:
                D.setdefault(p + q, []).append(p)
            del D[q]
        q += 1

def exclude_primes(primes_list):
    regex = re.compile("[024568]")
    included_primes_list = [str(prime) for prime in primes_list if prime < 10 or regex.search(str(prime)) is None]
    return included_primes_list

def circular_list(limit):
    circular_count = 0

    primes = set(exclude_primes(gen_primes(limit)))

    for prime in primes.copy(): # need copy to process allowing update original primes list
        digits = deque(prime)

        rotations = set()
        for rotation in range(len(digits)):
            digits.rotate(1)
            rotations.add("".join(digits))
        # check all rotations at once
        if rotations.issubset(primes):
            circular_count += len(rotations)
        # remove all rotations already checked from primes list
        primes.difference_update(rotations)

    return circular_count

start_cpu_time = time.clock()
print("Number of primes:", circular_list(1000000))
end_cpu_time = time.clock()
print("CPU seconds:", end_cpu_time - start_cpu_time)

This is a lot faster than without the optimization, but can probably be optimized further.

MTset
  • 90
  • 5
  • 1
    Nice splicing in of an optimization! I was going to ask about the cost of determining prime vs. searching for all the rotations, to see if made more sense to put your optimization into `gen_primes()`. However, I hit upon an optimization that matches yours for speed with minimal change. Since the question the OP was asked was about the count of such numbers, you can replace `list(gen_primes(limit))` in my rework with `set(gen_primes(limit))` and get the same speed up as you did! (Just results not in order.) This problem can be optimized in lots of fun ways! – cdlane May 17 '16 at 02:03
  • You were obviously on the right track with set and pointing out that only the count is required. Sigh! We had a ways to go to get to a solution as nice as: http://blog.dreamshire.com/project-euler-35-solution/. Some very cool functions behind the scenes found here: http://blog.dreamshire.com/common-functions-routines-project-euler/ – MTset May 17 '16 at 04:27
  • with `set`, the speedup of `excluded_primes` is 15%. – Will Ness May 17 '16 at 19:45
  • Building on the idea of using sets, especially if Will is right about the speedup, I refactored code to use sets more fully and optimised it not to reprocess the same circular primes. So for each check of the primes list it gets shorter, which I assume makes it faster to check (though partially offset by updating said list). – MTset May 17 '16 at 23:21
  • 1
    @MTset it is easy to check the speed by [running it on Ideone.com](http://ideone.com/cGP2Qm). It reports the run time. – Will Ness May 18 '16 at 15:51
  • Thank you @Will Ness for introducing me to https://ideone.com/. Looks very useful. Interestingly it took nearly twice the time to run there as on my machine in PyCharm. – MTset May 19 '16 at 00:21
0

Here's how I solved this, takes about 1.1s to run on my laptop...

It preloads the return list with 2 and 5.

Basically, it runs through all odd numbers between 3 and 1e6.

First, it runs all of the digits to see if any of them are divisible by 2 or 5.

If there are no such digits, it checks if the number is prime. First, it maintains a list of primes that it has found, and then runs the 6k+1 algorithm to test after that.

For numbers that are prime, it then runs all the rotations (using a quick string rotation function) to check if all of them are prime. If they are, it adds that number to the list.

Once all the numbers up to 1e6 are checked, it kicks out the count and the number of seconds it took to run.

It ran quickly enough I extended it to 100,000,000 (130s runtime), and found no additional matches.

"""
Circular primes
Problem 35 
The number, 197, is called a circular prime because all rotations of the digits: 197, 971, and 719, are themselves prime.

There are thirteen such primes below 100: 2, 3, 5, 7, 11, 13, 17, 31, 37, 71, 73, 79, and 97.

How many circular primes are there below one million?
"""
import time
start_time = time.time()


foundprimes = []

def isPrime(n):
    """Determine if cand is a prime number"""
    if n in foundprimes:
        return True

    if n <= 3:
        return n>1
    if n%2 == 0:
        return False
    if n%3 == 0:
        return False

    i=5
    while (i*i <= n):
        if n%i == 0:
            return False
        if n%(i+2) == 0:
            return False
        i+=6

    foundprimes.append(n)
    return True

def rotations(num):
    """Return all rotations of the given number"""

    if abs(num) < 10:
        return [num]

    numstr = str(num)
    strlen = len(numstr)
    returnarray = []
    for i in range(strlen):
        if i==0:
            pass
        else:
            start = numstr[i:]
            end = numstr[0:i]
            returnarray.append(int(start+end))


    return returnarray

def QCheck(num):
    """Do a quick check if there is an even number in the set...at some point it will be an even number in rotation"""
    numstr = str(num)
    for i in range(len(numstr)):
        if int(numstr[i]) % 2 == 0:
            return False
        if int(numstr[i]) % 5 == 0:
            return False

    return True


allrotations = [2,5]

for i in range(3,1000000,2):
    if QCheck(i):
        prime = isPrime(i)
        if prime:
            rotcheck = True
            for rot in rotations(i):
                if isPrime(rot) == False:
                    rotcheck = False

            if rotcheck:
                allrotations.append(i)

print(len(allrotations))
print("--- %s seconds ---" % (time.time() - start_time))