Problem 145: How Many Reversible Numbers Are There Below One-Billion?

View on Project Euler

Project Euler Problem 145 Solution

EulerSolve provides an optimized solution for Project Euler Problem 145, How Many Reversible Numbers Are There Below One-Billion?, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary A positive integer \(n\) is called reversible when \(n+\operatorname{rev}(n)\) is made entirely of odd digits, and neither \(n\) nor its reversal is allowed to acquire leading zeros. For numbers below \(10^9\), that means counting all valid lengths from 1 through 9 while excluding any number ending in 0. The important point is that reversibility is controlled by base-10 addition with carries. Once the digits are grouped into mirrored pairs, the search stops being about individual integers and becomes a finite combinatorial count over pair sums and carry states. Mathematical Approach Write an \(L\)-digit number as $$n=\sum_{i=0}^{L-1} x_i 10^i,\qquad x_{L-1}\neq 0,\qquad x_0\neq 0.$$ The condition \(x_0\neq 0\) is exactly the rule that the reversal may not begin with 0. The whole argument comes from studying the addition \(n+\operatorname{rev}(n)\) column by column. Mirrored columns and the carry recurrence Column \(i\) adds the mirrored digits \(x_i\) and \(x_{L-1-i}\). With incoming carry \(c_i\) and \(c_0=0\), define $$t_i=x_i+x_{L-1-i}+c_i,\qquad d_i=t_i \bmod 10,\qquad c_{i+1}=\left\lfloor\frac{t_i}{10}\right\rfloor.$$ Reversibility means every visible digit \(d_i\) is odd. Because \(x_i+x_{L-1-i}\le 18\), every carry is either 0 or 1....

Detailed mathematical approach

Problem Summary

A positive integer \(n\) is called reversible when \(n+\operatorname{rev}(n)\) is made entirely of odd digits, and neither \(n\) nor its reversal is allowed to acquire leading zeros. For numbers below \(10^9\), that means counting all valid lengths from 1 through 9 while excluding any number ending in 0.

The important point is that reversibility is controlled by base-10 addition with carries. Once the digits are grouped into mirrored pairs, the search stops being about individual integers and becomes a finite combinatorial count over pair sums and carry states.

Mathematical Approach

Write an \(L\)-digit number as

$$n=\sum_{i=0}^{L-1} x_i 10^i,\qquad x_{L-1}\neq 0,\qquad x_0\neq 0.$$

The condition \(x_0\neq 0\) is exactly the rule that the reversal may not begin with 0. The whole argument comes from studying the addition \(n+\operatorname{rev}(n)\) column by column.

Mirrored columns and the carry recurrence

Column \(i\) adds the mirrored digits \(x_i\) and \(x_{L-1-i}\). With incoming carry \(c_i\) and \(c_0=0\), define

$$t_i=x_i+x_{L-1-i}+c_i,\qquad d_i=t_i \bmod 10,\qquad c_{i+1}=\left\lfloor\frac{t_i}{10}\right\rfloor.$$

Reversibility means every visible digit \(d_i\) is odd. Because \(x_i+x_{L-1-i}\le 18\), every carry is either 0 or 1. If a final carry remains after the most significant column, it can only be 1, and that extra leading digit is still odd, so it is harmless.

This immediately gives the parity rule used by the implementations: if the incoming carry is 0, then the mirrored digit sum must be odd; if the incoming carry is 1, then the mirrored digit sum must be even.

Compressing the search to pair sums

Let \(h=\lfloor L/2\rfloor\) and define the mirrored pair sums

$$s_j=x_j+x_{L-1-j}\qquad (0\le j \lt h).$$

The exact ordered pair of digits matters only through its sum and through how many digit pairs realize that sum. For the outermost pair both digits must be nonzero, so

$$O(s)=\#\{(a,b)\in\{1,\dots,9\}^2:a+b=s\}.$$

For inner pairs zeros are allowed, so

$$I(s)=\#\{(a,b)\in\{0,\dots,9\}^2:a+b=s\}.$$

