Problem 414: Kaprekar Constant

View on Project Euler

Project Euler Problem 414 Solution

EulerSolve provides an optimized solution for Project Euler Problem 414, Kaprekar Constant, with C++, Python, Java, and a step-by-step mathematical explanation.

Problem Summary For each base \(B=6k+3\) with \(2 \le k \le 300\), we consider every integer \(i\) with \(0 \lt i \lt B^5\), write it as a 5-digit base-\(B\) string with leading zeros allowed, sort its digits in decreasing and increasing order, subtract, and repeat. For these bases, the 5-digit routine has a unique non-zero Kaprekar constant \(C_B\). Define \(s_B(i)=0\) when \(i=C_B\) or when the 5 digits of \(i\) are all equal, and otherwise let \(s_B(i)\) be the number of iterations needed to reach \(C_B\). Then $$S(B)=\sum_{0 \lt i \lt B^5} s_B(i),$$ and the goal is the last \(18\) digits of $$\sum_{k=2}^{300} S(6k+3).$$ Mathematical Approach Step 1: Compress Every 5-Digit State to Two Gaps Let the sorted digits of the current 5-digit string be $$x_0 \le x_1 \le x_2 \le x_3 \le x_4.$$ The descending and ascending numbers are $$N_{\downarrow}=x_4 B^4 + x_3 B^3 + x_2 B^2 + x_1 B + x_0,$$ $$N_{\uparrow}=x_0 B^4 + x_1 B^3 + x_2 B^2 + x_3 B + x_4.$$ The Kaprekar subtraction is therefore $$N_{\downarrow}-N_{\uparrow}=(x_4-x_0)(B^4-1)+(x_3-x_1)(B^3-B).$$ So the next value depends only on the two outer gaps $$u=x_4-x_0,\qquad v=x_3-x_1,$$ with \(0 \le v \le u \le B-1\). Define $$R_B(u,v)=u(B^4-1)+vB(B^2-1).$$ To advance one step, the implementation writes \(R_B(u,v)\) in base \(B\), sorts its five digits, and recomputes the new pair \((u',v')\)....

Detailed mathematical approach

Problem Summary

For each base \(B=6k+3\) with \(2 \le k \le 300\), we consider every integer \(i\) with \(0 \lt i \lt B^5\), write it as a 5-digit base-\(B\) string with leading zeros allowed, sort its digits in decreasing and increasing order, subtract, and repeat. For these bases, the 5-digit routine has a unique non-zero Kaprekar constant \(C_B\).

Define \(s_B(i)=0\) when \(i=C_B\) or when the 5 digits of \(i\) are all equal, and otherwise let \(s_B(i)\) be the number of iterations needed to reach \(C_B\). Then

$$S(B)=\sum_{0 \lt i \lt B^5} s_B(i),$$

and the goal is the last \(18\) digits of

$$\sum_{k=2}^{300} S(6k+3).$$

Mathematical Approach

Step 1: Compress Every 5-Digit State to Two Gaps

Let the sorted digits of the current 5-digit string be

$$x_0 \le x_1 \le x_2 \le x_3 \le x_4.$$

The descending and ascending numbers are

$$N_{\downarrow}=x_4 B^4 + x_3 B^3 + x_2 B^2 + x_1 B + x_0,$$

$$N_{\uparrow}=x_0 B^4 + x_1 B^3 + x_2 B^2 + x_3 B + x_4.$$

The Kaprekar subtraction is therefore

$$N_{\downarrow}-N_{\uparrow}=(x_4-x_0)(B^4-1)+(x_3-x_1)(B^3-B).$$

So the next value depends only on the two outer gaps

$$u=x_4-x_0,\qquad v=x_3-x_1,$$

with \(0 \le v \le u \le B-1\). Define

$$R_B(u,v)=u(B^4-1)+vB(B^2-1).$$

To advance one step, the implementation writes \(R_B(u,v)\) in base \(B\), sorts its five digits, and recomputes the new pair \((u',v')\). This gives a deterministic transition

$$T_B(u,v)=(u',v').$$

The original \(B^5\) search space is thus reduced to only \(O(B^2)\) gap states.

Step 2: The Fixed State and the Kaprekar Constant

Because \(B=6k+3\), the base is divisible by \(3\). Write

$$B=3m,$$

where \(m\) is odd. The non-zero fixed state used by the routine is

$$u=2m,\qquad v=m.$$

Substituting these values into the closed form gives

$$R_B(2m,m)=2m(B^4-1)+m(B^3-B)=2mB^4+mB^3-mB-2m.$$

In base \(B\), this is exactly

