Advent of Code 2024 - Day 2

Here is my solution for day 2 of Advent of Code:

5 Likes
3 Likes

I had come up with such a beautiful solution (until some edge-cases threw cold water).

The idea was to have a state of isIncreasing, isDecreasing, and Other and add indexes of pairs there, with the theory that I will have either isIncreasing or isDecreasing to be more than 1 in size, and other two to be 0 and 1 (or vice versa), then I’d remove the idx or idx + 1 from the array and check for safety.

Looked like there were 14 cases that violated this. So I had to get back to dampening.

:face_holding_back_tears: is how I’m feeling right now.

Oh, and my solution was more or less like @sevenseacat one (who btw has been an inspiration).

Not sharing code because it wasn’t Elixir.

1 Like

No optimization here, just building all possible list before trying them one by one :smiley:

defmodule AdventOfCode.Solutions.Y24.Day02 do
  alias AoC.Input

  def parse(input, _part) do
    Enum.map(Input.stream!(input, trim: true), &parse_line/1)
  end

  defp parse_line(line) do
    Enum.map(String.split(line, " "), &String.to_integer/1)
  end

  def part_one(problem) do
    problem
    |> Enum.filter(&safe?/1)
    |> length()
  end

  defp safe?([a, b | _] = list) when a < b, do: safe?(:asc, list)
  defp safe?([a, b | _] = list) when a > b, do: safe?(:desc, list)
  defp safe?([a, a | _]), do: false

  defp safe?(:asc, [a, b | rest]) when abs(a - b) in 1..3 and a < b, do: safe?(:asc, [b | rest])
  defp safe?(:desc, [a, b | rest]) when abs(a - b) in 1..3 and a > b, do: safe?(:desc, [b | rest])
  defp safe?(_, [_last]), do: true
  defp safe?(_, _), do: false

  def part_two(problem) do
    problem
    |> Enum.filter(&safeish?/1)
    |> length()
  end

  defp safeish?(list) do
    candidates = [list | Enum.map(0..(length(list) - 1), &List.delete_at(list, &1))]
    Enum.any?(candidates, &safe?/1)
  end
end

Edit:

candidates = Stream.concat([list], Stream.map(0..(length(list) - 1), &List.delete_at(list, &1)))

This would save memory but given the input size it’s actually slower.

2 Likes

Part 1:

all =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    line
    |> String.split(" ", trim: true)
    |> Enum.map(&String.to_integer/1)
  end)

safe? = fn levels ->
  Enum.reduce_while(levels, {nil, nil}, fn a, acc ->
    case acc do
      {nil, _} ->
        {:cont, {a, nil}}

      {b, nil} when abs(a - b) in [1, 2, 3] and a < b ->
        {:cont, {a, :decrease}}

      {b, nil} when abs(a - b) in [1, 2, 3] and a > b ->
        {:cont, {a, :increase}}

      {b, :decrease} when abs(a - b) in [1, 2, 3] and a < b ->
        {:cont, {a, :decrease}}

      {b, :increase} when abs(a - b) in [1, 2, 3] and a > b ->
        {:cont, {a, :increase}}

      _ ->
        {:halt, :invalid}
    end
  end)
  |> case do
    :invalid -> false
    _ -> true
  end
end

for levels <- all do
  safe?.(levels)
end
|> Enum.count(& &1)

Part 2:

all =
  puzzle_input
  |> String.split("\n", trim: true)
  |> Enum.map(fn line ->
    line
    |> String.split(" ", trim: true)
    |> Enum.map(&String.to_integer/1)
  end)

damp = fn levels ->
  levels
  |> Enum.with_index()
  |> Enum.map(fn {_, i} -> List.delete_at(levels, i) end)
end

safe? = fn levels ->
  Enum.reduce_while(levels, {nil, nil}, fn a, acc ->
    case acc do
      {nil, _} ->
        {:cont, {a, nil}}

      {b, nil} when abs(a - b) in [1, 2, 3] and a < b ->
        {:cont, {a, :decrease}}

      {b, nil} when abs(a - b) in [1, 2, 3] and a > b ->
        {:cont, {a, :increase}}

      {b, :decrease} when abs(a - b) in [1, 2, 3] and a < b ->
        {:cont, {a, :decrease}}

      {b, :increase} when abs(a - b) in [1, 2, 3] and a > b ->
        {:cont, {a, :increase}}

      _ ->
        {:halt, :invalid}
    end
  end)
  |> case do
    :invalid -> false
    _ -> true
  end