These multiplicities are

$$O(s)=\begin{cases} s-1,&2\le s\le 10,\\ 19-s,&11\le s\le 18,\\ 0,&\text{otherwise,} \end{cases} \qquad I(s)=\begin{cases} s+1,&0\le s\le 9,\\ 19-s,&10\le s\le 18,\\ 0,&\text{otherwise.} \end{cases}$$

So a chosen sum pattern \((s_0,\dots,s_{h-1})\) carries weight \(O(s_0)\prod_{j=1}^{h-1} I(s_j)\). If \(L\) is odd, we also choose a middle digit \(m\), whose column contributes \(2m\).

Symmetry forces a recurrence on the carries

For even \(L\), the column-sum sequence is

$$s_0,s_1,\dots,s_{h-1},s_{h-1},\dots,s_1,s_0.$$

For odd \(L\), it becomes

$$s_0,s_1,\dots,s_{h-1},2m,s_{h-1},\dots,s_1,s_0.$$

The same pair sum \(s_j\) appears twice. Therefore the carry entering its first appearance and the carry entering its mirrored appearance must be equal; otherwise one occurrence would require \(s_j\) to be odd and the other would require it to be even. Matching the first half with the mirrored second half yields the recurrence

$$c_{j+1}=c_{j-1}\qquad (1\le j \lt h).$$

So the carry pattern on the way toward the middle has period 2. That is the key invariant behind both the formulas and the code.

Even lengths: every carry is forced to be zero

Let \(L=2k\). The central mirrored pair is used in two consecutive columns, so its two incoming carries must agree. Together with \(c_{j+1}=c_{j-1}\) and \(c_0=0\), this forces

$$c_0=c_1=\cdots=c_k=0.$$

Hence every mirrored sum must be odd and strictly below 10. A larger odd sum would create a carry and immediately break the all-zero pattern. The outer pair then has 20 choices, coming from odd sums \(3,5,7,9\) with multiplicities \(2,4,6,8\). Each inner pair has 30 choices, coming from odd sums \(1,3,5,7,9\) with multiplicities \(2,4,6,8,10\).

Therefore the number of reversible numbers with \(2k\) digits is

$$R_{2k}=20\cdot 30^{k-1}.$$

For the present range this gives

$$R_2=20,\qquad R_4=600,\qquad R_6=18000,\qquad R_8=540000.$$

Odd lengths: only \(L\equiv 3 \pmod 4\) can work

Let \(L=2k+1\). The middle column is \(2m+c_k\). Since \(2m\) is even and the resulting digit must be odd, we must have

$$c_k=1.$$

But the recurrence \(c_{j+1}=c_{j-1}\) with \(c_0=0\) makes the carries alternate:

$$0,1,0,1,\dots.$$

So \(c_k=1\) is possible only when \(k\) is odd, which means \(L\equiv 3 \pmod 4\). This proves that lengths \(1,5,9,\dots\) contribute nothing.

When \(L=4m+3\), the alternating pattern fixes the allowed sum types. The outer pair must move from carry 0 to carry 1, so its sum must be odd and at least 11, giving 20 choices. Each inner step from carry 1 to carry 0 needs an even sum below 10, giving 25 choices. Each inner step from carry 0 to carry 1 needs an odd sum above 10, giving 20 choices. The middle digit must satisfy \(2m+1 \lt 10\), so there are 5 choices.

Thus

$$R_{4m+3}=20\cdot(25\cdot 20)^m\cdot 5=100\cdot 500^m,\qquad R_{4m+1}=0.$$

Below \(10^9\) this yields

$$R_3=100,\qquad R_7=50000,\qquad R_5=R_9=0.$$

Worked examples

The 2-digit number \(36\) is reversible because \(36+63=99\). The only mirrored sum is \(3+6=9\), which is odd and below 10, so neither column creates a carry.