$$R_B(2m,m)=(2m,\; m-1,\; B-1,\; 2m-1,\; m)_B.$$

Its sorted digit multiset is

$$\{m-1,\; m,\; 2m-1,\; 2m,\; B-1\},$$

so the outer differences remain

$$u=(B-1)-(m-1)=2m,\qquad v=(2m)-m=m,$$

hence

$$T_B(2m,m)=(2m,m).$$

This is the Kaprekar constant state. For example, when \(B=15\) we have \(m=5\), so the constant is

$$C_{15}=(10,4,14,9,5)_{15},$$

and when \(B=21\) we get

$$C_{21}=(14,6,20,13,7)_{21}.$$

Step 3: Count How Many Inputs Share One Gap State

Let \(M_B(u,v)\) be the number of 5-digit base-\(B\) strings whose sorted digits have outer gaps \((u,v)\). Fix \(u \gt 0\), let the smallest digit be \(a\), and let the largest digit be \(e=a+u\). There are exactly

$$B-u$$

possible values of \(a\). For each such \(a\), the count depends only on the equality pattern of the middle digits. The constants \(20,30,60,120\) below are multinomial counts such as \(5!/3!\), \(5!/(2!2!)\), \(5!/2!\), and \(5!\).

Case \(v=0\). Then the second and fourth sorted digits are equal, so the multiset has the form

$$[a,c,c,c,e].$$

If \(c=a\) or \(c=e\), there are \(5\) permutations in each case. If \(a \lt c \lt e\), there are \(u-1\) choices for \(c\), each with \(20\) permutations. Therefore

$$M_B(u,0)=(B-u)\bigl(5+5+20(u-1)\bigr),\qquad u \ge 1.$$

Case \(u=v\). Now the second digit must equal the minimum and the fourth digit must equal the maximum, so the multiset is

$$[a,a,c,e,e].$$

If \(c=a\) or \(c=e\), there are \(10\) permutations in each case. If \(a \lt c \lt e\), there are \(u-1\) choices and \(30\) permutations each. Hence

$$M_B(u,u)=(B-u)\bigl(10+10+30(u-1)\bigr),\qquad u \ge 1.$$

Case \(0 \lt v \lt u\). There are two boundary families where one inner digit collapses onto an endpoint:

$$[a,a,c,d,e],\quad [a,a,d,d,e],\quad [a,a,a,d,e],$$

and symmetrically

$$[a,b,c,e,e],\quad [a,b,b,e,e],\quad [a,b,e,e,e].$$

These contribute

$$2\bigl(60(v-1)+30+20\bigr).$$

If both inner gaps are strict, choose \(b\) with \(a \lt b \lt e-v\), so there are \(u-v-1\) possibilities. Then \(d=b+v\), and the remaining multisets are

$$[a,b,b,d,e],\qquad [a,b,d,d,e],\qquad [a,b,c,d,e],$$

where in the last form the middle digit satisfies \(b \lt c \lt d\), giving \(v-1\) choices. Their total contribution is

$$60(u-v-1)+60(u-v-1)+120(u-v-1)(v-1).$$

Combining everything,

$$M_B(u,v)=(B-u)\Bigl(100+120(v-1)+120(u-v-1)+120(u-v-1)(v-1)\Bigr),$$

for \(0 \lt v \lt u\).

Step 4: Depth on the Functional Graph

Every state \((u,v)\) has exactly one outgoing edge, namely \(T_B(u,v)\). So the gap states form a functional graph. Let \(D_B(u,v)\) be the number of additional Kaprekar steps needed after the first subtraction has produced the state \((u,v)\). Then

$$D_B(u,v)= \begin{cases} 0,& T_B(u,v)=(u,v),\\ 1 + D_B\!\bigl(T_B(u,v)\bigr),& T_B(u,v)\ne(u,v). \end{cases}$$

The implementation memoizes this recurrence, so each state depth is computed only once.

Step 5: Assemble \(S(B)\)

Among the \(B^5-1\) positive 5-digit strings, exactly \(B-1\) have all digits equal, and the Kaprekar constant itself also contributes \(0\). Every other starting value contributes one unavoidable first iteration before the gap-state depth takes over. Therefore the universal first-step contribution is

$$B^5-1-(B-1)-1=B^5-B-1.$$

After that first step, only the gap state matters, so

$$\boxed{S(B)=B^5-B-1+\sum_{u=1}^{B-1}\sum_{v=0}^{u} M_B(u,v)\,D_B(u,v).}$$

This is exactly the quantity accumulated by the implementation. The published checkpoints

$$S(15)=5274369,\qquad S(111)=400668930299$$

are recovered by this formula.

How the Code Works

