Advent of Code 2024 - Day 1

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

defmodule Day01 do def part1(input) do all = parse(input) {first, second} = Enum.unzip(all) Enum.zip([Enum.sort(first), Enum.sort(second)]) |> Enum.map(fn {a, b} -> abs(a - b) end) |> Enum.sum end def part2(input) do all = parse(input) {first, second} = Enum.unzip(all) frequencies = Enum.frequencies(second) first |> Enum.map(fn n -> n * Map.get(frequencies, n, 0) end) |> Enum.sum end defp parse(input) do input |> Enum.map(fn line -> {first, line} = Integer.parse(line) line = String.trim(line) {second, ""} = Integer.parse(line) {first, second} end) end end 
7 Likes

I did it in F# and now that I see your Elixir code, mine is exactly the same code (in a different language lol)

The three spaces was this years twone for me and cost me a minute.

2 Likes

Happy Advent Of Code everyone! :christmas_tree: :partying_face: :nerd_face:

Part1:

def run(puzzle) do puzzle |> parse() |> then(fn {l1, l2} -> Enum.zip(Enum.sort(l1), Enum.sort(l2)) end) |> Enum.reduce(0, fn {n1, n2}, acc -> acc + abs(n1 - n2) end) end def parse(puzzle) do puzzle |> String.split("\n") |> Enum.reduce({[], []}, fn line, {acc1, acc2} -> [n1, n2] = String.split(line, " ") {[to_integer(n1) | acc1], [to_integer(n2) | acc2]} end) end 

Part2:

def run(puzzle) do {l1, l2} = Part1.parse(puzzle) l2_frequencies = Enum.frequencies(l2) Enum.reduce(l1, 0, fn n, acc -> acc + n * Map.get(l2_frequencies, n, 0) end) end 
1 Like

Pretty fun despite it’s simplicity, but the prompts are getting looong:

defmodule AOC.Y2024.Day1 do @moduledoc false use AOC.Solution @impl true def load_data() do Data.load_day(2024, 1) |> Enum.map(&String.split(&1, ~r/\s+/, trim: true)) |> Enum.map(fn [a, b] -> {String.to_integer(a), String.to_integer(b)} end) |> Array.transpose() end @impl true def part_one([l, r]) do l |> Enum.sort() |> Enum.zip(Enum.sort(r)) |> General.map_sum(fn {a, b} -> abs(a - b) end) end @impl true def part_two([l, r]) do Enum.frequencies(r) |> then(fn freq -> General.map_sum(l, fn a -> a * Map.get(freq, a, 0) end) end) end end 

Github: aoc/lib/aoc/y2024/day_1.ex at main · woojiahao/aoc · GitHub (made a whole bunch of utility modules and functions from past years, to reduce code duplication)

3 Likes

Here are my solutions using Livebook:

3 Likes

Starting very simple, but Elixir shines :slight_smile:

It’s basically @bjorng 's solution.

defmodule AdventOfCode.Solutions.Y24.Day01 do alias AoC.Input def parse(input, _part) do input |> Input.stream!(trim: true) |> Enum.map(&parse_line/1) |> Enum.unzip() end defp parse_line(line) do [a, b] = String.split(line, " ", trim: true) {a, ""} = Integer.parse(a) {b, ""} = Integer.parse(b) {a, b} end def part_one(problem) do {left, right} = problem left = Enum.sort(left) right = Enum.sort(right) Enum.zip_with(left, right, fn a, b -> abs(a - b) end) |> Enum.sum() end def part_two(problem) do {left, right} = problem freqs = Enum.frequencies(right) left |> Enum.map(fn a -> a * Map.get(freqs, a, 0) end) |> Enum.sum() end end 
1 Like

It’s the most wonderful time of the year!

3 Likes

Part 1:

[_ | rest] = all = puzzle_input |> String.split(["\n", " "], trim: true) |> Enum.map(& String.to_integer/1) left_sorted = Enum.take_every(all, 2) |> Enum.sort() right_sorted = Enum.take_every(rest, 2) |> Enum.sort() Enum.zip_reduce([left_sorted, right_sorted], 0, fn [a, b], acc -> acc + abs(a - b) end) 

Part 2:

[_ | rest] = all = puzzle_input |> String.split(["\n", " "], trim: true) |> Enum.map(& String.to_integer/1) left = Enum.take_every(all, 2) right_frequencies = Enum.take_every(rest, 2) |> Enum.frequencies() left |> Enum.reduce(0, fn a, acc -> acc + (a * Map.get(right_frequencies, a, 0)) end) 
1 Like

Are we not supposed to also include code for opening and reading the input from a file? Or we can opt to just put it in a module attribute and parse that?