end

for levels <- all do
  safe?.(levels) || Enum.any?(damp.(levels), fn level -> safe?.(level) end)
end
|> Enum.count(& &1)

I was trying hard to find a smart-ass solution for part 2 without using List.delete_at/2, but in the end, I had to admit that I’m not that smart after all.

Part 1

puzzle_input
|> String.split("\n")
|> Enum.map(&String.split/1)
|> Enum.map(&Enum.map(&1, fn s -> String.to_integer(s) end))
|> Enum.count(fn
  [a, a | _] ->
    false
  
  [a, b | _] = line ->
    sign = div(a - b, abs(a - b))
  
    line
    |> Enum.chunk_every(2, 1, :discard)
    |> Enum.all?(fn [a, b] ->
      sign * (a - b) in 1..3
    end)
end)

Part 2

puzzle_input
|> String.split("\n")
|> Enum.map(&String.split/1)
|> Enum.map(&Enum.map(&1, fn s -> String.to_integer(s) end))
|> Enum.count(fn line ->
  0..length(line)
  |> Stream.map(&List.delete_at(line, &1))
  |> Enum.any?(fn
    [a, a | _] ->
      false
  
    [a, b | _] = line ->
      sign = div(a - b, abs(a - b))
  
      line
      |> Enum.chunk_every(2, 1, :discard)
      |> Enum.all?(fn [a, b] ->
        sign * (a - b) in 1..3
      end)
  end)
end)
2 Likes
#!/usr/bin/env elixir
# 2024. day 2.

defmodule A do
  @spec is_ok?([integer()]) :: boolean()
  def is_ok?(lst), do: is_ok?(lst, nil, nil)

  @spec is_ok?([integer()], nil | integer(), nil | :inc | :dec) :: boolean()
  defp is_ok?(lst, prev, dir)

  defp is_ok?([], _, _), do: true # empty list
  defp is_ok?([_fst], nil, _), do: true # single element list
  defp is_ok?([fst | rest], nil, nil), do: is_ok?(rest, fst, nil) # more than one element

  # same number!
  defp is_ok?([fst | _rest], fst, _dir), do: false

  # direction is unknown
  defp is_ok?([fst | rest], prev, nil) when fst < prev, do: (if prev-fst<=3, do: is_ok?(rest, fst, :dec), else: false)
  defp is_ok?([fst | rest], prev, nil) when fst > prev, do: (if fst-prev<=3, do: is_ok?(rest, fst, :inc), else: false)

  # direction is decreasing
  defp is_ok?([fst | rest], prev, :dec) when fst < prev, # we keep on decreasing
    do: (if prev-fst<=3, do: is_ok?(rest, fst, :dec), else: false)
  defp is_ok?([fst | _rest], prev, :dec) when fst > prev, do: false # we start to increase

  # direction is increasing
  defp is_ok?([fst | rest], prev, :inc) when fst > prev,
    do: (if fst-prev<=3, do: is_ok?(rest, fst, :inc), else: false) # we keep on increasing
  defp is_ok?([fst | _rest], prev, :inc) when fst < prev, do: false # we start to decrease

  # get all the sublists of lst with one element less than lst
  # A.sublists([1,2,3,4]) => [[1, 2, 3], [1, 2, 4], [1, 3, 4], [2, 3, 4]]
  def sublists(lst), do: sublists([], lst, [])
  def sublists(_pre, [], acc), do: acc
  def sublists(pre, [a | rest], acc), do: sublists([a | pre], rest, [(Enum.reverse(pre) ++ rest) | acc])
end

File.stream!("day02.txt")
|> Stream.map(fn line -> String.split(line) |> Enum.map(&String.to_integer/1) |> A.is_ok?() end)
|> Enum.count(&Function.identity/1)
|> IO.inspect(label: "part 1")