The 3-digit number \(219\) is reversible because \(219+912=1131\). The outer sum is \(2+9=11\), so the carry pattern begins \(0\to 1\). The middle column is \(2\cdot 1+1=3\), which returns the carry to 0. The last outer column again contributes 11 and produces the leading 1. Every digit of \(1131\) is odd, exactly as the carry analysis predicts.

Adding the nonzero counts gives the total below \(10^9\):

$$20+100+600+18000+50000+540000=608720.$$

How the Code Works

The C++, Python, and Java implementations do not brute-force all integers below one billion. Instead, they build the combinatorial count directly from mirrored digit-pair sums.

Multiplicity tables for digit-pair sums

The implementation first counts how many ordered digit pairs produce each sum from 0 to 18. One table is for the outer pair, where both digits must lie in \(\{1,\dots,9\}\). The other is for inner pairs, where digits may be 0. This turns many different digit assignments into a single weighted state.

Depth-first enumeration by length

For each digit length from 2 through 9, the implementation chooses the sequence \((s_0,\dots,s_{h-1})\) recursively. Every time a new mirrored sum is chosen, the running weight is multiplied by the number of digit pairs that realize it. For odd lengths, the implementation also tries all 10 possible middle digits.

Carry validation of a completed pattern

Once a full sum pattern has been chosen, the implementation reconstructs the column sequence \(s_0,\dots,s_{h-1},2m,s_{h-1},\dots,s_0\) when needed, then simulates the addition from right to left. If any produced digit is even, the pattern is discarded immediately. Otherwise its accumulated weight is added to the answer. The C++ version also includes small checkpoint counts such as \(R_2=20\), \(R_3=100\), and \(R_7=50000\) as sanity checks.

Why this matches the derivation

The code is a direct implementation of the mathematics above: mirrored digit pairs are collapsed into sum states, the carry recurrence is evaluated exactly, and multiplicities account for all concrete digit choices. The closed forms \(20\cdot 30^{k-1}\) and \(100\cdot 500^m\) are consequences of those rules, not separate assumptions.

Complexity Analysis

For a fixed length \(L\), the search explores at most \(19^{\lfloor L/2\rfloor}\) mirrored-sum patterns, with an extra factor of 10 for odd lengths because of the middle digit. Each completed pattern is checked in \(O(L)\) time by simulating the carries. So the generic complexity per length is

$$O\!\left(L\cdot 19^{\lfloor L/2\rfloor}\right),$$

with \(O(L)\) memory for the current pattern and carry scan.

For this problem \(L\le 9\), so the actual runtime is tiny. Mathematically, once the carry invariants are recognized, the answer can even be written in closed form; the implementations keep the weighted DFS because it is short, exact, and still effectively constant-time for the required range.

Footnotes and References

  1. Problem page: Project Euler 145
  2. Carry in addition: Wikipedia - Carry (arithmetic)
  3. Positional notation: Wikipedia - Positional notation
  4. Parity: Wikipedia - Parity (mathematics)
  5. Digit reversal: MathWorld - Digit Reversal

Problem 145 source code

C++

#include <array>
#include <cstdint>
#include <iostream>
#include <stdexcept>
#include <string>

namespace {

using u64 = std::uint64_t;

struct Options {
    u64 limit = 1000000000ULL;
    bool run_checkpoints = true;
};

bool parse_u64_after_prefix(const std::string& arg, const std::string& prefix, u64& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }

    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }

    u64 parsed = 0;
    for (char c : tail) {
        if (c < '0' || c > '9') {
            return false;
        }
        parsed = parsed * 10ULL + static_cast<u64>(c - '0');
    }
    value = parsed;
    return true;
}

