Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us
Notesclub

Day 14: Parabolic Reflector Dish

day14.livemd

Day 14: Parabolic Reflector Dish

Mix.install([
  {:kino, "~> 0.12.0"}
])

Input

input = Kino.Input.textarea("Please, paste your input here:")
defmodule Day14Shared do
  def parse(input) do
    input
    |> Kino.Input.read()
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.reduce(%{max_x: 0, max_y: 0}, fn {row, y}, map ->
      row
      |> String.split("", trim: true)
      |> Enum.with_index()
      |> Enum.reduce(map, fn {value, x}, map ->
        map
        |> Map.put({x, y}, value)
        |> Map.update(:max_x, x, &max(&1, x))
        |> Map.update(:max_y, y, &max(&1, y))
      end)
    end)
  end

  def render(%{max_x: mx, max_y: my} = map) do
    Enum.map_join(0..my, "\n", fn y ->
      Enum.map_join(0..mx, "", fn x ->
        Map.get(map, {x, y}, "?")
      end)
    end)
    |> Kernel.<>("\n---")
    |> IO.puts()

    map
  end

  def tilt(%{max_x: mx, max_y: my} = map, tilt_to) when tilt_to in [:north, :south] do
    {range1, range2} = tilt_ranges(tilt_to, mx, my)

    Enum.reduce(range1, map, fn y, map ->
      range2
      |> Enum.filter(&amp;(Map.get(map, {&amp;1, y}) == "O"))
      |> Enum.reduce(map, fn x, map ->
        move_rock(tilt_to, x, y, mx, my, map)
      end)
    end)
  end

  def tilt(%{max_x: mx, max_y: my} = map, tilt_to) when tilt_to in [:east, :west] do
    {range1, range2} = tilt_ranges(tilt_to, mx, my)

    Enum.reduce(range1, map, fn x, map ->
      range2
      |> Enum.filter(&amp;(Map.get(map, {x, &amp;1}) == "O"))
      |> Enum.reduce(map, fn y, map ->
        move_rock(tilt_to, x, y, mx, my, map)
      end)
    end)
  end

  defp tilt_ranges(:north, mx, my), do: {0..my, 0..mx}
  defp tilt_ranges(:east, mx, my), do: {mx..0, my..0}
  defp tilt_ranges(:south, mx, my), do: {my..0, mx..0}
  defp tilt_ranges(:west, mx, my), do: {0..mx, 0..my}

  def move_rock(tilt_direction, x, y, mx, my, map) do
    range =
      case tilt_direction do
        :north -> y..0
        :east -> x..mx
        :south -> y..my
        :west -> x..0
      end

    Enum.reduce_while(range, map, fn coord, map ->
      pos =
        if tilt_direction in [:north, :south] do
          {x, coord}
        else
          {coord, y}
        end

      case move(tilt_direction, pos, map) do
        {:ok, new_pos} ->
          new_map =
            map
            |> Map.put(pos, ".")
            |> Map.put(new_pos, "O")

          {:cont, new_map}

        {:stuck, _} ->
          {:halt, map}
      end
    end)
  end

  def load(%{max_x: mx, max_y: my} = map) do
    my..0
    |> Enum.with_index(1)
    |> Enum.reduce(0, fn {y, factor}, total_load ->
      o_rocks_n = Enum.count(0..mx, fn x -> Map.get(map, {x, y}) == "O" end)

      total_load + o_rocks_n * factor
    end)
  end

  defp move(:north, {x, y}, map) do
    case Map.get(map, {x, y - 1}) do
      "." -> {:ok, {x, y - 1}}
      _ -> {:stuck, {x, y}}
    end
  end

  defp move(:east, {x, y}, map) do
    case Map.get(map, {x + 1, y}) do
      "." -> {:ok, {x + 1, y}}
      _ -> {:stuck, {x, y}}
    end
  end

  defp move(:south, {x, y}, map) do
    case Map.get(map, {x, y + 1}) do
      "." -> {:ok, {x, y + 1}}
      _ -> {:stuck, {x, y}}
    end
  end

  defp move(:west, {x, y}, map) do
    case Map.get(map, {x - 1, y}) do
      "." -> {:ok, {x - 1, y}}
      _ -> {:stuck, {x, y}}
    end
  end

  def cycle(map) do
    map
    |> tilt(:north)
    # |> render()
    |> tilt(:west)
    # |> render()
    |> tilt(:south)
    # |> render()
    |> tilt(:east)

    # |> render()
  end