File.stream!("day02.txt")
|> Stream.map(fn line ->
  lst = String.split(line) |> Enum.map(&String.to_integer/1)
  Enum.any?([lst | A.sublists(lst)], fn lst -> A.is_ok?(lst) end)
end)
|> Enum.count(&Function.identity/1)
|> IO.inspect(label: "part 2")
2 Likes

Part1:

defmodule Advent.Y2024.Day02.Part1 do
  def run(puzzle) do
    puzzle |> parse() |> Enum.count(&safe?/1)
  end

  def parse(puzzle) do
    for line <- String.split(puzzle, "\n") do
      line |> String.split(" ") |> Enum.map(&String.to_integer/1)
    end
  end

  def safe?(report) do
    with true <- report == Enum.sort(report) || report == Enum.sort(report, :desc),
         chunks <- Enum.chunk_every(report, 2, 1, :discard) do
      Enum.all?(chunks, fn [a, b] -> abs(a - b) in 1..3 end)
    else
      _ -> false
    end
  end
end

Part2

defmodule Advent.Y2024.Day02.Part2 do
  alias Advent.Y2024.Day02.Part1

  def run(puzzle) do
    puzzle |> Part1.parse() |> Enum.count(&almost_safe?/1)
  end

  def almost_safe?(report) do
    Enum.any?(
      0..(length(report) - 1),
      &(report |> List.delete_at(&1) |> Part1.safe?())
    )
  end
end

2 Likes

in Elixir, by convention, predicate functions (functions that return trueor false) are named ok? (not is_ok? which is redundant)

Still, this is elegant recursive code :+1:

2 Likes

Here is mine. :alien:

defmodule Aoc2024.Solutions.Y24.Day02 do
  alias AoC.Input

  def parse(input, _part) do
    input
    |> Input.stream!()
    |> Enum.map(fn line ->
      Enum.map(String.split(line, " "), fn part ->
        elem(Integer.parse(part), 0)
      end)
    end)
  end

  def part_one(problem) do
    Enum.reduce(problem, 0, fn [n1, n2 | _] = line, acc ->
      acc + check_levels(line, n1 < n2)
    end)
  end

  def part_two(problem) do
    Enum.reduce(problem, 0, fn line, acc ->
      Enum.reduce_while(0..(length(line) - 1), 0, fn i, acc ->
        [n1, n2 | _] = line = List.delete_at(line, i)

        case check_levels(line, n1 < n2) do
          1 -> {:halt, 1}
          0 -> {:cont, acc}
        end
      end) + acc
    end)
  end

  defp check_levels([_], _up?), do: 1

  defp check_levels([n1, n2 | rest], true) when n1 < n2 do
    if n2 - n1 <= 3, do: check_levels([n2 | rest], true), else: 0
  end

  defp check_levels([n1, n2 | rest], false) when n1 > n2 do
    if n1 - n2 <= 3, do: check_levels([n2 | rest], false), else: 0
  end

  defp check_levels(_, _), do: 0
end

Bench

Using default year: 2024
Using default day: 2
Solution for 2024 day 2
part_one: 326 in 8.63ms
part_two: 381 in 2.74ms

edit: Weird that part two is faster :thinking:

Part 1 is a recursive mess but it is super fast (30ÎĽs)

def part1(reports) do
  Enum.count(reports, &check_report/1)
end

def check_report([a, b | _]) when abs(b - a) not in [1, 2, 3], do: false
def check_report([a, b | rest]), do: check_report([b | rest], b - a)
def check_report([_], _dir), do: true

def check_report([a, b | rest], dir) do
  new_dir = b - a

  if ((dir < 0 and new_dir < 0) or (dir > 0 and new_dir > 0)) and abs(new_dir) <= 3 do
    check_report([b | rest], new_dir)
  else
    false
  end
end

Part 2 iterates over all the possibilities using part 1

def part2(reports) do
  Enum.count(reports, fn report ->
    Enum.find_value(0..(length(report) - 1), false, fn dampen ->
      report |> List.delete_at(dampen) |> check_report()
    end)
  end)
end
1 Like

Here’s my attempt - new to Elixir for AOC 24

