호두나무 공방/Advent of Code

Advent of Code 2021 Day 20 in Elixir - Trench Map 풀이

2021. 12. 30. 01:41

문제 보기

11일차 문제에 이어 등장한, 생명 게임을 연상시키는 문제였다. '이미지 강화 알고리즘'이라는 설정으로, 자신과 주변 셀 값을 기반으로 한 규칙에 따라 이후 셀의 값을 정하는 계산을 여러 번 연속해서 적용해야 했다. 구현 자체가 어려운 건 아니었지만 '이미지가 무한하다'는 조건이 제법 까다로웠다.
주변이 전부 0인데 무한한 게 대체 무슨 문제인가 하고 생각했는데, 문제로 나온 규칙을 보니 자신을 포함해 주변이 전부 0인 경우 다음 값이 1이고, 주변이 전부 1인 경우 다음 값이 0이 되더라(이 문제의 유일한 꼬임 포인트가 이것이라, 아마 다른 사람이 풀어도 이 부분은 똑같지 않을까 싶다). 고민 끝에 이미지 주변에 여백을 추가하는 한편, 데이터상 가장 가장자리에서 이미지 바깥을 참조해야 하는 상황에서 반복 회차의 홀짝 여부에 따라 값을 다르게 반환하도록 하여 어찌어찌 해결했다(이미지 바깥의 여백은 0 -> 1 -> 0 -> 1로 번갈아가며 바뀌므로, 여백은 짝수번째 반복에서는 0, 홀수번째 반복에서는 1이어야 한다).

defmodule Day20 do
  def parse_input(file_name) do
    [algorithm, original_image] =
      file_name
      |> File.read!()
      |> String.split("\n\n")

    original_image =
      original_image
      |> String.split("\n")
      |> Enum.map(&String.codepoints/1)

    image_map = original_image |> nested_list_to_map()

    w = original_image |> hd() |> length()
    h = original_image |> length()

    {algorithm, image_map, w, h}
  end

  defp nested_list_to_map(nested_list) do
    nested_list
    |> Enum.with_index()
    |> Enum.flat_map(fn {row, row_index} ->
      row
      |> Enum.with_index()
      |> Enum.map(fn {val, col_index} ->
        {{col_index, row_index}, val}
      end)
    end)
    |> Enum.into(%{})
  end

  def run_q1(file_name \\ "input/day20.txt", count) do
    {algorithm, image_map, width, height} = parse_input(file_name)

    offset = 3

    {result_image, w_s..w_e, h_s..h_e} =
      do_run_q1(
        algorithm,
        image_map,
        (0 - offset)..(width + offset - 1),
        (0 - offset)..(height + offset - 1),
        count
      )

    # |> IO.inspect()

    resulting_w_range = (w_s + offset)..(w_e - offset)
    resulting_h_range = (h_s + offset)..(h_e - offset)

    trimmed_result_image =
      result_image
      |> Enum.filter(fn {{x, y}, _v} ->
        x in resulting_w_range and y in resulting_h_range
      end)
      |> Enum.into(%{})

    print_image(trimmed_result_image, resulting_w_range, resulting_h_range)
    |> IO.puts()

    Enum.count(trimmed_result_image, fn {_k, v} -> v == "#" end)
  end

  defp do_run_q1(_, image_map, w_range, h_range, 0), do: {image_map, w_range, h_range}

  defp do_run_q1(algorithm, image_map, w_s..w_e, h_s..h_e, count) do
    IO.inspect(count)
    new_w_range = (w_s - 1)..(w_e + 1)
    new_h_range = (h_s - 1)..(h_e + 1)

    new_image_map =
      for y <- new_h_range,
          x <- new_w_range,
          into: %{},
          do: {{x, y}, get_alg(algorithm, image_map, {x, y}, count)}

    # print_image(new_image_map, new_w_range, new_h_range)
    # |> IO.puts()

    do_run_q1(algorithm, new_image_map, new_w_range, new_h_range, count - 1)
  end

  defp get_alg(algorithm, image_map, {x, y}, count) do
    idx =
      [
        {x - 1, y - 1},
        {x, y - 1},
        {x + 1, y - 1},
        {x - 1, y},
        {x, y},
        {x + 1, y},
        {x - 1, y + 1},
        {x, y + 1},
        {x + 1, y + 1}
      ]
      |> Enum.map(&get_pixel(image_map, &1, count))
      |> Enum.join("")
      |> String.to_integer(2)

    String.at(algorithm, idx)
  end

  defp get_pixel(image_map, point, count) do
    default = if rem(count, 2) == 1, do: "#", else: "."
    if Map.get(image_map, point, default) == "#", do: 1, else: 0
  end

  defp print_image(image_map, w_range, h_range) do
    h_range
    |> Enum.map(fn y ->
      w_range
      |> Enum.map(fn x ->
        if get_pixel(image_map, {x, y}, 0) == 1, do: "#", else: "."
      end)
      |> Enum.join("")
    end)
    |> Enum.join("\n")
  end
end