The Binding of Isaac (Gen. 22) is a fascinating story, for a multitude of reasons.

One thing that appeals to me, as a shochet (slaughterer), is that it has in it (as far as I could tell) the earliest instance of the word "שחט" in the Torah (22:10).

I was wondering if the telling of the Akeidah contains within it anything for us practitioners of shechita, about the halachos of shechita.


Question: Lychrel calculator (Project Euler 55)
Answer: Hints


According to the example in the problem description, 349 is not a Lychrel number. As a hint for speeding things up, I'd like to point out that the example computation might also tell you something about, say, the number 1292.

What I'm trying to say is that when you evaluate 1292, you will be performing part of the same computation that you already did with 349. The same applies to the reversed forms of each number, but there's a catch: the reversal is not symmetric when trailing zeros are present. 210 reversed is 12, but 12 reversed is 21.
A single reverse-add operation typically maps multiple numbers to the same number. If you figure out how that works, you can reduce the number of cases you need to consider.


Changes suggested by hint 1


To be able to take timings with timeit, I first rearranged your top-level code to a function. I also changed the ranges to 50 and 10000, for reasons explained in Gareth's answer.

def e55():
    candidates = []
    for number in range(10000):
        if is_lychrel(number):
            candidates.append(number)
    return len(candidates)


This runs in 158 ms on my computer:

$ python3 -m timeit -s "from e55_op import e55" "e55()" 
10 loops, best of 3: 158 msec per loop    

The main function could be written more concisely. This one is marginally slower at 161 ms:

def e55():
    return sum(1 for number in range(10000) if is_lychrel(number))

Now to the performance optimization. I want to avoid following the same sequence of iteration more than once. First, I'll extract the loop from is_lychrel into a recursive function. This in itself does not make it run faster, but neither does it slow it down.

def check(number, depth):
    ''' Returns True if 'number' is neither a palindrome
    nor becomes one in 'depth' iterations of reverse-add.'''
    rev = reverse(number)
    return number != rev and (not depth or check(number + rev, depth-1))

def is_lychrel(number):
    ''' Returns True if 'number' is a lychrel candidate,
        but works only with numbers from 1 to 10000 '''
    return check(number + reverse(number), 49) 

The speedup comes from making the recursive function cache its return values in a dictionary. I'll also eliminate the is_lychrel function as it is called from only one place.


The complete code is below and runs in 36.5 ms. I also timed Gareth's proposal: 54.8 ms.

def reverse(number):
    ''' Returns the reverse order of an integer,
        for example: reverse(123) returns the integer 321 '''
    number = str(number)
    return int(number[::-1])

def check(number, depth, cache):
    ''' Returns True if 'number' is neither a palindrome
        nor becomes one in 'depth' iterations of reverse-add.'''
    if number not in cache:
        rev = reverse(number)
        cache[number] = (number != rev 
            and (not depth or check(number + rev, depth-1, cache)))
    return cache[number]

def e55():
    cache = {}
    return sum(1 for number in range(10000) 
               if check(number + reverse(number), 49, cache))


Notes:


I'm resetting the cache inside e55 because timeit runs the function multiple times. Each iteration has to be able to start from the same state.
Calling check with depth=49 causes 50 iterations to be evaluated, because it starts from an already reverse-added number.


Hint 2: math

Let's consider all 4-digit numbers. Labeling the digits abcd we can represent the number as 

a*1000 + b*100 + c*10 + d


Reverse-add results in

(a+d)*1000 + (b+c)*100 + (c+b)*10 + (d+a)
= (a+d)*1001 + (b+c)*110


As a ranges from 1 to 9 and the other digits from 0 to 9, (a+d) can take values from 1 to 18 and (b+c) from 0 to 18. Thus there are 18*19 = 342 possible values for the expression (a+d)*1001 + (b+c)*110. That gives an opportunity to speed up the program if we make it loop over the 342 sums instead of the 9000 4-digit numbers. There is the extra complication that we need to calculate how many numbers map to each reverse-add result, but that is not very complicated. 

The 3-, 2- and 1-digit cases can be analyzed in the same way.

Hint 2: implementation

I'm using collections.Counter to determine how many ways there are to add two numbers for each sum. I've inlined the reverse function because it is called from only one place.

This code runs in 4.64 ms, which is a 34X speedup from the original.

from collections import Counter

def check(number, depth, cache):
    ''' Returns True if 'number' is neither a palindrome
        nor becomes one in 'depth' iterations of reverse-add.'''
    if number not in cache:
        rev = int(str(number)[::-1])
        cache[number] = (number != rev 
            and (not depth or check(number + rev, depth-1, cache)))
    return cache[number]

def cases():
    ''' Yields numbers that can result from reverse-adding numbers
        below 10000, and the frequency of each'''
    freqs00_99 = Counter(i % 10 + i // 10 for i in range(100))
    freqs10_99 = Counter(i % 10 + i // 10 for i in range(10,100))

    for i in range(10):
        yield 2*i, 1        # from one digit
    for i, f_outer in freqs10_99.items():
        yield 11*i, f_outer # from two digits
        for j in range(10):
            yield 101*i + 20*j, f_outer  # from three digits
        for j, f_inner in freqs00_99.items():
            yield 1001*i + 110*j, f_outer*f_inner   # from four digits

def e55():
    cache = {}
    return sum(freq for i, freq in cases() if check(i, 49, cache))