Skip to content

Commit 48d95d5

Browse files
Add alphametics exercise (#183)
1 parent 25270a8 commit 48d95d5

15 files changed

Lines changed: 615 additions & 0 deletions

File tree

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,14 @@
722722
"prerequisites": [],
723723
"difficulty": 8
724724
},
725+
{
726+
"slug": "alphametics",
727+
"name": "Alphametics",
728+
"uuid": "ac086674-808b-4f36-be70-725174f1caff",
729+
"practices": [],
730+
"prerequisites": [],
731+
"difficulty": 8
732+
},
725733
{
726734
"slug": "bank-account",
727735
"name": "Bank Account",
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Instructions
2+
3+
Given an alphametics puzzle, find the correct solution.
4+
5+
[Alphametics][alphametics] is a puzzle where letters in words are replaced with numbers.
6+
7+
For example `SEND + MORE = MONEY`:
8+
9+
```text
10+
S E N D
11+
M O R E +
12+
-----------
13+
M O N E Y
14+
```
15+
16+
Replacing these with valid numbers gives:
17+
18+
```text
19+
9 5 6 7
20+
1 0 8 5 +
21+
-----------
22+
1 0 6 5 2
23+
```
24+
25+
This is correct because every letter is replaced by a different number and the words, translated into numbers, then make a valid sum.
26+
27+
Each letter must represent a different digit, and the leading digit of a multi-digit number must not be zero.
28+
29+
[alphametics]: https://en.wikipedia.org/wiki/Alphametics
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
namespace Alphametics
2+
3+
private structure Letter where
4+
ch : Char
5+
leading : Nat
6+
rank : Nat
7+
weight : Array Int
8+
9+
private def wordSigns (tokens : List String) : List (String × Int) := Id.run do
10+
let mut sign : Int := -1
11+
let mut result : List (String × Int) := []
12+
for tok in tokens do
13+
if tok == "==" then
14+
sign := 1
15+
else if tok != "+" then
16+
result := (tok, sign) :: result
17+
return result
18+
19+
private def uniqueLetters (puzzle : String) : List Char :=
20+
puzzle.toList.filter Char.isAlpha
21+
|>.foldl (fun acc c => if acc.contains c then acc else c :: acc) []
22+
23+
private def makeLetter (words : List (String × Int)) (numCols : Nat) (ch : Char) : Letter := Id.run do
24+
let mut weight : Array Int := Array.replicate numCols 0
25+
let mut leading : Nat := 0
26+
for (word, sign) in words do
27+
let chars := word.toList.toArray
28+
let len := chars.size
29+
if len > 1 && chars[0]! == ch then
30+
leading := 1
31+
for i in [:len] do
32+
if chars[len - 1 - i]! == ch then
33+
weight := weight.set! i (weight[i]! + sign)
34+
let rank :=
35+
(List.range numCols).find? (fun col => weight[col]! != 0)
36+
|>.getD (numCols - 1)
37+
return { ch, leading, rank, weight }
38+
39+
private def columnSum (letters : List Letter) (mapping : List (Char × Nat)) (col : Nat) : Int :=
40+
letters.foldl (fun acc l =>
41+
acc + l.weight[col]! * (Int.ofNat (mapping.lookup l.ch |>.getD 0))) 0
42+
43+
-- Depth-first search. `remaining` holds letters not yet assigned,
44+
-- sorted by rank ascending; `col` is the current column (0 = rightmost).
45+
-- When the next letter's rank exceeds `col`, validate the column sum
46+
-- (accounting for `carry`) and advance to the next column.
47+
private partial def search (numCols : Nat) (allLetters : List Letter)
48+
(col : Nat) (carry : Int) (remaining : List Letter) (claimed : Nat)
49+
(mapping : List (Char × Nat)) : Option (List (Char × Nat)) :=
50+
let columnComplete : Bool :=
51+
match remaining with
52+
| [] => true
53+
| entry :: _ => entry.rank > col
54+
if columnComplete then
55+
let colSum := carry + columnSum allLetters mapping col
56+
if colSum % 10 != 0 then
57+
none
58+
else if col + 1 < numCols then
59+
search numCols allLetters (col + 1) (colSum / 10) remaining claimed mapping
60+
else if colSum == 0 then
61+
some (mapping.mergeSort (fun a b => a.1 ≤ b.1))
62+
else
63+
none
64+
else
65+
match remaining with
66+
| [] => none
67+
| entry :: rest =>
68+
((List.range 10).filter (· ≥ entry.leading)).findSome? (fun digit =>
69+
if claimed &&& (1 <<< digit) == 0 then
70+
search numCols allLetters col carry rest
71+
(claimed ||| (1 <<< digit)) ((entry.ch, digit) :: mapping)
72+
else
73+
none)
74+
75+
def solve (puzzle : String) : Option (List (Char × Nat)) :=
76+
let words := wordSigns ((puzzle.splitOn " ").filter (· != ""))
77+
let numCols := (words.map (fun (w, _) => w.length)).foldl max 0
78+
let uniq := uniqueLetters puzzle
79+
let allLetters := (uniq.map (makeLetter words numCols)).mergeSort (fun a b => a.rank ≤ b.rank)
80+
search numCols allLetters 0 0 allLetters 0 []
81+
82+
end Alphametics
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"authors": [
3+
"keiravillekode"
4+
],
5+
"files": {
6+
"solution": [
7+
"Alphametics.lean"
8+
],
9+
"test": [
10+
"AlphameticsTest.lean"
11+
],
12+
"example": [
13+
".meta/Example.lean"
14+
]
15+
},
16+
"blurb": "Given an alphametics puzzle, find the correct solution."
17+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[
2+
{
3+
"description": "puzzle with a colors theme",
4+
"property": "solve",
5+
"input": {
6+
"puzzle": "GREEN + ORANGE == COLORS"
7+
},
8+
"expected": {
9+
"A": 5,
10+
"C": 2,
11+
"E": 4,
12+
"G": 8,
13+
"L": 9,
14+
"N": 6,
15+
"O": 1,
16+
"R": 3,
17+
"S": 0
18+
}
19+
}
20+
]
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
12+
[e0c08b07-9028-4d5f-91e1-d178fead8e1a]
13+
description = "puzzle with three letters"
14+
15+
[a504ee41-cb92-4ec2-9f11-c37e95ab3f25]
16+
description = "solution must have unique value for each letter"
17+
18+
[4e3b81d2-be7b-4c5c-9a80-cd72bc6d465a]
19+
description = "leading zero solution is invalid"
20+
21+
[8a3e3168-d1ee-4df7-94c7-b9c54845ac3a]
22+
description = "puzzle with two digits final carry"
23+
24+
[a9630645-15bd-48b6-a61e-d85c4021cc09]
25+
description = "puzzle with four letters"
26+
27+
[3d905a86-5a52-4e4e-bf80-8951535791bd]
28+
description = "puzzle with six letters"
29+
30+
[4febca56-e7b7-4789-97b9-530d09ba95f0]
31+
description = "puzzle with seven letters"
32+
33+
[12125a75-7284-4f9a-a5fa-191471e0d44f]
34+
description = "puzzle with eight letters"
35+
36+
[fb05955f-38dc-477a-a0b6-5ef78969fffa]
37+
description = "puzzle with ten letters"
38+
39+
[9a101e81-9216-472b-b458-b513a7adacf7]
40+
description = "puzzle with ten letters and 199 addends"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Alphametics
2+
3+
def solve (puzzle : String) : Option (List (Char × Nat)) :=
4+
sorry --remove this line and implement the function
5+
6+
end Alphametics
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import LeanTest
2+
import Alphametics
3+
4+
open LeanTest
5+
6+
def alphameticsTests : TestSuite :=
7+
(TestSuite.empty "Alphametics")
8+
|>.addTest "puzzle with three letters" (do
9+
return assertEqual (some [('B', 9), ('I', 1), ('L', 0)])
10+
(Alphametics.solve "I + BB == ILL"))
11+
|>.addTest "solution must have unique value for each letter" (do
12+
return assertEqual none
13+
(Alphametics.solve "A == B"))
14+
|>.addTest "leading zero solution is invalid" (do
15+
return assertEqual none
16+
(Alphametics.solve "ACA + DD == BD"))
17+
|>.addTest "puzzle with two digits final carry" (do
18+
return assertEqual (some [('A', 9), ('B', 1), ('C', 0)])
19+
(Alphametics.solve "A + A + A + A + A + A + A + A + A + A + A + B == BCC"))
20+
|>.addTest "puzzle with four letters" (do
21+
return assertEqual (some [('A', 9), ('M', 1), ('O', 0), ('S', 2)])
22+
(Alphametics.solve "AS + A == MOM"))
23+
|>.addTest "puzzle with six letters" (do
24+
return assertEqual (some [('A', 0), ('E', 2), ('L', 1), ('N', 7), ('O', 4), ('T', 9)])
25+
(Alphametics.solve "NO + NO + TOO == LATE"))
26+
|>.addTest "puzzle with seven letters" (do
27+
return assertEqual (some [('E', 4), ('G', 2), ('H', 5), ('I', 0), ('L', 1), ('S', 9), ('T', 7)])
28+
(Alphametics.solve "HE + SEES + THE == LIGHT"))
29+
|>.addTest "puzzle with eight letters" (do
30+
return assertEqual (some [('D', 7), ('E', 5), ('M', 1), ('N', 6), ('O', 0), ('R', 8), ('S', 9), ('Y', 2)])
31+
(Alphametics.solve "SEND + MORE == MONEY"))
32+
|>.addTest "puzzle with ten letters" (do
33+
return assertEqual (some [('A', 5), ('D', 3), ('E', 4), ('F', 7), ('G', 8), ('N', 0), ('O', 2), ('R', 1), ('S', 6), ('T', 9)])
34+
(Alphametics.solve "AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE"))
35+
|>.addTest "puzzle with ten letters and 199 addends" (do
36+
return assertEqual (some [('A', 1), ('E', 0), ('F', 5), ('H', 8), ('I', 7), ('L', 2), ('O', 6), ('R', 3), ('S', 4), ('T', 9)])
37+
(Alphametics.solve "THIS + A + FIRE + THEREFORE + FOR + ALL + HISTORIES + I + TELL + A + TALE + THAT + FALSIFIES + ITS + TITLE + TIS + A + LIE + THE + TALE + OF + THE + LAST + FIRE + HORSES + LATE + AFTER + THE + FIRST + FATHERS + FORESEE + THE + HORRORS + THE + LAST + FREE + TROLL + TERRIFIES + THE + HORSES + OF + FIRE + THE + TROLL + RESTS + AT + THE + HOLE + OF + LOSSES + IT + IS + THERE + THAT + SHE + STORES + ROLES + OF + LEATHERS + AFTER + SHE + SATISFIES + HER + HATE + OFF + THOSE + FEARS + A + TASTE + RISES + AS + SHE + HEARS + THE + LEAST + FAR + HORSE + THOSE + FAST + HORSES + THAT + FIRST + HEAR + THE + TROLL + FLEE + OFF + TO + THE + FOREST + THE + HORSES + THAT + ALERTS + RAISE + THE + STARES + OF + THE + OTHERS + AS + THE + TROLL + ASSAILS + AT + THE + TOTAL + SHIFT + HER + TEETH + TEAR + HOOF + OFF + TORSO + AS + THE + LAST + HORSE + FORFEITS + ITS + LIFE + THE + FIRST + FATHERS + HEAR + OF + THE + HORRORS + THEIR + FEARS + THAT + THE + FIRES + FOR + THEIR + FEASTS + ARREST + AS + THE + FIRST + FATHERS + RESETTLE + THE + LAST + OF + THE + FIRE + HORSES + THE + LAST + TROLL + HARASSES + THE + FOREST + HEART + FREE + AT + LAST + OF + THE + LAST + TROLL + ALL + OFFER + THEIR + FIRE + HEAT + TO + THE + ASSISTERS + FAR + OFF + THE + TROLL + FASTS + ITS + LIFE + SHORTER + AS + STARS + RISE + THE + HORSES + REST + SAFE + AFTER + ALL + SHARE + HOT + FISH + AS + THEIR + AFFILIATES + TAILOR + A + ROOFS + FOR + THEIR + SAFE == FORTRESSES"))
38+
|>.addTest "puzzle with a colors theme" (do
39+
return assertEqual (some [('A', 5), ('C', 2), ('E', 4), ('G', 8), ('L', 9), ('N', 6), ('O', 1), ('R', 3), ('S', 0)])
40+
(Alphametics.solve "GREEN + ORANGE == COLORS"))
41+
42+
def main : IO UInt32 := do
43+
runTestSuitesWithExitCode [alphameticsTests]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name = "alphametics"
2+
version = "0.1.0"
3+
defaultTargets = ["AlphameticsTest"]
4+
testDriver = "AlphameticsTest"
5+
6+
[[lean_lib]]
7+
name = "LeanTest"
8+
srcDir = "vendor/LeanTest"
9+
10+
[[lean_lib]]
11+
name = "Alphametics"
12+
13+
[[lean_exe]]
14+
name = "AlphameticsTest"
15+
root = "AlphameticsTest"
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
leanprover/lean4:v4.29.0

0 commit comments

Comments
 (0)