Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Advent of Code 2024: Day 2

days/Day2.livemd

Advent of Code 2024: Day 2

Mix.install([
  {:req, "~> 0.5.8"},
  {:combinatorics, "~> 0.1.0"}
])

Data

require Integer
opts = [headers: [{"cookie", "session=#{System.fetch_env!("AOC_SESSION_COOKIE")}"}]]
input = Req.get!("https://adventofcode.com/2024/day/2/input", opts).body
test_input = "7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9
"

Parser

Parser Description

defmodule Parser do
  def parse input do
    String.split(input,["\n","\r\n"], trim: true)
    |> Enum.map(&to_integer_list/1)
  end

  defp to_integer_list list do
    String.split(list) |> Enum.map(&String.to_integer/1)
  end
end

Parser.parse test_input
Parser.parse input

Solution

Part 1

The is_safe function is the key part of both solutions. It reduces through a set of readings checking that the conditions are met all the way through. We then simply run it for each set of readings and count how many are safe.

Part 2

For part 2 we brute force by gettings all the possible combinations of each set of readings and running them through the is_safe function: if any of them succeed then the readings are safe.

defmodule Day2 do
  def part1 input do
    Parser.parse(input) |> Enum.count(&(is_safe(&1)))  
  end

  def part2 input do 
    Parser.parse(input) |> Enum.count(&(is_damped_safe(&1)))
  end

  defp is_damped_safe readings do
    combinations(readings) |> Enum.any?(fn reading -> is_safe(reading) end)
  end

  # Reduce through the list keeping track of direction and working out if the list
  # is still safe at each point.  Could be improved with a reduce_while
  defp is_safe readings do
    Enum.reduce(readings, { :unknown }, fn reading, acc -> 
      case acc do
        { :unsafe } -> 
          { :unsafe }        
        { :unknown } ->
          { :safe, :unknown, reading }
        { :safe, :unknown, x } when x > reading and abs(x-reading) < 4 ->
          { :safe, :decreasing, reading}
        { :safe, :unknown, x } when x < reading and abs(x-reading) < 4 ->
          { :safe, :increasing, reading}        
        { :safe, :decreasing, x } when x > reading and abs(x-reading) < 4 ->
          { :safe, :decreasing, reading }
        { :safe, :increasing, x } when x < reading and abs(x-reading) < 4 ->
          { :safe, :increasing, reading }        
        _ ->
          { :unsafe }
      end
    end)
    |> then(fn result -> 
      case result do
        {:unsafe} -> false
        _ -> true        
      end
    end)
  end

  defp combinations list do
    [ list | Combinatorics.n_combinations(length(list)-1, list) ]
  end
end

Day2.part1 test_input

Testing

ExUnit.start(auto_run: false, exclude: [:skip])

defmodule Day2Specs do
  @test_input test_input
  @real_input input
  use ExUnit.Case, async: false

  test "part 1 should return 2 for the test input" do
    assert Day2.part1(@test_input) === 2
  end
  
  test "part 1 should return 422 for the real input" do
    assert Day2.part1(@real_input) === 442
  end
  
  test "part 2 should return 4 for the test input" do
    assert Day2.part2(@test_input) === 4
  end
  
  test "part 2 should return 493 for the real input" do
    assert Day2.part2(@real_input) === 493
  end
  
end

ExUnit.run()

Results

Part 1 Result

Day2.part1(input)

Part 2 Result

Day2.part2(input)