defmodule Aoc2024.Day2 do

  # opts = [headers: [{"cookie", "session=#{System.fetch_env!("AOC_SESSION_COOKIE")}"}]]
  # input = Req.get!("https://adventofcode.com/2024/day/2/input", opts).body

  def part_1(input) do
    input 
      |> String.split("\n", trim: true)
      |> Enum.map(&convert_to_int_list/1)
      |> Enum.map(&check_report/1)
      |> Enum.count(fn s -> elem(s, 0) == :safe end)     
  end

  def part_2(input) do
    input 
      |> String.split("\n", trim: true)
      |> Enum.map(&convert_to_int_list/1)
      |> Enum.map(&safe_combinations?/1)
      |> Enum.count(fn s -> s == true end)
  end

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

  defp safe_combinations?(reading) do
    [reading | (for idx <- (0..length(reading) - 1), do: List.delete_at(reading, idx))]
    |> Enum.map(&check_report/1)
    |> Enum.any?(fn s -> elem(s, 0) == :safe end)
  end

  defp check_report(reading) do
    reading
    |> Enum.reduce({:unknown}, fn x, acc -> 
      case acc do
        {:unsafe} -> 
          {:unsafe}
        {:unknown} -> 
          {:safe, :unknown, x}
        {:safe, :unknown, prev} when prev < x and abs(prev - x) < 4  ->
          {:safe, :increasing, x}
        {:safe, :unknown, prev} when prev > x and abs(prev - x) < 4 ->
          {:safe, :decreasing, x}
        {:safe, :increasing, prev} when prev < x and abs(prev - x) < 4 ->
          {:safe, :increasing, x}
        {:safe, :decreasing, prev} when prev > x and abs(prev - x) < 4 ->
          {:safe, :decreasing, x}
        _ ->
          {:unsafe}            
      end    
      end)
  end
  
end
1 Like

I am going to learn from your code. I tried something similar but failed (got 15 reports lower), I am guessing I missed one of the {:safe, :unknown} case. Thanks for sharing.

EDIT: Nevermind, looks like I didn’t do it like this, I had used a fold with idx list to combine for the cases. But this code gave me an idea any way :slight_smile:

Part 1:

def part1(file), do: file |> file_to_lists_of_ints |> Enum.count(&safe?/1)

def safe?(line) do
  pairs = Enum.chunk_every(line, 2, 1, :discard)
  [a, [h | t]] = pairs |> Enum.map(fn [x, y] -> [abs(y - x), y > x] end) |> Enum.zip_with(& &1)
  Enum.all?(a, &(0 < &1 and &1 < 4)) and Enum.all?(t, &(&1 == h))
end

Part 2:

def part2(file) do
  file
  |> file_to_lists_of_ints
  |> Enum.count(fn line -> line |> drop1 |> Enum.any?(&safe?/1) end)
end

def drop1(l), do: for(i <- 0..(length(l) - 1), do: List.delete_at(l, i))

Boilerplate:

def file_to_lists_of_ints(file) do
  file |> File.read!() |> String.trim() |> String.split("\n") |> Enum.map(&String.to_integer/1)
end

EDIT: bit o’ code golf (same approach, fewer defs).

Just brute forcing all the possibilities…

1 Like

After looking at some other replies, I can use a range to whittle my safe? down more:

def safe?(line) do
  [h | t] = line |> Enum.chunk_every(2, 1, :discard) |> Enum.map(fn [x, y] -> y - x end)
  Enum.all?([h | t], &(abs(&1) in 1..3)) and Enum.all?(t, &(&1 > 0 == h > 0))
end

I too tried to find a way not to brute force it, but in the end caved to brute force:

After way too many hours at least i’ve got a working solution to part a.
It’s really hard for me to unlearn what i learned from non functional languages over the years and i see that i overcomplicate things that can be written way more elegant. But hey, i’m learning the language for like three days now and AoC gives me a lot of opportunity for trial and error (And watching other solutions to learn more about the standard library - i need to have a look at the reduce functions!)

Today i learned: cond - I was getting incorrect results because i tried to rebuild something “if… else if… else”-alike.