The C++, Python, and Java implementations all use the same mathematical reduction. For each base \(B\), they enumerate the triangular set of gap states \((u,v)\), compute the deterministic successor by evaluating \(R_B(u,v)\) and sorting its five base-\(B\) digits, memoize the depth to the fixed state, multiply that depth by the corresponding multiplicity \(M_B(u,v)\), and finally add the universal offset \(B^5-B-1\). All arithmetic is reduced modulo \(10^{18}\) because only the last \(18\) digits are required.

Complexity Analysis

For a fixed base \(B\), the number of states is \(O(B^2)\). Each transition uses only constant-time arithmetic plus a sort of five digits, which is also constant. Memoization ensures that each state depth is evaluated once, so the total work per base is \(O(B^2)\) time and \(O(B^2)\) memory. This is exponentially smaller than brute-forcing all \(B^5\) starting values.

Footnotes and References

  1. Problem page: https://projecteuler.net/problem=414
  2. Kaprekar routine: Wikipedia — Kaprekar's routine
  3. Functional graph: Wikipedia — Functional graph
  4. Multinomial coefficient: Wikipedia — Multinomial theorem

Problem 414 source code

C++

#include <algorithm>
#include <array>
#include <cstdint>
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <vector>

namespace {

using i64 = long long;
using u64 = std::uint64_t;
using u128 = __uint128_t;

constexpr u64 MOD = 1000000000000000000ULL;  // last 18 digits

struct Options {
    int k_from = 2;
    int k_to = 300;
    bool run_checkpoints = true;
};

bool parse_int_after_prefix(const std::string& arg, const std::string& prefix, int& value) {
    if (arg.rfind(prefix, 0U) != 0U) {
        return false;
    }
    const std::string tail = arg.substr(prefix.size());
    if (tail.empty()) {
        return false;
    }
    try {
        value = std::stoi(tail);
    } catch (...) {
        return false;
    }
    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_int_after_prefix(arg, "--k-from=", options.k_from) ||
            parse_int_after_prefix(arg, "--k-to=", options.k_to)) {
            continue;
        }
        std::cerr << "Unknown argument: " << arg << '\n';
        return false;
    }
    return options.k_from >= 2 && options.k_to >= options.k_from;
}

u64 pow_u64(u64 base, int exp) {
    u64 r = 1;
    for (int i = 0; i < exp; ++i) {
        r *= base;
    }
    return r;
}

std::pair<int, int> next_state(int p, int q, int b) {
    const u64 b2 = static_cast<u64>(b) * static_cast<u64>(b);
    const u64 b4 = b2 * b2;
    u64 x = static_cast<u64>(p) * (b4 - 1ULL) +
            static_cast<u64>(q) * (b2 - 1ULL) * static_cast<u64>(b);

    std::array<int, 5> d{};
    for (int i = 0; i < 5; ++i) {
        d[static_cast<std::size_t>(i)] = static_cast<int>(x % static_cast<u64>(b));
        x /= static_cast<u64>(b);
    }
    std::sort(d.begin(), d.end());
    return {d[4] - d[0], d[3] - d[1]};
}

i64 ways(int p, int q, int b) {
    i64 t = 0;
    if (q == 0) {
        if (p == 0) {
            t = 1;                        // aaaaa
        } else {
            t += 120 / 24 * 2;            // aaaae, aeeee
            t += 120 / 6 * (p - 1);       // accce
        }
    } else if (p == q) {
        t += 120 / 2 / 2 * (p - 1);       // aacee
        t += 120 / 6 / 2 * 2;             // aaaee, aaeee
    } else {
        t += 120 / 2 * (q - 1);           // abcee
        t += 120 / 2 / 2;                 // abbee
        t += 120 / 6;                     // abeee
        t *= 2;

        if (p - 2 >= q) {
            t += 120 / 2 * (p - 1 - q) * 2;          // abbde, abdde
            t += 120 * (p - 1 - q) * (q - 1);        // abcde
        }
    }
    return static_cast<i64>(b - p) * t;
}

int dfs_depth(int p, int q, int b, std::vector<std::vector<int>>& memo) {
    int& cell = memo[static_cast<std::size_t>(p)][static_cast<std::size_t>(q)];
    if (cell != -1) {
        return cell;
    }
    const auto nxt = next_state(p, q, b);
    if (nxt.first == p && nxt.second == q) {
        cell = 0;
    } else {
        cell = dfs_depth(nxt.first, nxt.second, b, memo) + 1;
    }
    return cell;
}