Well, I used KinoAOC — KinoAOC v0.1.7 for Livebook, and it binds the input to puzzle_input automatically :slight_smile:

1 Like

I made my own utility modules and functions because I’ve been using Elixir for the past 3-4 AoCs so I’ve accumulated use cases. The goal of posts should be the actual solution, it’s less important where the input comes from!

First time trying to do anything in elixir. I’m still learning in early stage and sometimes it twist my mind to be able to express what i want to do. I think i overcomplicated it a lot, but at least got the correct result :smiley:

defmodule Aoc2024.Day1 do def getresult do {:ok, contents} = File.read("input") # [ %{left: 40094, right: 37480}, %{left: 52117, right: 14510}, ... ] itemlist = contents |> String.split("\n") |> Enum.map(fn(line) -> splitlist(line) end) # Sorted list with only left values left_items = itemlist |> Enum.sort(fn(i1, i2) -> order_items(i1, i2, :left) end) |> Enum.map(fn(i) -> i.left end) # same for the right values right_items = itemlist |> Enum.sort(fn(i1, i2) -> order_items(i1, i2, :right) end) |> Enum.map(fn(i) -> i.right end) # List of diff of each left and right value at the same list position difflist = Enum.zip(left_items, right_items) |> Enum.map(fn{li, ri} -> abs(li - ri) end) # sum the values via recursive add add_values(difflist, 0) end defp splitlist(line) do [left , right] = line |> String.split(" ", trim: true) %{left: String.to_integer(left) , right: String.to_integer(right)} end defp order_items(i1, i2, side) do case side do :left -> i1.left <= i2.left :right -> i1.right <= i2.right end end defp add_values([h | t], result) do h + add_values(t, result) end defp add_values([], result) do result end end IO.puts(Integer.to_string(Aoc2024.Day1.getresult)) 
2 Likes

Yup, quite normal, just asked in case I missed something. Thanks.

My crummy attempt today!
As I progress, I suspect I will extract common functions out into a shared module, like absolute_difference.

Here’s mine - I did it in Livebook which was a real help, coming from a python/F#-ish world:

# ── Setup ── fname = "/Users/marksmith/Downloads/AOC24/Day01/input.txt" lines = File.stream!(fname) |> Enum.map(&String.split/1) # ── Part 1 ── {left, right} = lines |> Enum.map(&List.to_tuple/1) |> Enum.map( fn {first,second} -> {String.to_integer(first), String.to_integer(second)} end) |> Enum.unzip() left = Enum.sort(left) right = Enum.sort(right) first_answer = Enum.zip(left, right) |> Enum.map(fn {l,r} -> abs(l-r) end) |> Enum.sum() # ── Part 2 ── right_freqs = Enum.frequencies(right) defmodule Etc do def match_val(x, rf) do {_, freq} = Enum.find(rf, {x, 0}, fn {a,_} -> a == x end) freq end end second_answer = left |> Enum.map(fn x -> {x, Etc.match_val(x, right_freqs)} end) |> Enum.map(fn {x,y} -> x * y end) |> Enum.sum() 

There’s probably a more compact way of doing the unzip/sort/rezip bit in Part1 but I wanted to be able to understand it when I looked back on it :slight_smile: .

For Part 2 I struggled to extract the second element of a tuple having just found it, so it ended up in a function that returned the second element. I wanted to write that as an anonymous function but struggled with the syntax so went with the defmodule/def approach.

2 Likes

Here’s mine. Much more coding lines than I’d like but I guess my work bleeds into everything. I like small functions and separation of concerns and doctests and typespecs. :person_shrugging:

defmodule Day01 do @type id :: non_neg_integer() @type pair :: {id(), id()} @type pairs :: [pair()] @type distance :: non_neg_integer() @type similarity :: non_neg_integer() @spec parse_line(String.t()) :: pair() def parse_line(line) do line |> String.trim() |> String.split(~r/\s+/) |> Enum.map(fn text -> {number, ""} = Integer.parse(text) number end) |> List.to_tuple() end @spec parse_input(String.t()) :: [pair()] def parse_input(input) do input |> String.split("\n") |> Enum.reject(fn line -> line == "" end) |> Enum.map(&parse_line/1) end @spec sum_of_distances(pairs()) :: distance() def sum_of_distances(input) do {left, right} = Enum.unzip(input) left = Enum.sort(left) right = Enum.sort(right) left |> Enum.zip(right) |> Enum.map(fn {a, b} -> abs(a - b) end) |> Enum.sum() end # @spec similarity_score(pairs()) :: similarity() def similarity_score(input) do {left, right} = Enum.unzip(input) frequencies = Enum.frequencies(right) left |> Enum.map(fn n -> n * Map.get(frequencies, n, 0) end) |> Enum.sum() end @doc ~S""" iex> Day01.part_1("3 4\n4 3\n2 5\n1 3\n3 9\n3 3\n") 11 """ @spec part_1(String.t()) :: distance() def part_1(input \\ input()) do input |> parse_input() |> sum_of_distances() end @doc ~S""" iex> Day01.part_2("3 4\n4 3\n2 5\n1 3\n3 9\n3 3\n") 31 iex> Day01.part_2("3 4\n4 3\n2 3\n1 3\n3 9\n3 3\n") 40 """ @spec part_2(String.t()) :: similarity() def part_2(input \\ input()) do input |> parse_input() |> similarity_score() end def input(), do: File.read!("input/day_01.txt") end 

