cblavier

cblavier

Hey there :wave:

CleanShot 2020-12-11 at 10.59.25@2x

No magic or algorithmic finesse today, I just finished the challenge and I my code is quite slow (1sec for part1, 3sec for part2).

But at least I managed to made my code readable and to reuse most of part1 for part2.

My code: part1 / part2

Showing Posts 1 to 10

Aetherus

Aetherus

I did this too, and I want to see better solutions. :slight_smile:

LostKobrakai

LostKobrakai

I’ve gone ahead with a genserver for the simulation (not really needed but :man_shrugging:) and a state struct, which even implements String.Chars (mostly for comparing to the examples in tests). I like the code, but it’s not very fast as well.

https://github.com/LostKobrakai/aoc2020/commit/52a9f4866e22ccd6609dd6acc875fa9d1323f8a7

cblavier

cblavier OP

by the way, I often need to do some while true ... break in AOC challenges
Here is what I use instead:

Stream.cycle([0])
|> Enum.reduce_while([], fn _, acc ->
   ...
end)

Working, but I don’t really like the [0] list coming from nowhere.
Any other way?

LostKobrakai

LostKobrakai

aaronnamba

aaronnamba

There are many cases where it works well to build a Stream, filter it, and take 1.

mexicat

mexicat

Mine is also quite slow (~3s for p2) but at least the code is not too convoluted this time :slight_smile:

defmodule AdventOfCode.Day11 do
  def part1(input) do
    input
    |> make_grid()
    |> next(&switch_status/2)
  end

  def part2(input) do
    input
    |> make_grid()
    |> next(&switch_status_2/2)
  end

  def make_grid(input) do
    for {row, row_index} <- input |> String.split("\n") |> Enum.with_index(),
        {col, col_index} <- row |> String.codepoints() |> Enum.with_index(),
        into: %{},
        do: {{col_index, row_index}, col}
  end

  def seat_status(grid, x, y) do
    Map.get(grid, {x, y})
  end

  def count_occupied_seats(grid) do
    grid
    |> Map.values()
    |> Enum.count(&(&1 == "#"))
  end

  def next(grid, switch_fn) do
    new_grid =
      grid
      |> Enum.filter(fn {_k, v} -> v != "." end)
      |> Enum.map(&switch_fn.(grid, &1))
      |> Enum.into(grid)

    if new_grid == grid, do: count_occupied_seats(grid), else: next(new_grid, switch_fn)
  end

  def switch_status(grid, {{x, y}, status}) do
    adjacent =
      for other_x <- (x - 1)..(x + 1),
          other_y <- (y - 1)..(y + 1),
          {other_x, other_y} != {x, y},
          seat_status(grid, other_x, other_y) == "#" do
        true
      end
      |> length()

    cond do
      status == "#" and adjacent >= 4 -> {{x, y}, "L"}
      status == "L" and adjacent == 0 -> {{x, y}, "#"}
      true -> {{x, y}, status}
    end
  end

  def switch_status_2(grid, {pos, status}) do
    in_sight =
      [{-1, -1}, {-1, 0}, {-1, +1}, {0, -1}, {0, +1}, {1, -1}, {1, 0}, {1, 1}]
      |> Enum.map(&in_line_of_sight(grid, pos, &1))
      |> Enum.count(& &1)

    cond do
      status == "#" and in_sight >= 5 -> {pos, "L"}
      status == "L" and in_sight == 0 -> {pos, "#"}
      true -> {pos, status}
    end
  end

  def in_line_of_sight(grid, {x, y}, dir = {dir_x, dir_y}) do
    {new_x, new_y} = {x + dir_x, y + dir_y}

    case seat_status(grid, new_x, new_y) do
      "#" -> true
      "L" -> false
      nil -> false
      _ -> in_line_of_sight(grid, {new_x, new_y}, dir)
    end
  end

  # for debugging
  def visualize_grid(grid) do
    max_x = grid |> Enum.map(fn {{x, _y}, _} -> x end) |> Enum.max()
    max_y = grid |> Enum.map(fn {{_x, y}, _} -> y end) |> Enum.max()

    for y <- 0..max_y do
      for x <- 0..max_x do
        Map.get(grid, {x, y})
      end
      |> Enum.join()
    end
    |> Enum.join("\n")
    |> IO.puts()
  end
end
faried

faried

Could’ve used a better line of sight calculation. Oh, well.

