Skip to content

Commit 029d879

Browse files
Add alphametics exercise (#378)
1 parent 57f9a28 commit 029d879

8 files changed

Lines changed: 420 additions & 0 deletions

File tree

config.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -817,6 +817,14 @@
817817
"prerequisites": [],
818818
"difficulty": 7
819819
},
820+
{
821+
"slug": "alphametics",
822+
"name": "Alphametics",
823+
"uuid": "fb9e5c15-2480-4267-8235-6ad5ac5bfdd8",
824+
"practices": [],
825+
"prerequisites": [],
826+
"difficulty": 8
827+
},
820828
{
821829
"slug": "circular-buffer",
822830
"name": "Circular Buffer",
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: 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.sml"
8+
],
9+
"test": [
10+
"test.sml"
11+
],
12+
"example": [
13+
".meta/example.sml"
14+
]
15+
},
16+
"blurb": "Given an alphametics puzzle, find the correct solution."
17+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
fun solve (puzzle: string): string =
2+
let
3+
(* Tokenize: words and "=" retained; "+" and spaces are separators *)
4+
val tokens = String.tokens (fn c => c = #" " orelse c = #"+") puzzle
5+
6+
val nColumns = foldl (fn (t, m) => Int.max (String.size t, m)) 0 tokens
7+
8+
fun isWord token = Char.isAlpha (String.sub (token, 0))
9+
10+
(* Letter info: weight vector has per-column coefficients, rank is rightmost column *)
11+
type info = {letter: char, leading: int, weight: int vector, rank: int}
12+
13+
(* Build info for a given letter by scanning all tokens *)
14+
fun letterInfo (ch: char): info option =
15+
let
16+
(* Is ch the leading letter of a non-trivial word? *)
17+
val leading =
18+
if List.exists (fn t =>
19+
isWord t andalso String.size t > 1 andalso String.sub (t, 0) = ch) tokens
20+
then 1 else 0
21+
22+
(* Per-column weight coefficients *)
23+
fun weightAt col =
24+
let
25+
fun processToken (token, (sign, sum)) =
26+
if not (isWord token) then (~sign, sum)
27+
else
28+
let val len = String.size token
29+
in
30+
if col < len andalso String.sub (token, len - 1 - col) = ch
31+
then (sign, sum + sign)
32+
else (sign, sum)
33+
end
34+
in #2 (foldl processToken (1, 0) tokens) end
35+
36+
val weight = Vector.tabulate (nColumns, weightAt)
37+
38+
(* Rightmost column where ch appears *)
39+
val rank = Vector.foldli (fn (i, w, r) =>
40+
if w <> 0 then Int.min (i, r) else r) nColumns weight
41+
in
42+
if rank = nColumns then NONE
43+
else SOME {
44+
letter = ch,
45+
leading = leading,
46+
weight = weight,
47+
rank = rank
48+
}
49+
end
50+
51+
(* Assemble letter infos for A-Z, sorted by rank *)
52+
fun sortByRank infos =
53+
let
54+
fun insert (x: info, []) = [x]
55+
| insert (x, (y: info) :: ys) =
56+
if #rank x <= #rank y then x :: y :: ys
57+
else y :: insert (x, ys)
58+
in foldl (fn (e, acc) => insert (e, acc)) [] infos end
59+
60+
val letters: info list =
61+
sortByRank (List.mapPartial letterInfo
62+
(List.tabulate (26, fn i => chr (Char.ord #"A" + i))))
63+
64+
(* Mapping: associates each letter with its assigned digit *)
65+
type mapping = (char * int) list
66+
67+
fun lookup (_, []: mapping) = 0
68+
| lookup (ch, (c, digit) :: rest) = if c = ch then digit else lookup (ch, rest)
69+
70+
fun isClaimed (claimed, d) =
71+
Word.andb (claimed, Word.<< (0w1, Word.fromInt d)) <> 0w0
72+
73+
fun claim (claimed, d) =
74+
Word.orb (claimed, Word.<< (0w1, Word.fromInt d))
75+
76+
(* Sum of weight[col] * digit for all letters *)
77+
fun columnSum (col, mapping) =
78+
foldl (fn ({letter, weight, ...}: info, sum) =>
79+
if col < Vector.length weight
80+
then sum + Vector.sub (weight, col) * lookup (letter, mapping)
81+
else sum) 0 letters
82+
83+
(* Check column and advance, or finish *)
84+
fun advanceColumn (remaining, col, claimed, carry, mapping) =
85+
let val colSum = carry + columnSum (col, mapping)
86+
in
87+
if colSum mod 10 <> 0 then NONE
88+
else if col + 1 < nColumns then
89+
search (remaining, col + 1, claimed, colSum div 10, mapping)
90+
else if colSum = 0 then SOME mapping
91+
else NONE
92+
end
93+
94+
(* Search: assign digits to letters column by column *)
95+
and search (remaining, col, claimed, carry, mapping) =
96+
case remaining of
97+
[] => advanceColumn ([], col, claimed, carry, mapping)
98+
| (letter :: rest) =>
99+
if #rank letter > col then
100+
advanceColumn (remaining, col, claimed, carry, mapping)
101+
else
102+
let
103+
fun tryDigit digit =
104+
if digit > 9 then NONE
105+
else if isClaimed (claimed, digit) then tryDigit (digit + 1)
106+
else
107+
case search (rest, col, claim (claimed, digit),
108+
carry, (#letter letter, digit) :: mapping) of
109+
SOME m => SOME m
110+
| NONE => tryDigit (digit + 1)
111+
in tryDigit (#leading letter) end
112+
113+
(* Convert puzzle string by substituting digits from mapping *)
114+
fun substitute mapping =
115+
String.implode (map (fn c =>
116+
if Char.isAlpha c then chr (Char.ord #"0" + lookup (c, mapping))
117+
else c) (String.explode puzzle))
118+
in
119+
case search (letters, 0, 0w0, 0, []) of
120+
SOME m => substitute m
121+
| NONE => raise Fail "no solution"
122+
end
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
fun solve (puzzle: string): string =
2+
raise Fail "'solve' is not implemented"
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
(* version 1.0.0 *)
2+
3+
use "testlib.sml";
4+
use "alphametics.sml";
5+
6+
infixr |>
7+
fun x |> f = f x
8+
9+
val testsuite =
10+
describe "alphametics" [
11+
test "puzzle with three letters"
12+
(fn _ => solve "I + BB == ILL" |> Expect.equalTo "1 + 99 == 100"),
13+
14+
test "solution must have unique value for each letter"
15+
(fn _ => (fn _ => solve "A == B") |> Expect.error (Fail "no solution")),
16+
17+
test "leading zero solution is invalid"
18+
(fn _ => (fn _ => solve "ACA + DD == BD") |> Expect.error (Fail "no solution")),
19+
20+
test "puzzle with two digits final carry"
21+
(fn _ => solve "A + A + A + A + A + A + A + A + A + A + A + B == BCC" |> Expect.equalTo "9 + 9 + 9 + 9 + 9 + 9 + 9 + 9 + 9 + 9 + 9 + 1 == 100"),
22+
23+
test "puzzle with four letters"
24+
(fn _ => solve "AS + A == MOM" |> Expect.equalTo "92 + 9 == 101"),
25+
26+
test "puzzle with six letters"
27+
(fn _ => solve "NO + NO + TOO == LATE" |> Expect.equalTo "74 + 74 + 944 == 1092"),
28+
29+
test "puzzle with seven letters"
30+
(fn _ => solve "HE + SEES + THE == LIGHT" |> Expect.equalTo "54 + 9449 + 754 == 10257"),
31+
32+
test "puzzle with eight letters"
33+
(fn _ => solve "SEND + MORE == MONEY" |> Expect.equalTo "9567 + 1085 == 10652"),
34+
35+
test "puzzle with ten letters"
36+
(fn _ => solve "AND + A + STRONG + OFFENSE + AS + A + GOOD == DEFENSE" |> Expect.equalTo "503 + 5 + 691208 + 2774064 + 56 + 5 + 8223 == 3474064"),
37+
38+
test "puzzle with ten letters and 199 addends"
39+
(fn _ => 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" |> Expect.equalTo "9874 + 1 + 5730 + 980305630 + 563 + 122 + 874963704 + 7 + 9022 + 1 + 9120 + 9819 + 512475704 + 794 + 97920 + 974 + 1 + 270 + 980 + 9120 + 65 + 980 + 2149 + 5730 + 863404 + 2190 + 15903 + 980 + 57349 + 5198034 + 5630400 + 980 + 8633634 + 980 + 2149 + 5300 + 93622 + 903375704 + 980 + 863404 + 65 + 5730 + 980 + 93622 + 30494 + 19 + 980 + 8620 + 65 + 264404 + 79 + 74 + 98030 + 9819 + 480 + 496304 + 36204 + 65 + 20198034 + 15903 + 480 + 419745704 + 803 + 8190 + 655 + 98640 + 50134 + 1 + 91490 + 37404 + 14 + 480 + 80134 + 980 + 20149 + 513 + 86340 + 98640 + 5149 + 863404 + 9819 + 57349 + 8013 + 980 + 93622 + 5200 + 655 + 96 + 980 + 563049 + 980 + 863404 + 9819 + 120394 + 31740 + 980 + 491304 + 65 + 980 + 698034 + 14 + 980 + 93622 + 1441724 + 19 + 980 + 96912 + 48759 + 803 + 90098 + 9013 + 8665 + 655 + 96346 + 14 + 980 + 2149 + 86340 + 56350794 + 794 + 2750 + 980 + 57349 + 5198034 + 8013 + 65 + 980 + 8633634 + 98073 + 50134 + 9819 + 980 + 57304 + 563 + 98073 + 501494 + 133049 + 14 + 980 + 57349 + 5198034 + 30409920 + 980 + 2149 + 65 + 980 + 5730 + 863404 + 980 + 2149 + 93622 + 81314404 + 980 + 563049 + 80139 + 5300 + 19 + 2149 + 65 + 980 + 2149 + 93622 + 122 + 65503 + 98073 + 5730 + 8019 + 96 + 980 + 144749034 + 513 + 655 + 980 + 93622 + 51494 + 794 + 2750 + 4863903 + 14 + 49134 + 3740 + 980 + 863404 + 3049 + 4150 + 15903 + 122 + 48130 + 869 + 5748 + 14 + 98073 + 1557271904 + 917263 + 1 + 36654 + 563 + 98073 + 4150 == 5639304404")
40+
]
41+
42+
val _ = Test.run testsuite

0 commit comments

Comments
 (0)