Since I did not want to cheat I only checked the solutions of others post factum and interestingly enough mine is almost identical to @bjorng.

1 Like

When parsing I made sure to re-use the match context:

❯ ERL_COMPILER_OPTIONS=bin_opt_info mix compile --force Compiling 1 file (.ex) warning: OPTIMIZED: match context reused │ 38 │ parse(rest, {[first_number | left], [second_number | right]}) │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ │ └─ lib/aoc24.ex:38 Generated aoc_24 app 
defmodule Aoc24 do def day_1_1() do {left, right} = parse(File.read!("./input_day_1.txt"), {[], []}) Enum.zip_reduce(Enum.sort(left), Enum.sort(right), 0, fn left, right, acc -> abs(left - right) + acc end) end def day_1_2() do {left, right} = parse(File.read!("./input_day_1.txt"), {[], []}) frequencies = Enum.frequencies(right) Enum.reduce(left, 0, fn number, score -> number * Map.get(frequencies, number, 0) + score end) end @new_line "\n" @spaces " " defp parse(<<>>, acc), do: acc defp parse( <<first::binary-size(5), @spaces, second::binary-size(5), @new_line, rest::binary>>, {left, right} ) do first_number = String.to_integer(first) second_number = String.to_integer(second) parse(rest, {[first_number | left], [second_number | right]}) end end 
1 Like

Part 1:

file |> file_to_parsed_lines(&parse_line/1) |> Enum.zip_with(&Enum.sort/1) |> Enum.zip_reduce(0, fn [a, b], acc -> acc + abs(a - b) end) 

Part 2:

[left, right] = file |> file_to_parsed_lines(&parse_line/1) |> Enum.zip_with(&Enum.frequencies/1) Enum.reduce(left, 0, fn {k, v}, sum -> sum + k * v * Map.get(right, k, 0) end) 

Boilerplate:

def file_to_parsed_lines(file, parse_fun) do file |> File.read!() |> String.trim() |> String.split("\n") |> Enum.map(parse_fun) end def parse_line(line), do: line |> String.split() |> Enum.map(&String.to_integer/1) 
1 Like

Here is mine. Not sure how optimal it is, didn’t use frequencies. :cowboy_hat_face:

defmodule Aoc2024.Solutions.Y24.Day01 do alias AoC.Input def parse(input, _part) do input |> Input.stream!() |> Enum.reduce({[], []}, fn line, acc -> {acc1, acc2} = acc {num1, num2} = parse_line(line) {[num1 | acc1], [num2 | acc2]} end) end def part_one({list1, list2}) do Enum.sort(list1) |> Stream.zip(Enum.sort(list2)) |> Enum.reduce(0, fn {num1, num2}, acc -> abs(num1 - num2) + acc end) end def part_two({list1, list2}) do list2 = Enum.sort(list2) list1 |> Enum.sort() |> Enum.reduce(0, fn num, acc -> count_number(list2, num) * num + acc end) end defp parse_line(line) do line |> String.trim() |> String.split() |> then(fn [num1, num2] -> {num1, _} = Integer.parse(num1) {num2, _} = Integer.parse(num2) {num1, num2} end) end defp count_number(list, num) do Enum.reduce_while(list, 0, fn e, acc -> cond do e < num -> {:cont, acc} e == num -> {:cont, acc + 1} true -> {:halt, acc} end end) end end 

Using aoc lib.

Using default year: 2024 Using default day: 1 Solution for 2024 day 1 part_one: 1580061 in 44.31ms part_two: 23046913 in 32.65ms 
1 Like

I solved it shortly after lunch today, and after skipping last year, I had quite a fight getting back into my boilerplate framework.

Anyway it’s been a nice warmup exercise and I’m looking forward for the next exercises.