u64 S_of_base(int b) {
    const u64 total = pow_u64(static_cast<u64>(b), 5);

    std::vector<std::vector<int>> memo(
        static_cast<std::size_t>(b),
        std::vector<int>(static_cast<std::size_t>(b), -1));

    u64 ans = 0ULL;
    for (int p = 1; p <= b - 1; ++p) {
        for (int q = 0; q <= p; ++q) {
            const i64 w = ways(p, q, b);
            const int d = dfs_depth(p, q, b, memo);
            ans = (ans + static_cast<u64>((u128)(w % static_cast<i64>(MOD)) * d % MOD)) % MOD;
        }
    }

    // +1 first-step contribution for all non-special numbers:
    // total numbers minus {0}, minus all-equal positive numbers (b-1), minus Kaprekar constant.
    ans = (ans + total - static_cast<u64>(b) - 1ULL) % MOD;
    return ans;
}

bool run_checkpoints() {
    if (S_of_base(15) != 5274369ULL) {
        std::cerr << "Checkpoint failed: S(15)\n";
        return false;
    }
    if (S_of_base(111) != 400668930299ULL) {
        std::cerr << "Checkpoint failed: S(111)\n";
        return false;
    }
    return true;
}

u64 solve_sum(int k_from, int k_to) {
    u64 ans = 0ULL;
    for (int k = k_from; k <= k_to; ++k) {
        const int b = 6 * k + 3;
        ans = (ans + S_of_base(b)) % MOD;
    }
    return ans;
}

struct SumWorkerArgs {
    int tid = 0;
    int thread_count = 1;
    int k_from = 2;
    int k_to = 300;
    u64 partial = 0ULL;
};

void* solve_sum_worker(void* raw) {
    auto* args = static_cast<SumWorkerArgs*>(raw);
    u64 local = 0ULL;
    for (int k = args->k_from + args->tid; k <= args->k_to; k += args->thread_count) {
        const int b = 6 * k + 3;
        local = (local + S_of_base(b)) % MOD;
    }
    args->partial = local;
    return nullptr;
}

u64 solve_sum_parallel(int k_from, int k_to) {
    const int total_k = k_to - k_from + 1;
    if (total_k <= 1) {
        return solve_sum(k_from, k_to);
    }

    long hw = sysconf(_SC_NPROCESSORS_ONLN);
    if (hw <= 1) {
        return solve_sum(k_from, k_to);
    }

    const int thread_count = std::min<int>(total_k, static_cast<int>(hw));
    std::vector<pthread_t> threads(static_cast<std::size_t>(thread_count));
    std::vector<SumWorkerArgs> args(static_cast<std::size_t>(thread_count));

    for (int t = 0; t < thread_count; ++t) {
        args[static_cast<std::size_t>(t)].tid = t;
        args[static_cast<std::size_t>(t)].thread_count = thread_count;
        args[static_cast<std::size_t>(t)].k_from = k_from;
        args[static_cast<std::size_t>(t)].k_to = k_to;
        args[static_cast<std::size_t>(t)].partial = 0ULL;
    }

    bool create_failed = false;
    int started = 0;
    for (int t = 0; t < thread_count; ++t) {
        if (pthread_create(&threads[static_cast<std::size_t>(t)], nullptr,
                           solve_sum_worker, &args[static_cast<std::size_t>(t)]) != 0) {
            create_failed = true;
            break;
        }
        ++started;
    }

    for (int t = 0; t < started; ++t) {
        pthread_join(threads[static_cast<std::size_t>(t)], nullptr);
    }

    if (create_failed) {
        return solve_sum(k_from, k_to);
    }

    u64 ans = 0ULL;
    for (const auto& a : args) {
        ans = (ans + a.partial) % MOD;
    }
    return ans;
}

}  // 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;
    }

    const u64 answer = solve_sum_parallel(options.k_from, options.k_to);
    std::cout << answer << '\n';
    return 0;
}

Python

from __future__ import annotations

import re
import shutil
import subprocess
from pathlib import Path

ANSWER_RE = re.compile(r"answer\s*:\s*(.+)$", re.IGNORECASE)
EQUAL_RE = re.compile(r"=\s*(.+)$")


def parse_output(stdout: str) -> str:
    lines = [line.strip() for line in stdout.splitlines() if line.strip()]
    if not lines:
        return ""
    answers = []
    equals = []
    for line in lines:
        m1 = ANSWER_RE.search(line)
        if m1:
            answers.append(m1.group(1).strip())
        m2 = EQUAL_RE.search(line)
        if m2:
            equals.append(m2.group(1).strip())
    if answers:
        return answers[-1]
    if equals:
        return equals[-1]
    return lines[-1]


