Advent of Code 2024 - Day 5 in F#
Date: 2024-12-05 | create | build | advent-of-code | fsharp |
In this post I'll share my solution to Advent of Code 2024 - Day 5 using F#.
HAMINIONs Members get full access to the project files so you can git clone
and run it yourself.
Day 5 - Part 1
Prompt: https://adventofcode.com/2024/day/5
Given:
- Page ordering rules: X|Y
- If X and Y in list, X must come before Y
- Pages in each update
- x,y,z
Ask:
- Find the pages in correct order
- Sum the middle element of each of these
SubProblems
- A: How to tell if stuff is in order or not
- A.1:
- Set of numbers to left
- lookup of number to vals that must be after it
- iterate over pages - O(n * m)
- add page to set of numbers to left
- Lookup number and see if any rules match
- If they do -> bad
- If fine - fine
- A.1:
Solutions:
- A: Preprocess input, check if each is in order, sum middle - O(2n + n * m^2), 2n
- Process input into list of page lists - O(n), n
- Process input int lookup of X -> [Y, Y, ...] - O(n), n
- (All page rules saying X comes before Y)
- Function - is_page_in_order - O(pages length * rule length)
- Set of pages to left of pointer
- Iterate over all pages
- get current page
- get current page rules for things that come after it
- if any of those rules include pages to left -> invalid
- add current page to pages to left
- return is_valid
- Iterate over every page list, checking is_page_in_order - O(n * is_page_in_order)
- Keep only the ones in order
- Return sum of the middle thing
Code:
type Day5Input =
{
NumberToValuesAfterIt: IDictionary<int, int list>
Pages: int list list
}
let read_input
(file_name: string)
: Day5Input
=
let lines =
Utils.read_all_input_file_lines
file_name
let x_to_vals_after_it =
lines
|> Seq.filter (fun l -> l.Contains("|"))
|> Seq.map(fun l -> l.Split("|"))
|> Seq.map(fun vals -> (vals[0], vals[1]))
|> Seq.groupBy(fun (x, y) -> x)
|> Seq.map (fun (x, groups) ->
let just_ys =
groups
|> Seq.map (fun g -> int (snd g))
|> Seq.toList
((int x), just_ys)
)
|> dict
let all_pages =
lines
|> Seq.filter(fun l -> l.Contains(","))
|> Seq.map(fun l -> l.Split(","))
|> Seq.map(fun vals ->
vals
|> Seq.map (fun v -> int v)
|> Seq.toList)
|> Seq.toList
{
NumberToValuesAfterIt = x_to_vals_after_it
Pages = all_pages
}
let is_page_in_order
(page_to_pages_after_it: IDictionary<int, int list>)
(pages: int list)
: bool
=
let mutable pages_to_left = Set.ofList [pages[0]]
let mutable is_valid = true
for i = 1 to pages.Length - 1 do
let current_page = pages[i]
match page_to_pages_after_it.TryGetValue current_page with
| false, _ -> ()
| true, pages_that_must_come_later ->
let page_in_wrong_order =
pages_that_must_come_later
|> Seq.exists (fun p ->
pages_to_left.Contains(p)
)
if page_in_wrong_order then
is_valid <- false
pages_to_left <- pages_to_left.Add(current_page)
is_valid
let solve_part1
(file_name: string)
=
printfn "Day 5.1: Solving"
let input = read_input file_name
let is_in_order =
is_page_in_order input.NumberToValuesAfterIt
let total =
input.Pages
|> Seq.choose (fun page ->
match is_in_order (page |> Seq.toList) with
| true -> Some page
| false -> None
)
|> Seq.map (fun valid_pages ->
valid_pages[valid_pages.Length / 2]
)
|> Seq.sum
printfn "Day 5.1: Total: %A" total
My Answer -> Accepted
Day 5 - Part 2
Prompt: https://adventofcode.com/2024/day/5
Given:
- Same input as above
Ask:
- Find only incorrect pages
- Put them in order
- Then sum up the middle bits
Subproblems:
- A: How to reorder
- A.1: Just randomize til we find one? -> May never finish
- A.2: Try all combos of ordering... - O(m^2 * m) -> Permutations so is (n!)
- A.3: If too slow, could do some sort of topological sort w a graph
Solutions:
- A: Brute force - O(n * m!)
- I tried to do this but eventually realized (after trying to figure out why my soln wasn't finished after 10+ mins) this leads to a O(n * m!) solution and m! gets REALLY big. My test / debug input finished quickly but the real input never got past even the first page check. On investigation I realized this was smth like 20! which is 2.432902e+18 or 2432901000000000000 which like 2 quintillion or smth. Even at 1000 cals per second we'd take ~2432901000000000 seconds which is like 77k years or smth (my math is probably wrong) but anyway WAY too much time.
- B: Sort this stuff - O(n * mlogm), ~3n
- If we think ab it, the page ordering rules are just ways to order pages. We already have pretty efficient algorithms for sorting so if we could leverage that then we could potentially order any out-of-order page in ~O(mlogm)
- Preprocess the input so we have both the X before Y and Y before X rules
- Then use those rules in a custom comparer to determine ordering. If no set ordering, consider them equal.
- Caveat: This works if we have a stable ordering. It's possible the page orders we're given have cycles in which case this could be an instable sort and lead to an infinite loop or smth (F# crashes on this). Luckily AOC does seem to give us orderings without cycles so this seems to work okay.
Code:
type Day5Input2 =
{
NumberToValuesAfterIt: IDictionary<int, int Set>
NumberToValuesBeforeIt: IDictionary<int, int Set>
Pages: int list list
}
let read_input2
(file_name: string)
: Day5Input2
=
let lines =
Utils.read_all_input_file_lines
file_name
let all_x_y_pairs =
lines
|> Seq.filter (fun l -> l.Contains("|"))
|> Seq.map(fun l -> l.Split("|"))
|> Seq.map(fun vals -> (vals[0], vals[1]))
let x_to_vals_after_it =
all_x_y_pairs
|> Seq.groupBy(fun (x, y) -> x)
|> Seq.map (fun (x, groups) ->
let just_ys =
groups
|> Seq.map (fun g -> int (snd g))
|> Seq.toList
|> Set.ofList
((int x), just_ys)
)
|> dict
let y_to_vals_before_it =
all_x_y_pairs
|> Seq.groupBy(fun (x, y) -> y)
|> Seq.map (fun (y, groups) ->
let just_xs =
groups
|> Seq.map (fun g -> int (fst g))
|> Seq.toList
|> Set.ofList
((int y), just_xs)
)
|> dict
let all_pages =
lines
|> Seq.filter(fun l -> l.Contains(","))
|> Seq.map(fun l -> l.Split(","))
|> Seq.map(fun vals ->
vals
|> Seq.map (fun v -> int v)
|> Seq.toList)
|> Seq.toList
{
NumberToValuesAfterIt = x_to_vals_after_it
NumberToValuesBeforeIt = y_to_vals_before_it
Pages = all_pages
}
let create_valid_from_invalid
(input: Day5Input2)
(invalid_pages: int list)
: int list
=
invalid_pages
|> List.sortWith( fun x y ->
if x = y
then 0
else
let x_before_y = (
(match input.NumberToValuesAfterIt.TryGetValue x with
| false, _ -> false
| true, rules ->
rules.Contains(y))
|| (
match input.NumberToValuesBeforeIt.TryGetValue y with
| false, _ -> false
| true, rules ->
rules.Contains(x)
)
)
if x_before_y
then -1
else
let y_before_x = (
(match input.NumberToValuesAfterIt.TryGetValue y with
| false, _ -> false
| true, rules ->
rules.Contains(x))
|| (
match input.NumberToValuesBeforeIt.TryGetValue x with
| false, _ -> false
| true, rules ->
rules.Contains(y)
)
)
if y_before_x
then 1
else
0
)
let is_page_in_order2
(page_to_pages_after_it: IDictionary<int, int Set>)
(pages: int list)
: bool
=
let mutable pages_to_left = Set.ofList [pages[0]]
let mutable is_valid = true
for i = 1 to pages.Length - 1 do
let current_page = pages[i]
match page_to_pages_after_it.TryGetValue current_page with
| false, _ -> ()
| true, pages_that_must_come_later ->
let page_in_wrong_order =
pages_that_must_come_later
|> Seq.exists (fun p ->
pages_to_left.Contains(p)
)
if page_in_wrong_order then
is_valid <- false
pages_to_left <- pages_to_left.Add(current_page)
is_valid
let solve_part2
(file_name: string)
=
printfn "Day 5.2: Solving"
let input = read_input2 file_name
let is_in_order =
is_page_in_order2 input.NumberToValuesAfterIt
let total =
input.Pages
|> Seq.choose (fun page ->
match is_in_order (page |> Seq.toList) with
| true -> None
| false -> Some page
)
|> Seq.map (fun invalid_pages ->
create_valid_from_invalid
input
invalid_pages
)
|> Seq.map (fun valid_pages ->
assert is_in_order valid_pages
valid_pages[valid_pages.Length / 2]
)
|> Seq.sum
printfn "Day 5.2: Total: %A" total
Next
I enjoyed this problem a lot - I haven't run into an IRL situation in awhile where the size of input processing was prohibitively expensive so this was a nice reminder that hey it totally is worth doing the napkin math to make sure you're not doing O(n!) calcs. By the same token though n and n^2 often lead to vastly simpler algorithms and for many data sizes it makes sense to bias simplicity over "optimal". But n! is another matter =)
If you liked this post - please leave a like, comment, and/or share to your network for the algorithm.
You might also like:
Want more like this?
The best / easiest way to support my work is by subscribing for future updates and sharing with your network.