bool parse_arguments(int argc, char** argv, Options& options) {
    for (int i = 1; i < argc; ++i) {
        const std::string arg(argv[i]);

        if (arg == "--skip-checkpoints") {
            options.run_checkpoints = false;
            continue;
        }
        if (parse_u64_after_prefix(arg, "--limit=", options.limit)) {
            continue;
        }

        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.limit >= 1;
}

u64 pow10_u64(int e) {
    u64 v = 1;
    for (int i = 0; i < e; ++i) {
        v *= 10ULL;
    }
    return v;
}

int digits_count(u64 n) {
    int d = 0;
    do {
        ++d;
        n /= 10ULL;
    } while (n > 0);
    return d;
}

u64 count_for_length(const int length) {
    if (length <= 1) {
        return 0;
    }

    std::array<int, 19> outer_count{};
    std::array<int, 19> inner_count{};

    for (int a = 1; a <= 9; ++a) {
        for (int b = 1; b <= 9; ++b) {
            ++outer_count[static_cast<std::size_t>(a + b)];
        }
    }

    for (int a = 0; a <= 9; ++a) {
        for (int b = 0; b <= 9; ++b) {
            ++inner_count[static_cast<std::size_t>(a + b)];
        }
    }

    const int half = length / 2;
    std::array<int, 10> sums{};
    u64 total = 0;

    const auto apply_sequence = [&](const int center_twice, const u64 weight) -> u64 {
        int carry = 0;

        for (int i = 0; i < length; ++i) {
            int pair_sum = 0;
            if ((length % 2) == 1 && i == half) {
                pair_sum = center_twice;
            } else {
                const int idx = (i < half) ? i : (length - 1 - i);
                pair_sum = sums[static_cast<std::size_t>(idx)];
            }

            const int digit = (pair_sum + carry) % 10;
            if ((digit % 2) == 0) {
                return 0;
            }
            carry = (pair_sum + carry) / 10;
        }

        if (carry > 1) {
            return 0;
        }

        return weight;
    };

    const auto dfs = [&](auto&& self, int idx, u64 weight) -> void {
        if (idx == half) {
            if ((length % 2) == 0) {
                total += apply_sequence(0, weight);
            } else {
                for (int middle = 0; middle <= 9; ++middle) {
                    total += apply_sequence(2 * middle, weight);
                }
            }
            return;
        }

        for (int s = 0; s <= 18; ++s) {
            const int ways = (idx == 0) ? outer_count[static_cast<std::size_t>(s)]
                                        : inner_count[static_cast<std::size_t>(s)];
            if (ways == 0) {
                continue;
            }
            sums[static_cast<std::size_t>(idx)] = s;
            self(self, idx + 1, weight * static_cast<u64>(ways));
        }
    };

    dfs(dfs, 0, 1ULL);
    return total;
}

bool is_power_of_ten(const u64 x) {
    if (x == 0) {
        return false;
    }
    u64 v = x;
    while ((v % 10ULL) == 0ULL) {
        v /= 10ULL;
    }
    return v == 1ULL;
}

u64 solve(const u64 limit) {
    if (limit <= 1) {
        return 0;
    }

    const int max_len = digits_count(limit - 1);
    u64 total = 0;

    for (int len = 1; len < max_len; ++len) {
        total += count_for_length(len);
    }

    if (limit == pow10_u64(max_len)) {
        total += count_for_length(max_len);
        return total;
    }

    if (limit <= 10000000ULL) {
        for (u64 n = pow10_u64(max_len - 1); n < limit; ++n) {
            if ((n % 10ULL) == 0ULL) {
                continue;
            }
            u64 rev = 0;
            u64 t = n;
            while (t > 0) {
                rev = rev * 10ULL + (t % 10ULL);
                t /= 10ULL;
            }
            u64 s = n + rev;
            bool ok = true;
            while (s > 0) {
                if (((s % 10ULL) % 2ULL) == 0ULL) {
                    ok = false;
                    break;
                }
                s /= 10ULL;
            }
            if (ok) {
                ++total;
            }
        }
        return total;
    }

    throw std::runtime_error("Non power-of-10 limits above 10^7 are not supported");
}

bool run_checkpoints() {
    if (count_for_length(2) != 20ULL) {
        std::cerr << "Checkpoint failed for length 2" << '\n';
        return false;
    }
    if (count_for_length(3) != 100ULL) {
        std::cerr << "Checkpoint failed for length 3" << '\n';
        return false;
    }
    if (count_for_length(7) != 50000ULL) {
        std::cerr << "Checkpoint failed for length 7" << '\n';
        return false;
    }
    return true;
}

}  // namespace