def should_skip_cpp_checkpoints(src: Path) -> bool:
    try:
        text = src.read_text(encoding="utf-8", errors="ignore")
    except OSError:
        return False
    return "--skip-checkpoints" in text


def run_cpp(binary: Path, src: Path, root: Path) -> str:
    cmd = [str(binary)]
    if should_skip_cpp_checkpoints(src):
        cmd.append("--skip-checkpoints")

    try:
        return subprocess.check_output(cmd, text=True, cwd=root)
    except subprocess.CalledProcessError:
        return subprocess.check_output(cmd, text=True, cwd=src.parent)


def solve() -> str:
    problem_id = __file__.split("Euler")[-1].split(".")[0]
    root = Path(__file__).resolve().parent.parent
    src = root / "solutionsCpp" / f"Euler{problem_id}.cpp"
    binary = root / "solutionsCpp" / f".euler{problem_id}_py_bridge"

    if not binary.exists() or src.stat().st_mtime > binary.stat().st_mtime:
        compiler = shutil.which("clang++") or shutil.which("g++")
        if not compiler:
            raise RuntimeError("No C++ compiler found (clang++/g++).")
        subprocess.check_call([compiler, "-std=c++17", "-O2", str(src), "-o", str(binary)])

    output = run_cpp(binary=binary, src=src, root=root)
    parsed = parse_output(output)
    if not parsed:
        raise RuntimeError(f"Euler{problem_id} bridge produced empty output.")
    return parsed


if __name__ == "__main__":
    print(solve())

Java

import java.util.Arrays;
import java.util.stream.IntStream;

public class Euler414 {
    static final long MOD = 1000000000000000000L;

    static long powU64(long base, int exp) {
        long r = 1;
        for (int i = 0; i < exp; ++i) {
            r *= base;
        }
        return r;
    }

    static int[] nextState(int p, int q, int b) {
        long b2 = (long) b * b;
        long b4 = b2 * b2;
        long x = (long) p * (b4 - 1L) + (long) q * (b2 - 1L) * (long) b;

        int[] d = new int[5];
        for (int i = 0; i < 5; ++i) {
            d[i] = (int) (x % b);
            x /= b;
        }
        Arrays.sort(d);
        return new int[] { d[4] - d[0], d[3] - d[1] };
    }

    static long ways(int p, int q, int b) {
        long t = 0;
        if (q == 0) {
            if (p == 0) {
                t = 1;
            } else {
                t += 120 / 24 * 2;
                t += 120 / 6 * (p - 1);
            }
        } else if (p == q) {
            t += 120 / 2 / 2 * (p - 1);
            t += 120 / 6 / 2 * 2;
        } else {
            t += 120 / 2 * (q - 1);
            t += 120 / 2 / 2;
            t += 120 / 6;
            t *= 2;

            if (p - 2 >= q) {
                t += 120 / 2 * (p - 1 - q) * 2;
                t += 120 * (p - 1 - q) * (q - 1);
            }
        }
        return (long) (b - p) * t;
    }

    static int dfsDepth(int p, int q, int b, int[][] memo) {
        if (memo[p][q] != -1) {
            return memo[p][q];
        }
        int[] nxt = nextState(p, q, b);
        if (nxt[0] == p && nxt[1] == q) {
            memo[p][q] = 0;
        } else {
            memo[p][q] = dfsDepth(nxt[0], nxt[1], b, memo) + 1;
        }
        return memo[p][q];
    }

    static long sOfBase(int b) {
        long total = powU64(b, 5);
        int[][] memo = new int[b][b];
        for (int i = 0; i < b; i++) {
            Arrays.fill(memo[i], -1);
        }

        long ans = 0;
        for (int p = 1; p < b; ++p) {
            for (int q = 0; q <= p; ++q) {
                long w = ways(p, q, b);
                int d = dfsDepth(p, q, b, memo);

                long wMod = w % MOD;
                long term = 0;
                for (int i = 0; i < d; i++) {
                    term += wMod;
                    if (term >= MOD)
                        term -= MOD;
                }

                ans += term;
                if (ans >= MOD)
                    ans -= MOD;
            }
        }

        long contribution = (total - b - 1) % MOD;
        if (contribution < 0)
            contribution += MOD;
        ans = (ans + contribution) % MOD;
        return ans;
    }

    static String solve() {
        int kFrom = 2;
        int kTo = 300;

        long ans = IntStream.rangeClosed(kFrom, kTo)
                .parallel()
                .mapToLong(k -> {
                    int b = 6 * k + 3;
                    return sOfBase(b);
                })
                .reduce(0, (a, b) -> (a + b) % MOD);

        return Long.toString(ans);
    }

    public static void main(String[] args) {
        System.out.println(solve());
    }
}