HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

The Palindromic Odometer Puzzler: A Programmatic Solution

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
theodometerprogrammaticsolutionpuzzlerpalindromic

Problem

That is a programmatic solution to a puzzler from Car Talk, its summary is:


"I noticed that the last 4 digits were palindromic. I drove a mile,
and the last 5 were palindromic. I drove another mile and the middle 4
were palindromic, and the ends were not involved. And then one mile
later, all 6 digits were palindromic."

def is_palindrome(string):
    """Finds if a string is a plaindrome or not.

       string: str

       returns: bool
       """
    return string == string[::-1]

def find_puzzle_number():
    """Finds the number which was first seen.

       prints: int
       """
    guess = 100000  
    # the odometer shows only 6 numbers,
    # therefore 999999 is the last number to test.
    while guess < 1000000:
        if is_palindrome(str(guess)[2:]):  # last 4 numbers.       
            if is_palindrome(str(guess + 1)[1:]):  # last 5 numbers.
                if is_palindrome(str(guess + 2)[1:5]):  # middle 4 numbers.     
                    if is_palindrome(str(guess + 3)):
                        print(guess)

        guess = guess + 1

print('These are the possible odometer readings: ')
find_puzzle_number()


Notes:

  • I'm a hobbyist and a beginner.



  • I don't only want to optimize the code, but also my skills and style. Therefore any notes, advice and suggestions aren't only welcomed, but also encouraged!

Solution

The documentation

string: str

   returns: bool


is pretty obvious. I would skip it. The rest of the documentation can be more idiomatically written as

"""Return whether input sequence is a palindrome."""


(note palindrome, not plaindrome). I removed the term string because many sequence types can be palindromic, like [1, 2, 3, 2, 1].

find_puzzle_number should yield, not print, its values.

I would change the documentation to specify the rules the output satisfy.

def find_puzzle_number():
    """Generates 6-digit integers, x, where all of

    * the last 4 digits of x,
    * the last 5 digits of x+1,
    * the middle 4 digits of x+2, and
    * all 6 digits of x+3

    are palindromes.
    """
    guess = 100000
    # the odometer shows only 6 numbers,
    # therefore 999999 is the last number to test.
    while guess < 1000000:
        if is_palindrome(str(guess)[2:]):  # last 4 numbers.
            if is_palindrome(str(guess + 1)[1:]):  # last 5 numbers.
                if is_palindrome(str(guess + 2)[1:5]):  # middle 4 numbers.
                    if is_palindrome(str(guess + 3)):
                        yield guess

        guess = guess + 1


The while should become a for

# 6 digit numbers
for guess in range(10**5, 10**6):


Then the if chain of is_palindromes can be simplified:

valid = (
    is_palindrome(str(guess + 0)[2:]) and
    is_palindrome(str(guess + 1)[1:]) and
    is_palindrome(str(guess + 2)[1:5]) and
    is_palindrome(str(guess + 3))
)

if valid:
    yield guess


Now note something bad: guess + 3, can overflow. Either you want to clamp the code so that guess + 3 < 10**6, or you want to perform modular arithmetic.

Further, you don't know that 10**5 is a valid lower bound: 000000 is a valid reading. You can use format(guess, "06") to pad like this.

If you care about speed, PyPy makes the code run in about a tenth the time.

def is_palindrome(seq):
    """Return whether input sequence is a palindrome."""
    return seq == seq[::-1]

def find_puzzle_number():
    """Generates 6-digit integers, x, where all of

    * the last 4 digits of x,
    * the last 5 digits of x+1,
    * the middle 4 digits of x+2, and
    * all 6 digits of x+3

    are palindromes.
    """
    def as_reading(x):
        return format(x % 10**6, "06")

    # 6 digit numbers
    for guess in range(10**6):
        valid = (
            is_palindrome(as_reading(guess + 0)[2:]) and
            is_palindrome(as_reading(guess + 1)[1:]) and
            is_palindrome(as_reading(guess + 2)[1:5]) and
            is_palindrome(as_reading(guess + 3))
        )

        if valid:
            yield guess

print('These are the possible odometer readings: ')
for solution in find_puzzle_number():
    print(solution)


Note that there are simple optimizations you can apply. For instance, you can easily iterate over only those values for which guess + 3 is a palindrome by construction - this reduces the values to iterate over from \$10^6\$ to \$10^3\$.

for guess_digits in range(10**3):
    # guess + 3 is a palindrome
    # NOTE: relies on correct handling of wrapping
    guess = guess_digits + int(format(guess_digits, "06")[::-1]) - 3


This makes the code basically instantaneous, so if optimizations were pointless to apply before they're most certainly pointless to apply now.

Code Snippets

string: str

   returns: bool
"""Return whether input sequence is a palindrome."""
def find_puzzle_number():
    """Generates 6-digit integers, x, where all of

    * the last 4 digits of x,
    * the last 5 digits of x+1,
    * the middle 4 digits of x+2, and
    * all 6 digits of x+3

    are palindromes.
    """
    guess = 100000
    # the odometer shows only 6 numbers,
    # therefore 999999 is the last number to test.
    while guess < 1000000:
        if is_palindrome(str(guess)[2:]):  # last 4 numbers.
            if is_palindrome(str(guess + 1)[1:]):  # last 5 numbers.
                if is_palindrome(str(guess + 2)[1:5]):  # middle 4 numbers.
                    if is_palindrome(str(guess + 3)):
                        yield guess

        guess = guess + 1
# 6 digit numbers
for guess in range(10**5, 10**6):
valid = (
    is_palindrome(str(guess + 0)[2:]) and
    is_palindrome(str(guess + 1)[1:]) and
    is_palindrome(str(guess + 2)[1:5]) and
    is_palindrome(str(guess + 3))
)

if valid:
    yield guess

Context

StackExchange Code Review Q#136794, answer score: 7

Revisions (0)

No revisions yet.