-
Notifications
You must be signed in to change notification settings - Fork 0
/
Day12.fs
227 lines (182 loc) · 7.71 KB
/
Day12.fs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
module aoc23.Day12
[<Struct>]
type ConditionRecord =
{ offset: int
states: char list
groups: int list }
[<Struct>]
type RecordStats =
{ knownOperational: int
unknownOperational: int
unknownDamaged: int }
member x.isSolvable = x.unknownOperational >= 0 && x.unknownDamaged >= 0
member x.isSolved = x.unknownOperational = 0 && x.knownOperational = 0
module Parser =
let line str =
match str |> StringEx.splitC ' ' with
| [| state; counts |] ->
{ offset = 0
states = state |> Seq.toList
groups = counts |> StringEx.splitC ',' |> Seq.map int |> Seq.toList }
| _ -> failwith "unexpected input"
let (|Operational|Damaged|Unknown|) =
function
| '#' -> Operational
| '.' -> Damaged
| '?' -> Unknown
| c -> failwith $"Unknown state '{c}'"
module ConditionRecord =
let unfold record =
let repeat times separator list =
[ for i in 1..times do
if i <> 1 then
yield! separator |> Option.toList
yield! list ]
{ offset = 0
states = repeat 5 (Some '?') record.states
groups = repeat 5 None record.groups }
let offsetTail offset record =
{ offset = record.offset + offset
states = record.states |> List.skip (min (record.states.Length) offset)
groups =
match record.groups with
| [] -> []
| _ :: tail -> tail }
let stats record =
let totalOperational = record.groups |> List.sum
let totalDamaged = (record.states |> List.length) - totalOperational
let counts = record.states |> List.countBy id |> Map
let knownOperational = (counts |> Map.tryFind '#' |> Option.defaultValue 0)
let knownDamaged = (counts |> Map.tryFind '.' |> Option.defaultValue 0)
{ knownOperational = knownOperational
unknownOperational = totalOperational - knownOperational
unknownDamaged = totalDamaged - knownDamaged }
let nextGroupSolutionOffsets record =
let rec allCombinations offset stats group =
function
// resolve unknown spring options, recurse without increasing the offset
| operational, Unknown :: remaining ->
[ if stats.unknownOperational > 0 then
yield!
allCombinations
offset
{ stats with
unknownOperational = stats.unknownOperational - 1 }
group
(operational, '#' :: remaining)
if stats.unknownDamaged > 0 then
yield!
allCombinations
offset
{ stats with
unknownDamaged = stats.unknownDamaged - 1 }
group
(operational, '.' :: remaining) ]
// matched a correctly sized group terminated by damaged or at the end
| operational, ([] | Damaged :: _) when operational = group -> [ offset + 1 ]
// group became too big, discard
| operational, _ when operational = group -> []
// reached the end before finding the group, discard
| _, [] -> []
// just continue when not inside an operational group yet
| 0, Damaged :: remaining -> allCombinations (offset + 1) stats group (0, remaining)
// hit a damaged spring before reaching needed group size, discard
| _, Damaged :: _ -> []
// continue and increment counter when inside an operational group
| operational, Operational :: remaining ->
allCombinations (offset + 1) stats group (operational + 1, remaining)
match (record, stats record) with
| _, stats when stats.isSolved -> [ 0 ]
| { groups = nexGroup :: _
states = states },
stats when stats.isSolvable -> allCombinations 0 stats nexGroup (0, states)
| _ -> []
let allSolutionsCount record =
([ (1L, record) ], [ 1 .. record.groups.Length ])
||> List.fold (fun records _rank ->
let expand (multiplier, record) =
record
|> nextGroupSolutionOffsets
|> List.map (fun offsetBy ->
{| multiplier = multiplier
record = record
offsetBy = offsetBy
destinationOffset = record.offset + offsetBy |})
let recordTails =
records
|> List.collect expand
|> List.groupBy _.destinationOffset
|> List.map (fun (_, x) ->
let recordTail = x.Head.record |> offsetTail x.Head.offsetBy
let multiplier = x |> List.sumBy (fun i -> i.multiplier)
multiplier, recordTail)
recordTails)
|> List.sumBy fst
let part1 lines =
lines
|> Array.map Parser.line
|> Array.Parallel.map (ConditionRecord.allSolutionsCount)
|> Array.sum
let part2 lines =
lines
|> Array.map Parser.line
|> Array.Parallel.map (ConditionRecord.unfold >> ConditionRecord.allSolutionsCount)
|> Array.sum
let run = runReadAllLines part1 part2
module Tests =
open Xunit
open Swensen.Unquote
[<Theory>]
[<InlineData("???.### 1,1,3", 1)>]
[<InlineData(".??..??...?##. 1,1,3", 4)>]
[<InlineData("?#?#?#?#?#?#?#? 1,3,1,6", 1)>]
[<InlineData("????.#...#... 4,1,1", 1)>]
[<InlineData("????.######..#####. 1,6,5", 4)>]
[<InlineData("?###???????? 3,2,1", 10)>]
let ``part 1 line-wise`` input expected = part1 [| input |] =! expected
[<Theory>]
[<InlineData("???.### 1,1,3", 1)>]
[<InlineData(".??..??...?##. 1,1,3", 16384)>]
[<InlineData("?#?#?#?#?#?#?#? 1,3,1,6", 1)>]
[<InlineData("????.#...#... 4,1,1", 16)>]
[<InlineData("????.######..#####. 1,6,5", 2500)>]
[<InlineData("?###???????? 3,2,1", 506250)>]
let ``part 2 line-wise`` input expected = part2 [| input |] =! expected
[<Theory>]
[<InlineData(".# 1", ".#?.#?.#?.#?.# 1,1,1,1,1")>]
[<InlineData("???.### 1,1,3", "???.###????.###????.###????.###????.### 1,1,3,1,1,3,1,1,3,1,1,3,1,1,3")>]
let ``unfold`` example expected =
test <@ (example |> Parser.line |> ConditionRecord.unfold) = (expected |> Parser.line) @>
[<Fact>]
let ``next group solution count`` () =
let record = "???.### 1,3" |> Parser.line
let solutionOffsets = record |> ConditionRecord.nextGroupSolutionOffsets
solutionOffsets =! [ 2; 3; 4 ]
module stats =
[<Fact>]
let ``stats`` () =
let stats = "???.### 1,1,3" |> Parser.line |> ConditionRecord.stats
stats
=! { knownOperational = 3
unknownDamaged = 1
unknownOperational = 2 }
stats.isSolvable =! true
stats.isSolved =! false
[<Fact>]
let ``stats2`` () =
let stats = "#.#.### 1,1,3" |> Parser.line |> ConditionRecord.stats
stats
=! { knownOperational = 5
unknownDamaged = 0
unknownOperational = 0 }
stats.isSolved =! false
stats.isSolvable =! true
[<Fact>]
let ``empty stats`` () =
let stats = { offset = 0; groups = []; states = [] } |> ConditionRecord.stats
stats
=! { knownOperational = 0
unknownDamaged = 0
unknownOperational = 0 }
stats.isSolved =! true
stats.isSolvable =! true