end

Day14Shared.parse(input)
# |> Day14Shared.cycle()
# |> Day14Shared.cycle()
# |> Day14Shared.cycle()
# |> Day14Shared.render()

Part 1

defmodule Day14Part1 do
  import Day14Shared, except: [parse: 1]

  def solve(map) do
    map
    |> tilt(:north)
    # |> render()
    |> load()
  end
end

input
|> Day14Shared.parse()
|> Day14Part1.solve()

Part 2

defmodule Day14Part2 do
  @lrp_min_length 10
  @total_steps 1_000_000_000

  import Day14Shared, except: [parse: 1]

  def solve(map, steps \\ 300) do
    loads =
      0..steps
      |> Enum.reduce({map, []}, fn _, {map, loads} ->
        new_map = cycle(map)
        load = load(new_map)

        {new_map, [load | loads]}
      end)
      |> elem(1)
      |> Enum.reverse()

    # deltas =
    #   loads
    #   |> Enum.chunk_every(2, 1, :discard)
    #   |> Enum.map(fn [a, b] -> b - a end)

    case lrp(loads) do
      {:ok, pattern} -> final_load(loads, pattern)
      {:error, msg} -> msg
    end
  end

  @doc """
  Finds out what is the Longest Recurring Pattern in the given list with the POWER of PCRE!
  """
  def lrp(deltas) do
    stringified = Enum.join(deltas, ",")

    matches =
      ~r/(?=(.+)\1)/
      |> Regex.scan(stringified, capture: :all_but_first)
      |> List.flatten()
      |> Enum.find(&amp;(String.length(&amp;1) > @lrp_min_length))

    if is_nil(matches) do
      {:error, "try to increase depth you pass to (but not too much)"}
    else
      ptrn = String.split(matches, ",", trim: true) |> Enum.map(&amp;String.to_integer/1)
      {:ok, ptrn}
    end
  end

  @doc """
  Does a few simple math operations on the given deltas list and recurring pattern we
  found earlies:

  1. Gets the initial (non-recurring) sequence; we need it's sum and number of items.
  2. Calculates the sum and number of items in the recurring pattern.
  3. Finds out the tail piece; also sum of its items.
  4. Sum all three pieces of data.

  Note on "tail piece".

  There almost always a non-empty tail piece that will be non-full recurring pattern.

  Visually, it might be represented like this:

  [i, n, i, t, s, e, q][p, a, t, t, e, r, n][...][p, a, t, t, e, r, n][p, a, t, t]
  """
  def final_load(loads, pattern) do
    init_seq = initial_sequence(loads, pattern)

    IO.inspect(init_seq, label: "initial_sequence", charlists: :as_lists)
    IO.inspect(pattern, label: "pattern", charlists: :as_lists)

    init_count = Enum.count(init_seq)
    pattern_count = Enum.count(pattern)
    tail_size = rem(@total_steps - init_count, pattern_count)

    pattern
    |> Enum.take(tail_size)
    |> List.last()
  end

  @doc """
  Finds out where the initial (non-recurring) sequence ends and returns it.

  After this section we will see only recurring pattern repeat one after another.
  """
  def initial_sequence(deltas, pattern) do
    elements =
      Enum.find(1..length(pattern), fn i ->
        Enum.drop(deltas, i) |> List.starts_with?(pattern)
      end)

    Enum.take(deltas, elements)
  end
end

input
|> Day14Shared.parse()
|> Day14Part2.solve()

# 99669 is wrong answer
# 99291 is the right answe