Advent of Code 2024 - Day 2 in F#

Date: 2024-12-02 | create | build | fsharp | advent-of-code |

In this post I'll share my solution to Advent of Code 2024 Day 2 using F#.

HAMINIONs Members get full access to the project files so you can git clone and run it yourself.

Day 2 - Part 1

Prompt: https://adventofcode.com/2024/day/2

My Takeaway:

(*
Part 1

Given: 
* List of reports
    * One report per line
    * Report is a list of numbers called `levels` separated by spaces

Ask:
* Figure out which reports are safe
    * Safe if:
        * All increasing or decreasing
        * Levels differ by at least one and at most 3
* Return count of safe reports

Solutions: 
* A: Parse safety, then count - O(3n), 2n
    * Parse out data into reports - O(n), n
    * Iterate over reports to get safety - O(n), n
        * Check all increasing or decreasing
        * Check if levels differ by 1-3
    * Sum safety to get final count - O(n)
*)

Code:

let is_report_safe 
    (report: int list) 
    : bool 
    = 
    if report.Length <= 1
    then true 
    else 

    let first = report[0]
    let second = report[1]

    let is_descending = first >= second 

    let are_pairs_safe = 
        report
        |> List.pairwise
        |> List.map (fun pair -> 
            let first = fst pair 
            let second = snd pair 

            if first = second 
            then false 
            else 

            if (first >= second) <> is_descending
            then false 
            else 

            let difference = abs (first - second) 
            if difference < 1 || difference > 3 
            then false 
            else 

            true
        )

    are_pairs_safe
    |> List.forall id

let solve_part1 = fun () -> 

    printfn "Problem 2.1: Solving"

    let exeDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
    let filePath = Path.Combine(exeDir, "Static", "day_2_input.txt")
    let lines = File.ReadLines(filePath)

    let all_reports = 
        lines 
        |> Seq.map (fun l -> 
            l.Split(" ")
            |> Array.map (fun i -> int i)
        )

    let total_safe_reports = 
        all_reports
        |> Seq.map (fun report -> is_report_safe (report |> List.ofArray))
        |> Seq.filter (fun report_safety -> report_safety = true)
        |> Seq.length

    printfn "Problem 2.1: Total Safe Reports - %A" total_safe_reports
    total_safe_reports

My Answer -> Accepted

Some notes:

  • I'm using the if / then / else format for simple early returns. A more functional approach would likely use railroading but sometimes I find guard statements easier to read.
  • List.pairwise turns a list into a list of tuples containing all pairs

Day 2 - Part 2

Prompt: https://adventofcode.com/2024/day/2

My Takeaways:

(*
Part 2

Given: 
* Same reports as above

Ask: 
* Count safe reports - assuming you canremove up to 1 bad level
    * Removal removes that one level but rest of report must remain okay

Solution: -> A: Simple, reuses code
* A: Same approach as above but if it fails, we try to see if can make it succeed - O(n * m^2)
    * Same as above
    * For failure cases - try to see if can succeed:
        * Brute force - try each combo of the list with one removal
            * If any succeed - take it as a success and try again
* B: ~Single pass - O(n * 3m)
    * Intuition - if we only have one failure case then there are only 3 values we
    need to check - x-1, x, x + 1 - if none of those succeed then we'd have to 
    remove more than one item to get a "safe" report
    * This is similar to A but limits retries to 3n

*)
  • I decided to go with Solution A as it seemed simplest to get right. This may not be the optimal Leetcode solution but for this case A seems easier to reason about, utilizes already-tested code, and is not that much slower so here seems like a better choice overall since we don't foresee any perf issues with it.

Code: (is_report_safe is reused from above code)

let remove_at_index 
    (list: int list)
    (index: int)
    : int list
    = 
    List.concat [
        list.[0..index - 1]
        list.[index + 1..]
    ]

let is_report_safe_using_dampener 
    (report: int list) 
    : bool 
    =
    // If og report is safe -> return true
    if (is_report_safe report) 
    then true 
    else 

    // hamy: exists to short-circuit on first "true" value
    seq {0..report.Length - 1}
    |> Seq.exists(fun index -> 
        remove_at_index report index 
        |> is_report_safe
    )

let solve_part2 = fun () -> 

    printfn "Problem 2.2: Solving"

    let exe_dir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)
    let file_path = Path.Combine(exe_dir, "Static", "day_2_input.txt")
    let lines = File.ReadLines(file_path)

    let all_reports = 
        lines 
        |> Seq.map (fun l -> 
            l.Split(" ")
            |> Array.map (fun i -> int i)
        )

    let total_safe_reports = 
        all_reports
        |> Seq.map (fun report -> report |> List.ofArray)
        |> Seq.map (
            fun report -> 
                    
                is_report_safe_using_dampener report)
        |> Seq.filter (fun report_safety -> report_safety = true)
        |> Seq.length

    printfn "Problem 2.2: Total Safe Reports - %A" total_safe_reports
    total_safe_reports

My answer -> Accepted

Next

See my other AOC 2024 solutions in F# here.

HAMINIONs Members get full access to the project files so you can git clone and run it yourself.

If you liked this post 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.