defmodule Aoc2024.Day2 do
  def solve_a do
    reports =
      File.stream!("input")
      |> Enum.map(&String.trim(&1, "\n"))
      |> Enum.map(&String.split/1)

    check_a(reports, 0, 0)
  end

  @doc "Execute the checks recusively for each report"
  def check_a([h | t], s, u) do

    current_report =
      Enum.map(h, fn(x) -> String.to_integer(x) end)

    if safe?(current_report) do
      check_a(t, s+1, u)
    else
      check_a(t, s, u+1)
    end
  end
  def check_a([], safe, unsafe) do
    IO.puts "Safe: #{Integer.to_string(safe)} - Unsafe: #{Integer.to_string(unsafe)}"
  end

  def safe?([h1, h2 | t] = report) do
    if difference_safe?(h1, h2 , t) do
      if continuous?(report, :init) do
        true
      else
        false
      end
    else
      false
    end
  end

  def safe?([_ | []]) do
    true
  end

  def difference_safe?(a, b, [ h | t ]) do
    case abs(a - b) <= 3 do
      true -> difference_safe?(b, h, t)
      false -> false
    end
  end

  def difference_safe?(a, b, []) do
    case abs(a - b) <= 3 do
      true -> true
      false -> false
    end
  end

  def continuous?([a, b | t] = report, direction) do
    cond do
      a > b ->
        case direction do
          :asc ->  continuous?([b | t], :asc)
          :init ->  continuous?([b | t], :asc)
          _ ->  false
        end
      a < b ->
        case direction do
          :desc -> continuous?([b | t], :desc)
          :init -> continuous?([b | t], :desc)
          _ ->  false
        end
      a == b ->
        false
    end
  end

  def continuous?([ _ ], _) do
    true
  end
end

Let’s see if i can get my head around part 2 today, too :grinning:

My second day solution took me much longer than necessary.

I managed to get most of it done between getting the kids to the bus and having my first meeting of the day. Though part B was off… And throughout the day, I took some minutes between meetings or during breaks to try to fix this.

Now, 12 hours after I have started the AoC day, I found a working solution, and in hindsight, my previous iterations, either skipped the variant with only the first or the last level, which lead to skewed results :slightly_frowning_face:

1 Like

I’ve ended up doing something that stops computation asap instead of brute force all possibilities.

defmodule D2 do
  def p1(file) do
    file
    |> reports()
    |> Stream.filter(&safe_report_p1?/1)
    |> Enum.count()
  end

  defp reports(file) do
    file
    |> File.stream!(:line)
    |> Stream.map(fn line ->
      line |> String.split() |> Enum.map(&String.to_integer/1)
    end)
  end

  def safe_report_p1?(report) do
    safe_report_p1?(report, :asc) or safe_report_p1?(report, :desc)
  end

  def safe_report_p1?([], _) do
    true
  end
  def safe_report_p1?([_], _) do
    true
  end
  def safe_report_p1?([a, b | rest], :asc) when a < b and b - a <= 3 do
    safe_report_p1?([b | rest], :asc)
  end
  def safe_report_p1?([a, b | rest], :desc) when a > b and a - b <= 3 do
    safe_report_p1?([b | rest], :desc)
  end
  def safe_report_p1?(_, _) do
    false
  end

  def p2(file) do
    file
    |> reports()
    |> Stream.filter(&safe_report_p2?/1)
    |> Enum.count()
  end

  def safe_report_p2?(report) do
    safe_report_p2?([], report, :asc) or safe_report_p2?([], report, :desc)
  end

  def safe_report_p2?(_, [], _) do
    true
  end
  def safe_report_p2?(_, [_], _) do
    true
  end
  def safe_report_p2?(scanned, [a, b | rest], :asc) when a < b and b - a <= 3 do
    safe_report_p2?(scanned ++ [a], [b | rest], :asc)
  end
  def safe_report_p2?(scanned, [a, b | rest], :desc) when a > b and a - b <= 3 do
    safe_report_p2?(scanned ++ [a], [b | rest], :desc)
  end
  def safe_report_p2?(scanned, [a, b | rest], asc_or_desc) do
    safe_report_p1?(scanned ++ [a | rest], asc_or_desc) or
      safe_report_p1?(scanned ++ [b | rest], asc_or_desc)
  end
  def safe_report_p2?(_, _, _) do
    false
  end
end
2 Likes