int main(int argc, char** argv) {
    Options options;
    if (!parse_arguments(argc, argv, options)) {
        return 1;
    }
    if (options.run_checkpoints && !run_checkpoints()) {
        return 2;
    }

    try {
        std::cout << solve(options.limit) << '\n';
    } catch (const std::exception& ex) {
        std::cerr << ex.what() << '\n';
        return 3;
    }

    return 0;
}

Python

# Problem 145: How many reversible numbers are there below one-billion?
# Digit-pair DFS approach: for n-digit numbers, pair digits from outside in.

def solve():
    # Counts for digit pair sums
    outer = [0] * 19  # first/last digit (1-9 each, no leading zeros)
    inner = [0] * 19  # inner digit pairs (0-9 each)
    for a in range(1, 10):
        for b in range(1, 10):
            outer[a + b] += 1
    for a in range(10):
        for b in range(10):
            inner[a + b] += 1
    
    total = 0
    for length in range(2, 10):  # up to 9 digits (< 10^9)
        half = length // 2
        
        def dfs(idx, weight, sums):
            nonlocal total
            if idx == half:
                # Check all digit sums with carries
                if length % 2 == 0:
                    check(sums, 0, weight, length)
                else:
                    for mid in range(10):
                        check(sums, 2 * mid, weight, length)
                return
            
            cnt = outer if idx == 0 else inner
            for s in range(19):
                if cnt[s] == 0: continue
                dfs(idx + 1, weight * cnt[s], sums + [s])
        
        def check(sums, center_twice, weight, ln):
            nonlocal total
            carry = 0
            for i in range(ln):
                if ln % 2 == 1 and i == half:
                    pair_sum = center_twice
                else:
                    idx = i if i < half else (ln - 1 - i)
                    pair_sum = sums[idx]
                digit = (pair_sum + carry) % 10
                if digit % 2 == 0:
                    return
                carry = (pair_sum + carry) // 10
            if carry > 1:
                return
            total += weight
        
        dfs(0, 1, [])
    
    print(total)

solve()

Java

public class Euler145 {
    static int[] outer = new int[19], inner = new int[19];
    static long total;

    public static void main(String[] args) {
        for (int a = 1; a <= 9; a++)
            for (int b = 1; b <= 9; b++)
                outer[a + b]++;
        for (int a = 0; a <= 9; a++)
            for (int b = 0; b <= 9; b++)
                inner[a + b]++;
        long ans = 0;
        for (int len = 2; len <= 9; len++) {
            total = 0;
            dfs(len, 0, 1, new int[len / 2]);
            ans += total;
        }
        System.out.println(ans);
    }

    static void dfs(int len, int idx, long w, int[] sums) {
        int half = len / 2;
        if (idx == half) {
            if (len % 2 == 0)
                check(sums, 0, w, len);
            else
                for (int m = 0; m <= 9; m++)
                    check(sums, 2 * m, w, len);
            return;
        }
        int[] cnt = idx == 0 ? outer : inner;
        for (int s = 0; s <= 18; s++) {
            if (cnt[s] == 0)
                continue;
            sums[idx] = s;
            dfs(len, idx + 1, w * cnt[s], sums);
        }
    }

    static void check(int[] sums, int ct, long w, int ln) {
        int half = ln / 2, carry = 0;
        for (int i = 0; i < ln; i++) {
            int ps;
            if (ln % 2 == 1 && i == half)
                ps = ct;
            else {
                int ix = i < half ? i : ln - 1 - i;
                ps = sums[ix];
            }
            int d = (ps + carry) % 10;
            if (d % 2 == 0)
                return;
            carry = (ps + carry) / 10;
        }
        if (carry > 1)
            return;
        total += w;
    }
}