Advent of Code 2023 - Day 15

Got top 1000 for part 1, part 2 was super long so I took a while to understand it. Pretty fun day overall!

1 Like

My solution:

import AOC

aoc 2023, 15 do
  def p1(input) do
    input |> String.split(",", trim: true) |> Enum.map(&hash/1) |> Enum.sum()
  end

  def hash(string) do
    string
    |> to_charlist()
    |> Enum.reduce(0, fn ascii, hash -> rem((hash + ascii) * 17, 256) end)
  end

  def p2(input) do
    input
    |> String.split(",", trim: true)
    |> Enum.reduce(%{},
      fn command, boxes ->
        if String.contains?(command, "=") do
          [target, n] = String.split(command, "=")
          box = hash(target)
          n = String.to_integer(n)
          stack = Map.get(boxes, box, [])
          index = Enum.find_index(stack, fn {label, _} -> label == target end)
          stack = if is_nil(index) do
            [{target, n} | stack]
          else
            List.replace_at(stack, index, {target, n})
          end
          Map.put(boxes, box, stack)
        else
          target = String.trim(command, "-")
          box = hash(target)
          stack = Map.get(boxes, box, [])|> Enum.reject(fn {label, _} -> label == target end)
          Map.put(boxes, box, stack)
        end
      end)
    |> Enum.flat_map(fn {box, stack} -> stack |> Enum.reverse() |> Enum.with_index(1) |> Enum.map(fn {{_, focal}, slot} -> focal * slot * (box+1) end) end)
    |> Enum.sum()
  end
end

Not pretty, but it works

defmodule Day15 do
  def hash_str(s) do
    for ascii <- s |> String.to_charlist(),
        reduce: 0 do
      acc -> rem((acc + ascii) * 17, 256)
    end
  end

  def put_lens_in_box(lens, []) do
    [lens]
  end

  def put_lens_in_box([lens_id, length], lenses) do
    contains_lens? = lenses |> Enum.any?(fn [lens, _] -> lens === lens_id end)

    if contains_lens? do
      lenses
      |> Enum.map(fn [lens, len] ->
        if lens == lens_id do
          [lens, length]
        else
          [lens, len]
        end
      end)
    else
      lenses ++ [[lens_id, length]]
    end
  end

  def handle_instruction({:add, [lens_id, length], dest}, state) do
    lenses = Map.get(state, dest, [])
    lenses = put_lens_in_box([lens_id, length], lenses)
    state |> Map.put(dest, lenses)
  end

  def handle_instruction({:rem, [lens], dest}, state) do
    lenses = Map.get(state, dest, [])
    lenses = lenses |> Enum.filter(fn [bx_lens, _] -> bx_lens !== lens end)
    state |> Map.put(dest, lenses)
  end

  def solve_1() do
    {_, content} = File.read("./input/2023_15.txt")
    for command <- content |> String.trim() |> String.split(","), reduce: 0 do
      acc -> acc + hash_str(command)
    end |> IO.puts()
  end

  def solve_2() do
    {_, content} = File.read("./input/2023_15.txt")
    for {instruction, arg} <-
          [content]
          |> Stream.flat_map(fn s ->
            s
            |> String.trim()
            |> String.split(",")
          end)
          |> Stream.map(fn l ->
            case String.contains?(l, "=") do
              true ->
                [lens, length] = String.split(l, "=")
                {:add, [lens, length |> String.to_integer()]}

              false ->
                {:rem, [String.split(l, "-") |> Enum.at(0)]}
            end
          end),
        reduce: %{} do
      acc ->
        dest = hash_str(arg |> Enum.at(0))
        handle_instruction({instruction, arg, dest}, acc)
    end
    |> Enum.reduce(
      0,
      fn {box_id, lenses}, outer_acc ->
        outer_acc +
          for {[_, fcl_len], idx} <- lenses |> Enum.with_index(), reduce: 0 do
            acc ->
              acc + (box_id + 1) * (idx + 1) * fcl_len
          end
      end
    )
    |> IO.puts()
  end
end

This one was a bit tedious but easy :smiley:

My beginner’s solution, Day 15 part 1. Lens Library

defmodule Day15 do

  def part1(input) do
    parse(input)
    |> Enum.map(&hash/1)
    |> Enum.sum
  end

  def hash(string) do
    for <<char::8 <- string>>, reduce: 0 do
      acc -> rem(17 * (acc + char), 256)
    end
  end

  def parse(raw_sequence) do
    raw_sequence
    |> remove("\n") 
    |> String.split(",")
  end
  defp remove(s, p), do: String.replace(s, p, "")

end
1 Like

This was quite fun. Although I was lazy and used Aja.OrdMap

defmodule AdventOfCode.Y2023.Day15 do
  alias AdventOfCode.Helpers.InputReader
  alias Aja.OrdMap

  def input, do: InputReader.read_from_file(2023, 15)

  def run(input \\ input()) do
    input = parse_1(input)
    {run_1(input), run_2(input)}
  end

  defp run_1(input), do: Enum.reduce(input, 0, &(&2 + hash(&1)))

  defp run_2(input) do
    input
    |> parse_2()
    |> Enum.reduce(%{}, fn
      {op, b, len}, acc ->
        hash = hash(b)

        case op do
          :add -> Map.update(acc, hash, OrdMap.new(%{b => len}), &OrdMap.put(&1, b, len))
          :remove -> Map.update(acc, hash, OrdMap.new(%{}), &OrdMap.drop(&1, [b]))
        end
    end)
    |> total_focus()
  end

  defp total_focus(map) do
    Enum.reduce(map, 0, fn {box, boxes}, acc ->
      acc +
        (boxes
         |> OrdMap.to_list()
         |> Enum.with_index(1)
         |> Enum.reduce(0, &(&2 + row_focus(&1, box))))
    end)
  end

  defp row_focus({{_, len}, slot}, box), do: (box + 1) * slot * len

  def parse_1(input \\ input()), do: String.split(input, ",", trim: true)

  def parse_2(commands) do
    Enum.map(commands, fn cmd ->
      case Regex.run(~r{([a-zA-Z]+)(=|-)([0-9]*)}, cmd) do
        [_, label, "=", len] -> {:add, label, String.to_integer(len)}
        [_, label, "-", ""] -> {:remove, label, 0}
      end
    end)
  end

  def hash(lst) do
    for ch <- String.to_charlist(lst), reduce: 0 do
      acc -> rem((acc + ch) * 17, 256)
    end
  end
end

This one seemed rather straightforward to me, no fancy algorithms or optimisations. My solution for part 2 does feel a bit clumsy in terms of list handling, but it works.

defmodule Main do
  def hash(s), do: s |> String.to_charlist() |> Enum.reduce(0, fn c, h -> rem(17*(c+h),256) end)

  def solve1(ls), do: ls |> Enum.map(&hash/1) |> Enum.sum()

  def parse(i) do
    [l,a,n] = Regex.run(~r/^([a-z]+)([-=])(\d*)$/,i) |> tl()
    {l,hash(l),a,if n == "" do 0 else String.to_integer(n) end}
  end

  def solve2(ls) do
    ls |> Enum.map(&parse/1)
    |> Enum.reduce(%{}, fn {l,h,a,n}, d ->
          case a do
            "-" -> Map.update(d,h,[],&List.keydelete(&1,l,0))
            "=" -> Map.update(d,h,[{l,n}], &( if List.keymember?(&1,l,0) do
                          List.keyreplace(&1,l,0,{l,n})
                       else &1 ++ [{l,n}] end))
          end
        end)
    |> Enum.into([])
    |> Enum.map(fn {h,bxl} ->
          bxl |> Enum.with_index(1) |> Enum.map(fn {{_,p},i} -> (h+1)*i*p end) end)
    |> Enum.concat() |> Enum.sum()
  end

  def run() do
    get_input()
    # |> solve1()
    |> solve2()
	end

  def get_input() do
    # "testinput15"
    "input15"
    |> File.read!() |> String.trim() |> String.split(",")
  end
  
end

:timer.tc(&Main.run/0)
|> IO.inspect(charlists: :as_lists)

This day was easy.

I used regex too, especially named captures:

%{"label" => label, "op" => op, "length" => length} =
      Regex.named_captures(~r/(?<label>[a-z]*)(?<op>(=|-))(?<length>[0-9]*)/, str)

imo it’s prettier than using String.contains? and String.split

           case String.contains?(str, "=") do
              true ->
                 ... = String.split(str, "=")
               ...

              false ->
                ... = String.split(str, "-")
                ...
            end

Question: I tend to always use Enum.reduce/3 as I find it convenient to introduce it into a pipeline but I see a lot of people using:

for x <- enum, reduce: 0
 acc -> ...
end

Can you tell me why do you prefer this syntax? No judgement, I’m just curious… :wink:

The problem was really straightforward on both parts for this day. Makes me sad that I got so far behind on days 12-14. I didn’t do anything clever in my solution. This thread showed me some things I had not heard of. Namely, @code-shoily introducing Aja and @exists highlighting List.keydelete and List.keymember?. I wanted the theme this year to be binary pattern matching and manipulation as much as possible, but I didn’t want to overcomplicate this one and just used the String functions mostly.

defmodule Part1 do
    def solve(input) do
      input
      |> parse()
      |> Enum.sum()
    end

    defp parse(input) do
      hasher(input, 0, [])
    end

    def hasher(<<>>, current_val, acc), do: [current_val | acc]
    def hasher(<<",", rest::binary>>, current_val, acc), do: hasher(rest, 0, [current_val | acc])
    def hasher(<<"\n", rest::binary>>, current_val, acc), do: hasher(rest, current_val, acc)

    def hasher(<<next::8, rest::binary>>, current_val, acc),
      do: hasher(rest, hash_fun(next, current_val), acc)

    def hash_fun(next, current_val), do: rem((next + current_val) * 17, 256)
  end

  defmodule Part2 do
    def solve(input) do
      input
      |> parse()
      |> Enum.map(&calc_focus_pwr/1)
      |> total_focus_pwr()
    end

    defp parse(input) do
      input
      |> String.replace("\n", "")
      |> String.split(",")
      |> Enum.map(&parse_label/1)
      |> Enum.reduce(%{}, fn ops_label, map ->
        hash_mapper(ops_label, map)
      end)
    end

    defp parse_label(label) do
      String.split(label, ~r{-|=}, include_captures: true)
    end

    defp hash_mapper([label, op, len], map) do
      [box] = Part1.hasher(label, 0, [])

      case op do
        "-" ->
          Map.update(map, box, [], fn curr ->
            Enum.reject(curr, fn lens -> match?([^label, _], lens) end)
          end)

        "=" ->
          Map.update(map, box, [[label, len]], fn curr ->
            with nil <- Enum.find_index(curr, fn [a, _b] -> a == label end) do
              [[label, len] | curr]
            else
              ndx ->
                List.replace_at(curr, ndx, [label, len])
            end
          end)
      end
    end

    defp calc_focus_pwr({box, lenses}) do
      lenses
      |> Enum.reverse()
      |> Enum.with_index(1)
      |> Enum.map(fn {[_label, lens], ndx} -> String.to_integer(lens) * ndx * (box + 1) end)
      |> Enum.sum()
    end

    defp total_focus_pwr(enum), do: Enum.sum(enum)
  end
1 Like

Like you I mostly use Enum.reduce. When I do reach for the for comprehension syntax it’s usually to take advantage of how easy it is to combine multiple enumerables into one logic pipeline.

a = [1, 2, 3]
b = [?a, ?b, ?c]
for i <- a, c <- b, i + c == 100, reduce: 0 do 
  acc -> acc + i + c
end
# 300

versus

a
|> Enum.reduce(0, fn i, acc -> 
        b 
        |> Enum.filter(fn c -> c + i == 100 end) 
        |> Enum.reduce(acc, fn c, accacc -> 
               accacc + c + i end)
     end)
# 300

I suspect it also may seem more familiar to people more comfortable with imperative programming approaches. Finally, there might be some cases where the for comprehension does provide some performance improvements as per this discussion.

Thanks Steven for you reply

Syntax familiarity for people coming from the imperative world was the only one thing that came up to my mind.

Thanks for the link to the discussion. I haven’t read it before. I didn’t know for was more efficient than Enum. I’ll try to use it more often then. Advent of code is really nice, I always learn things when looking at the way others solved the same problem.