Recently a friend of mine who was staying at a hotel revealed that the six digit security code for the room was 313131. This looked like an easy code to crack and I was reminded of a post that I'd made recently titled Passcodes and Repeated Digits in November of 2025. Figure 1 shows the probabilities for the number of distinct digits chosen:
![]() |
Figure 1 |
Naturally, I wondered how many other numbers share this property and so I set Gemini to investigate this with the following prompt:
Write a program in Python, tailored for insertion into SageMathCell or a Jupyter notebook, that identifies all composite numbers from 4 up to a user chosen upper limit with the property that the prime factors, when arranged in a suitable order and then concatenated, produce a palindromic number. The default upper limit can be set to 40000. An example of such a number would be 313131 = 3 x 7 x 13 x 31 x 37 that can written as 7 x 3 x 13 x 31 x 37 to produce the palindromic number 73133137 when concatenated. The output of the program should be a table showing number and factorisation followed by a comma separated list of the qualifying numbers.
Naturally this a processor intensive process and my Jupyter notebook struggled mightily to generate the list of suitable numbers. After some time, I decided to restrict the range to between 28000 and 29000 and after further modifications by Gemini, I was able to achieve the following list of numbers:
28072, 28125, 28194, 28224, 28242, 28273, 28308, 28322, 28332, 28416, 28431, 28448, 28585, 28589, 28593, 28601, 28602, 28609, 28620, 28672, 28685, 28692, 28750, 28800, 28812, 28847, 28951
Figure 2 shows the details:
![]() |
Figure 2 |
![]() |
Figure 3 |
import sys
def get_prime_factors(n):
"""Returns a list of prime factors of n."""
factors = []
d = 2
temp = n
while d * d <= temp:
while temp % d == 0:
factors.append(d)
temp //= d
d += 1
if temp > 1:
factors.append(temp)
return factors
def is_palindrome(s):
"""Checks if a string is a palindrome."""
return s == s[::-1]
def distinct_permutations(iterable):
"""
Yields unique permutations of items in iterable.
This handles repeated elements efficiently (e.g., [2, 2, 3])
without generating all n! redundant combinations.
"""
# Sort the list to start with the lexicographically first permutation
s = sorted(iterable)
yield tuple(s)
n = len(s)
while True:
# 1. Find the largest index i such that s[i] < s[i+1]
i = n - 2
while i >= 0 and s[i] >= s[i+1]:
i -= 1
if i == -1:
return # All permutations generated
# 2. Find the largest index j such that s[i] < s[j]
j = n - 1
while s[j] <= s[i]:
j -= 1
# 3. Swap s[i] and s[j]
s[i], s[j] = s[j], s[i]
# 4. Reverse the sequence from s[i+1] up to the end
s[i+1:] = s[i+1:][::-1]
yield tuple(s)
def find_palindromic_composites(start_n, end_n):
results = []
actual_start = max(4, start_n)
for n in range(actual_start, end_n + 1):
factors = get_prime_factors(n)
# Skip primes
if len(factors) < 2:
continue
found_property = False
winning_perm = None
winning_concat = None
# Use the optimized generator instead of itertools.permutations
for p in distinct_permutations(factors):
concat_str = "".join(map(str, p))
if is_palindrome(concat_str):
found_property = True
winning_perm = p
winning_concat = concat_str
break
if found_property:
fact_str = " x ".join(map(str, winning_perm))
results.append((n, fact_str, winning_concat))
return results
# --- Configuration ---
LOWER_BOUND = 28000
UPPER_BOUND = 29000
# --- Main Execution ---
print(f"Searching for composite numbers between {LOWER_BOUND} and {UPPER_BOUND}...")
print("-" * 80)
print(f"{'Number':<8} | {'Valid Factor Ordering':<35} | {'Resulting Palindrome'}")
print("-" * 80)
qualifying_numbers = find_palindromic_composites(LOWER_BOUND, UPPER_BOUND)
if not qualifying_numbers:
print(f"No qualifying numbers found in the range {LOWER_BOUND} to {UPPER_BOUND}.")
else:
for num, factor_str, pal_str in qualifying_numbers:
print(f"{num:<8} | {factor_str:<35} | {pal_str}")
print("-" * 80)
print("\nComma separated list of qualifying numbers:")
print(", ".join(str(r[0]) for r in qualifying_numbers))




