Here is my solution for day 2 of Advent of Code:
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.
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.
No optimization here, just building all possible list before trying them one by one
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.
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)
#!/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")
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
in Elixir, by convention, predicate functions (functions that return true
or false
) are named ok?
(not is_ok?
which is redundant)
Still, this is elegant recursive code
Here is mine.
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
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
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
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
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 def
s).
Just brute forcing all the possibilities…
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
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
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