Problem 414: Kaprekar Constant
View on Project EulerProject 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
- Problem page: https://projecteuler.net/problem=414
- Kaprekar routine: Wikipedia — Kaprekar's routine
- Functional graph: Wikipedia — Functional graph
- 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());
}
}