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.