Kaprekar’s 6174

A number trick that always lands on 6174 — checked for every case, with one honest caveat.

kaprekar.py · output · proof · check · try it in the playground


The question

Take a four-digit number whose digits are not all the same. Arrange its digits from high to low and from low to high, and subtract the smaller number from the larger. Repeat with the result.

3524  ->  5432 - 2345 = 3087
3087  ->  8730 - 0378 = 8352
8352  ->  8532 - 2358 = 6174

Once you reach 6174 you stay there: 7641 − 1467 = 6174. The mathematician D. R. Kaprekar noticed that every starting number ends up at 6174.

Does every such number get there within seven steps?


A shortcut that makes it checkable

There are 10,000 four-digit strings (0000 to 9999, leading zeros allowed), but the first step only cares about which digits occur, not their order: 3524 and 2453 give the same result.

So it is enough to check each multiset of four digits (a bag of digits, order ignored) once. Leaving out the ten where all digits are equal (0000, 1111, …), there are 705 of them.


What we tell eyekind

One step of the routine, written as a backward rule:

# One step: the largest number the digits make minus the smallest.
implied_by(
  kaprekar_step(A, B),
  [
    digits(A, Ds),
    sort4(Ds, Asc),
    reverse(Asc, Desc),
    number_of(Asc, Low),
    number_of(Desc, High),
    is_(B, sub(High, Low)),
  ],
)

implied_by(conclusion, conditions) is a backward rule: the conclusion holds when all the conditions in the list hold, and eyekind uses it only when it needs it. Capitalized names are variables, placeholders for values. Read it as: one step takes A to B if A’s digits, sorted low to high, make the number Low, sorted high to low make High, and B is High − Low. Other rules, not shown, split a number into its four digits, sort them, turn digits back into a number, and list one representative number per multiset.

And the claim to test:

# reaches_6174(A, Steps): A was reached after Steps steps, and 6174 follows
# before the total number of steps exceeds seven.
fact(reaches_6174(6174, _))
implied_by(
  reaches_6174(A, Steps),
  [
    ne(A, 6174),
    lt(Steps, 7),
    kaprekar_step(A, B),
    is_(Next, add(Steps, 1)),
    reaches_6174(B, Next),
  ],
)

# The claim holds when no multiset is a counterexample; not(...) checks
# every one.
implied_by('counterexample', [digit_multiset(A), not_(reaches_6174(A, 0))])
implied_by(kaprekar_verified(6174, 7), not_('counterexample'))

not_(...) means “cannot be shown”. To decide it, eyekind tries to show the thing inside and concludes the opposite if it fails. That works here because the search is finite: 705 multisets, at most seven steps each.


What eyekind concludes

// Answers: each instance of a query() that holds.
kaprekar_verified(6174, 7)

The comment line says what follows: the answers to the program’s question. eyekind searched all 705 multisets and found no counterexample, so the claim holds. --stats reports 152,940 inferences for that search.


How “there is no counterexample” ends up in a proof

A proof normally shows how something was found: this rule, these facts, this calculation. But “there is no counterexample” is about something that was not found. There is nothing to point at.

eyekind records such a conclusion as an absence: a step marked 'absent'. It says: “I looked for this, the search finished, and it was not there.” The checker cannot redo that search from the proof alone, so it accepts the step on trust and says so openly, as an obligation: a statement that the reader must take on eyekind’s word, listed by name.


The proof, line by line

This is the complete proof:

// Answers: each instance of a query() that holds.
kaprekar_verified(6174, 7)

// Clauses: the program clauses the steps cite, by clause number.
clause(19, implied_by(kaprekar_verified(6174, 7), not('counterexample')))

// Steps: each goal, what justifies it (a clause, a builtin, a control, or an
// absence or collection taken on trust), the bindings and the goals it uses.
step(kaprekar_verified(6174, 7), clause(19), {}, [not('counterexample')])
step(not('counterexample'), 'absent', {}, [])

It has three parts:

Read the steps as sentences:

  1. Kaprekar’s claim is verified — by clause 19, with no variables to fill in, because there is no counterexample.
  2. There is no counterexample — 'absent': eyekind searched for one and found none. It uses nothing.

The whole exhaustive search sits inside that second step: all 705 multisets and every Kaprekar step they take. A proof can list what was found; it cannot list, one by one, all the ways something failed to be found.


Checked: what the report says

eyekind -c generates the proof and checks it against the program; --check-proof checks a saved proof. The checker never reasons by itself: it only confirms each step against the program. This is the complete report:

// Answers checked subject to the obligations below.
kaprekar_verified(6174, 7)

// C1-C8: resolution, acyclicity, justification, coverage,
// re-decision, boundary consistency, relevance and witness integrity.
condition('C1', 'resolution', 'ok', 1)
condition('C2', 'well_founded', 'ok', 2)
condition('C3', 'justification', 'ok', 2)
condition('C4', 'coverage', 'ok', 2)
condition('C5', 're_decision', 'ok', 0)
condition('C6', 'boundary_consistency', 'ok', 0)
condition('C7', 'relevance', 'ok', 3)
condition('C8', 'witness_integrity', 'ok', 0)

// Obligations: absence and collection completeness are taken on trust.
obligation('absent', 'theory_scoped', not('counterexample'))

// Proof statistics: recorded steps and how they were checked.
steps(2)
verified(1)
recomputed(0)
composed(0)
trusted(1)
claims(1)

// This verdict applies to the claims above; it does not establish completeness.
verdict('checked_with_obligations')

Each condition line names a check, says 'ok', and counts what it looked at:

The obligation line, in plain words: “there is no counterexample” was taken on trust; it is true relative to this program ('theory_scoped'), that is, as far as these rules and facts can show. The statistics say the same: 2 steps, 1 verified against the program, 1 trusted.

The verdict is checked with obligations: the claim follows from the program, provided you accept the one listed absence. It does not establish completeness, and it does not re-run the search.


Why the caveat is honest, not a flaw

The claim is a claim about absence: no number fails. Any checker would have to either redo the whole search or take it on trust.

eyekind chooses to say so in writing. If you need a proof with no trusted steps, --strict-proof rejects this one; for most uses, knowing exactly where the trust sits is what matters.


Try it

eyekind examples/kaprekar.py            # the answers
eyekind --proof examples/kaprekar.py    # answers with their proof
eyekind --goal "kaprekar_step(3524, B)" examples/kaprekar.py

Or open it in the playground. The --goal line asks for one step of the routine: it answers kaprekar_step(3524, 3087), the first line of the example above.


Takeaway

eyekind checked a famous number fact case by case, and its certificate says plainly which part rests on “we searched and found nothing” — so you know exactly what you are trusting.