defmodule Day11.Grid do
  @empty "L"
  @occupied "#"
  @space "."
  @seats [@empty, @occupied]

  def seat?(row, col, grid), do: Map.get(grid, {row, col}) in @seats

  def up(-1, _col, _area), do: {-1, -1}

  def up(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: up(row - 1, col, area)
  end

  def down(row, _, {numrows, _, _}) when row == numrows, do: {-1, -1}

  def down(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: down(row + 1, col, area)
  end

  def left(_row, -1, _area), do: {-1, -1}

  def left(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: left(row, col - 1, area)
  end

  def right(_, col, {_, numcols, _}) when col == numcols, do: {-1, -1}

  def right(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: right(row, col + 1, area)
  end

  def ne(-1, _col, _area), do: {-1, -1}
  def ne(_row, -1, _area), do: {-1, -1}

  def ne(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: ne(row - 1, col - 1, area)
  end

  def nw(-1, _col, _area), do: {-1, -1}
  def nw(_row, col, {_, numcols, _}) when col == numcols, do: {-1, -1}

  def nw(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: nw(row - 1, col + 1, area)
  end

  def se(_row, -1, _area), do: {-1, -1}
  def se(row, _col, {numrows, _, _}) when row == numrows, do: {-1, -1}

  def se(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: se(row + 1, col - 1, area)
  end

  def sw(row, col, {numrows, numcols, _}) when row == numrows or col == numcols,
    do: {-1, -1}

  def sw(row, col, {_, _, grid} = area) do
    if seat?(row, col, grid), do: {row, col}, else: sw(row + 1, col + 1, area)
  end

  def dirs(row, col, _area, 1 = _part) do
    [
      {row, col - 1},
      {row, col + 1},
      {row - 1, col},
      {row + 1, col},
      {row - 1, col - 1},
      {row - 1, col + 1},
      {row + 1, col - 1},
      {row + 1, col + 1}
    ]
  end

  def dirs(row, col, area, 2 = _part) do
    [
      left(row, col - 1, area),
      right(row, col + 1, area),
      up(row - 1, col, area),
      down(row + 1, col, area),
      ne(row - 1, col - 1, area),
      nw(row - 1, col + 1, area),
      se(row + 1, col - 1, area),
      sw(row + 1, col + 1, area)
    ]
  end

  def occupiedaround(row, col, {numrows, numcols, grid} = area, part) do
    dirs(row, col, area, part)
    |> Enum.count(fn {r, c} ->
      r in 0..(numrows - 1) and c in 0..(numcols - 1) and Map.get(grid, {r, c}) == @occupied
    end)
  end

  def change({r, c, @space}, _area, _part), do: {{r, c}, @space}

  def change({r, c, @empty}, area, part) do
    countoccupied = occupiedaround(r, c, area, part)
    {{r, c}, if(countoccupied == 0, do: @occupied, else: @empty)}
  end

  def change({r, c, @occupied}, area, part) do
    countoccupied = occupiedaround(r, c, area, part)

    maxfree = if part == 1, do: 3, else: 4

    {{r, c}, if(countoccupied > maxfree, do: @empty, else: @occupied)}
  end

  def seatround({numrows, numcols, grid} = area, part) do
    newgrid =
      for col <- 0..(numcols - 1),
          row <- 0..(numrows - 1) do
        change({row, col, Map.get(grid, {row, col})}, area, part)
      end
      |> Enum.reduce(%{}, fn {k, v}, acc -> Map.put(acc, k, v) end)

    {numrows, numcols, newgrid}
  end

  def countoccupied({numrows, numcols, grid}) do
    for(
      col <- 0..(numcols - 1),
      row <- 0..(numrows - 1),
      do: Map.get(grid, {row, col}) == @occupied
    )
    |> Enum.count(& &1)
  end

  def printarea({numrows, numcols, grid} = area) do
    for(col <- 0..(numcols - 1), row <- 0..(numrows - 1), do: Map.get(grid, {row, col}))
    |> Enum.chunk_every(numrows)
    |> Enum.map(&Enum.join(&1, ""))
    |> Enum.join("\n")
    |> IO.puts()

    area
  end
end

defmodule Day11 do
  alias Day11.Grid

  def readinput() do
    rows =
      File.read!("11.test.txt")
      |> String.split("\n", trim: true)

    numrows = length(rows)
    numcols = String.length(Enum.at(rows, 0))

    grid =
      rows
      |> Enum.with_index()
      |> Enum.flat_map(fn {rowstr, rownum} ->
        String.graphemes(rowstr)
        |> Enum.with_index()
        |> Enum.flat_map(fn {letter, colnum} -> %{{rownum, colnum} => letter} end)
      end)
      |> Enum.into(%{})

    {numrows, numcols, grid}
  end

  def untilstable(area, occupied, part) do
    newarea = Grid.seatround(area, part)
    newoccupied = Grid.countoccupied(newarea)

    if newoccupied == occupied, do: newoccupied, else: untilstable(newarea, newoccupied, part)
  end

  def part1(area \\ readinput()) do
    untilstable(area, Grid.countoccupied(area), 1)
  end

  def part2(area \\ readinput()) do
    untilstable(area, Grid.countoccupied(area), 2)
  end
end

faried

faried

I can replace most of that crunk with

  def move(-1, _col, _delta, _area), do: {-1, -1}
  def move(_row, -1, _delta, _area), do: {-1, -1}

  def move(row, _col, _delta, {numrows, _, _}) when row == numrows, do: {-1, -1}
  def move(_row, col, _delta, {_, numcols, _}) when col == numcols, do: {-1, -1}

  def move(row, col, {dx, dy} = delta, {_, _, grid} = area) do
    newrow = row + dx
    newcol = col + dy

    if seat?(newrow, newcol, grid), do: {newrow, newcol}, else: move(newrow, newcol, delta, area)
  end

  def dirs(row, col, area, 2 = _part) do
    [
      move(row, col, {0, -1}, area),
      move(row, col, {0, 1}, area),
      move(row, col, {-1, 0}, area),
      move(row, col, {1, 0}, area),
      move(row, col, {-1, -1}, area),
      move(row, col, {-1, 1}, area),
      move(row, col, {1, -1}, area),
      move(row, col, {1, 1}, area),
    ]
  end
LostKobrakai

LostKobrakai

Most of those computations involving coordinates can be done with vector math and {x, y} is basically the simplest form of representing an vector.

Hallski

Hallski

Ended up putting all the seats in a map and then lookup them up to calculate occupied seats. Also learnt the lesson of making sure to copy the entire test data instead of all but the last ten lines… :slight_smile:

Anyone know of a nicer way to iterate a stream until it stabilises, what I ended up with felt a bit cumbersome.

# Part 2
defmodule AdventOfCode.Day11 do
  def run() do
    AdventOfCode.Helpers.Data.read_from_file("day11.txt")
    |> to_room()
    |> Stream.iterate(&iterate/1)
    |> Stream.chunk_every(2, 1)
    |> Stream.filter(fn [new, last] -> new == last end)
    |> Enum.take(1)
    |> (fn [[x, _]] -> x end).()
    |> occupied_seats()
  end

  def occupied_seats(%{seats: seats}) do
    seats
    |> Enum.filter(fn {_, state} -> state == "#" end)
    |> Enum.count()
  end

  def to_room(lines) do
    seats = lines |> Enum.with_index() |> Enum.map(&create_row/1) |> Enum.reduce(&Map.merge/2)

    %{width: String.graphemes(hd(lines)) |> Enum.count(), height: Enum.count(lines), seats: seats}
  end

  def create_row({line, row_nr}) do
    line
    |> String.graphemes()
    |> Enum.with_index()
    |> Enum.filter(fn {spot, _} -> spot != "." end)
    |> Enum.reduce(%{}, fn {spot, col}, acc -> Map.put(acc, {row_nr, col}, spot) end)
  end

  def iterate(%{seats: seats} = room) do
    new_seats =
      seats
      |> Enum.map(fn {{row, col} = pos, state} -> {pos, update_room(room, row, col, state)} end)
      |> Map.new()

    %{room | seats: new_seats}
  end

  def update_room(room, row, col, state) do
    update_room(occupied(room, row, col), state)
  end

  def update_room(occupied, state) when occupied == 0 and state == "L", do: "#"
  def update_room(occupied, state) when occupied >= 5 and state == "#", do: "L"
  def update_room(_, state), do: state

  def occupied(room, row, col) do
    ["N", "E", "S", "W", "NW", "NE", "SE", "SW"]
    |> Enum.map(fn direction -> occupied(room, direction, row, col) end)
    |> Enum.reduce(&(&1 + &2))
  end

  def check_dir(%{height: height, width: width}, _, row, col)
      when row < 0 or row >= height or col < 0 or col >= width do
    0
  end

  def check_dir(%{seats: seats} = room, direction, row, col) do
    case Map.get(seats, {row, col}) do
      nil -> occupied(room, direction, row, col)
      "#" -> 1
      "L" -> 0
    end
  end

  def occupied(room, "N", row, col), do: check_dir(room, "N", row - 1, col)
  def occupied(room, "E", row, col), do: check_dir(room, "E", row, col + 1)
  def occupied(room, "S", row, col), do: check_dir(room, "S", row + 1, col)
  def occupied(room, "W", row, col), do: check_dir(room, "W", row, col - 1)
  def occupied(room, "NW", row, col), do: check_dir(room, "NW", row - 1, col - 1)
  def occupied(room, "NE", row, col), do: check_dir(room, "NE", row - 1, col + 1)
  def occupied(room, "SE", row, col), do: check_dir(room, "SE", row + 1, col + 1)
  def occupied(room, "SW", row, col), do: check_dir(room, "SW", row + 1, col - 1)
end

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
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
mhanberg
Hi everyone! The first release candidate for the Expert language server project is now available! We’ve published a press release detai...
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
Dmk
Xamal is a deployment tool for Elixir apps that deploys native releases to bare metal servers over SSH. It’s a port of GitHub - basecamp/...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews