igorb

igorb

For Part 1, I was lazy and didn’t want to maintain variables and pass them down to each function (which would also involve merging two different histories after each split), so I used the process dictionary trick I learned about in earlier days from @Aetherus to represent state and have the functions produce side effects (which I thought was ugly). For Part 2, though, I was quite happy with this approach because then I just wrapped this code in Task.async_stream/2 and found the max :slight_smile:

https://github.com/ibarakaiev/advent-of-code-2023/blob/main/lib/advent_of_code/day_16.ex

Showing Posts 1 to 10

rugyoga

rugyoga

Pretty straightforward.
A MapSet to prevent loops and brute force.

import AOC

aoc 2023, 16 do
  def p1(input) do
    input
    |> Grid.parse()
    |> then(fn {max, grid} -> {max, Map.new(grid)} end)
    |> compute({{0, 0}, :east})
  end

  def compute(grid, start) do
    grid
    |> recurse([start], MapSet.new(), MapSet.new())
    |> Enum.count()
  end

  def p2(input) do
    {{rows, cols}, points} = input |> Grid.parse()
    grid = {{rows, cols}, Map.new(points)}

    (for row <- 0..(rows-1), do: [{{row, 0}, :east}, {{row, cols-1}, :west}] ++
    for col <- 0..(cols-1), do: [{{0, col}, :south}, {{rows-1, col}, :north}])
    |> List.flatten()
    |> Enum.map(&compute(grid, &1))
    |> Enum.max()
  end

  def recurse(_, [], energized, _), do: energized
  def recurse({_, map} = grid, [active | actives], energized, seen) do
    {active_pos, active_dir} = active
    if not MapSet.member?(seen, active) and Grid.in?(grid, active_pos) do
      energized_new = MapSet.put(energized, active_pos)
      seen_new = MapSet.put(seen, active)
      f = fn candidates -> candidates |> Kernel.++(actives) |> then(&recurse(grid, &1, energized_new, seen_new)) end
      item = map[active_pos]
      cond do
        item == "." or
        (item == "|" and active_dir in [:north, :south]) or
        (item == "-" and active_dir in [:east, :west]) -> f.([next(active)])
        item == "|" -> f.([next({active_pos, :north}), next({active_pos, :south})])
        item == "-" -> f.([next({active_pos, :west}), next({active_pos, :east})])
        item == "/" -> f.([next({active_pos, flip_sw(active_dir)})])
        item == "\\" -> f.([next({active_pos, flip_se(active_dir)})])
      end
    else
      recurse(grid, actives, energized, seen)
    end
  end

  def flip_sw(dir), do: %{north: :east, south: :west, west: :south, east: :north}[dir]
  def flip_se(dir), do: %{north: :west, south: :east, west: :north, east: :south}[dir]
  def next({{row, col}, :west}), do: {{row, col-1}, :west}
  def next({{row, col}, :east}), do: {{row, col+1}, :east}
  def next({{row, col}, :north}), do: {{row-1, col}, :north}
  def next({{row, col}, :south}), do: {{row+1, col}, :south}
end
woojiahao

woojiahao

TIL of ~w()a to generate a list of atoms instead of words, pretty cool!

https://github.com/woojiahao/aoc/blob/main/lib/aoc/y2023/day_16.ex

Aetherus

Aetherus

Not that interested in today’s puzzles.

Parsing input

grid =
  for {row, i} <- puzzle_input |> String.split() |> Stream.with_index(),
      {val, j} <- row |> String.to_charlist() |> Stream.with_index(),
      into: %{},
      do: {{i, j}, val}

Solutions for both parts

defmodule AoC2023.Day16 do
  def part1(grid) do
    shoot(grid, {0, 0}, {0, 1}, MapSet.new())
    |> count_energized()
  end

  def part2(grid) do
    {max_i, max_j} = grid |> Map.keys() |> Enum.max()

    count =
      0..max_i
      |> Enum.map(&{&1, 0})
      |> Task.async_stream(&count_energized(shoot(grid, &1, {0, 1}, MapSet.new())), ordered: false)
      |> Enum.max()

    count =
      0..max_i
      |> Enum.map(&{&1, max_j})
      |> Task.async_stream(&count_energized(shoot(grid, &1, {0, -1}, MapSet.new())), ordered: false)
      |> Enum.reduce(count, &max/2)

    count =
      0..max_j
      |> Enum.map(&{0, &1})
      |> Task.async_stream(&count_energized(shoot(grid, &1, {1, 0}, MapSet.new())), ordered: false)
      |> Enum.reduce(count, &max/2)


    count =
      0..max_j
      |> Enum.map(&{max_i, &1})
      |> Task.async_stream(&count_energized(shoot(grid, &1, {-1, 0}, MapSet.new())), ordered: false)
      |> Enum.reduce(count, &max/2)

    elem(count, 1)
  end

  defp count_energized(set) do
    set
    |> Enum.map(&elem(&1, 0))
    |> Enum.uniq()
    |> length()
  end

  defp shoot(grid, {i, j} = position, {di, dj} = direction, acc) do
    if {position, direction} in acc do
      acc
    else
      case grid[position] do
        nil ->
          acc
  
        ?. ->
          shoot(grid, {i + di, j + dj}, direction, MapSet.put(acc, {position, direction}))
  
        ?\\ ->
          shoot(grid, {i + dj, j + di}, {dj, di}, MapSet.put(acc, {position, direction}))
  
        ?/ ->
          shoot(grid, {i - dj, j - di}, {-dj, -di}, MapSet.put(acc, {position, direction}))
  
        ?- when di == 0 ->
          shoot(grid, {i + di, j + dj}, direction, MapSet.put(acc, {position, direction}))
  
        ?| when dj == 0 ->
          shoot(grid, {i + di, j + dj}, direction, MapSet.put(acc, {position, direction}))
  
        ?- ->
          acc = shoot(grid, {i, j - 1}, {0, -1}, MapSet.put(acc, {position, direction}))
          shoot(grid, {i, j + 1}, {0, 1}, MapSet.put(acc, {position, direction}))

        ?| ->
          acc = shoot(grid, {i - 1, j}, {-1, 0}, MapSet.put(acc, {position, direction}))
          shoot(grid, {i + 1, j}, {1, 0}, MapSet.put(acc, {position, direction}))
      end
    end
  end
end
lud

lud

Hello,

I was afraid that the second part would ask to rotate mirrors to create the maximum energy but thanks it was much easier.

I have no idea how to optimize that so I just simulate all the beams for part 2, and it takes more than one second. Any idea?

https://github.com/lud/adventofcode/blob/main/lib/solutions/2023/day16.ex

Aetherus

Aetherus

I did the same. It also took me more than 1 second to run, so I just parallelized them :sweat_smile:

lud

lud

Ah yes I generally do not use task async stream because I like to debug the outputs but now that it works, i’ll try it :smiley:

Edit: yeah, 500ms, good enough, thanks!

Also ordered: false comes in handy :smiley:

woojiahao

woojiahao

I believe it’s possible to memoize the paths given (row, column, direction). Since you’re just choosing a different starting position, but if the beam traveling left reaches \, it should still be the same path throughout. Though, I did not try this out so it could be false

pehbehbeh

pehbehbeh

Bruteforced part 2 without memoization… takes around 16 seconds in LiveBook on my M2 Pro. :person_shrugging:

https://github.com/pehbehbeh/adventofcode/blob/main/2023/16.livemd

seoulection

seoulection

Not sure if this is the right place to post this (and also probably a silly question), but how are y’all parsing the example input? It seems like the escape character is messing up the rows. I’m just doing a simple String.split(input, "\n"), but the lists/rows are not equal length.

Where Next? Top

Trending in Challenges Top

Other Trending Topics Top

JesseHerrick
Hey, I’m Jesse and I’m the main contributor behind Dexter, a full-featured, lightning-fast Elixir LSP optimized for large codebases. It s...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New
ausimian
Emily is an Elixir library that runs Nx computations on Apple’s MLX. Install it as the default Nx backend and Nx, defn, Axon, Nx.Serving,...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews