호두나무 공방/Advent of Code

Advent of Code 2021 Day 8 in Elixir - Seven Segment Search

2021. 12. 8. 23:06

문제 보기

7-세그먼트를 이루는 신호가 꼬여 있을 때, 입력으로 들어오는 세그먼트 수와 빈도 등을 가지고 원래 어떤 숫자를 표시하려고 했는지를 유추하는 문제였다(거칠게 줄이면 이런데, 이번 문제는 이래저래 설명이 좀 길다. 자세하게는 원문을 참고!) 문제만으로는 '퍼즐'이라는 이름에 걸맞는 소재였는데, 이걸 코드로 짜려니 여간 귀찮은 것이 아니었다.

푼 방법을 대략 소개하면 이렇다. 아이디어가 어렵지는 않아서 구현에 집중하는 문제로 이해하고 풀었는데, 더 쉽게 풀 수 있는 방법이 있지 않을까 싶기도 하다.

#  aaaa
# b    c
# b    c
#  dddd
# e    f
# e    f
#  gggg
  • 세그먼트 수 2개짜리 입력은 1, 3개짜리 입력은 7, 4개짜리 입력은 4
  • 1(c, f)과 7(a, c, f)의 차이에서 윗변(a) 찾기
  • 세그먼트 수 5개짜리 입력인 0, 6, 9에서 공통적으로 나타나는 변은 a, b, f, g
  • 0, 6, 9의 공통변(a, b, f, g)과 1(c, f)의 차이에서 오른쪽 윗변(c)을 찾고, 1을 이루는 변 중 c가 아닌 쪽이 오른쪽 아랫변(f)
  • 4를 이루는 변(b, c, d, f)과 0, 6, 9의 공통변(a, b, f, g)의 교집합(b, f)에서 1을 이루지 않는 변이 왼쪽 윗변(b)
  • 4를 이루는 변(b, c, d, f)에서 지금까지 구한 b, c, f를 제거하면 중앙변(d)
  • 0, 6, 9의 공통변(a, b, f, g)에서 지금까지 구한 a, b, f를 제거하면 밑변(g)
  • 여기까지 구한 뒤 남는 변이 왼쪽 아랫변(e)

올해 AoC에서 지금까지 풀었던 문제 중에 제일 시간 많이 쓴 듯. 통계 페이지를 보니 나만 고생한 게 아닌 것 같아서 조금이나마 위안이 된다.

defmodule Day8 do
  @mapping_by_number %{
    MapSet.new([:top, :top_right, :bot_right, :bot, :bot_left, :top_left]) => 0,
    MapSet.new([:top_right, :bot_right]) => 1,
    MapSet.new([:top, :top_right, :mid, :bot_left, :bot]) => 2,
    MapSet.new([:top, :top_right, :mid, :bot_right, :bot]) => 3,
    MapSet.new([:top_left, :top_right, :mid, :bot_right]) => 4,
    MapSet.new([:top, :top_left, :mid, :bot_right, :bot]) => 5,
    MapSet.new([:top, :top_left, :mid, :bot_left, :bot_right, :bot]) => 6,
    MapSet.new([:top, :top_right, :bot_right]) => 7,
    MapSet.new([:top, :top_left, :top_right, :mid, :bot_left, :bot_right, :bot]) => 8,
    MapSet.new([:top, :top_left, :top_right, :mid, :bot_right, :bot]) => 9
  }

  def parse_input do
    path = "input/day8.txt"

    File.read!(path)
    |> String.split("\n")
    |> Enum.map(fn row ->
      row
      |> String.split(" | ")
      |> Enum.map(&String.split(&1, " "))
    end)
  end

  def run_q1 do
    digits =
      parse_input()
      |> Enum.map(fn [_pattern, digit] -> digit end)
      |> List.flatten()

    digits
    |> Enum.count(fn digit -> String.length(digit) in [2, 3, 4, 7] end)
  end

  def run_q2 do
    parse_input()
    |> Enum.map(fn [pattern, digit] ->
      mapping_by_pos = %{
        top: nil,
        top_left: nil,
        top_right: nil,
        mid: nil,
        bot_left: nil,
        bot_right: nil,
        bot: nil
      }

      # finding top
      digit_1 = find_digit_by_number_of_segment(pattern, 2) |> hd()
      digit_7 = find_digit_by_number_of_segment(pattern, 3) |> hd()
      digit_4 = find_digit_by_number_of_segment(pattern, 4) |> hd()

      mapping_by_pos =
        Map.put(mapping_by_pos, :top, MapSet.difference(digit_7, digit_1) |> first())

      # finding top_right, bot_right
      [seg5_cand1, seg5_cand2, seg5_cand3] = find_digit_by_number_of_segment(pattern, 6)

      segs_abfg =
        seg5_cand1
        |> MapSet.intersection(seg5_cand2)
        |> MapSet.intersection(seg5_cand3)

      mapping_by_pos =
        Map.put(mapping_by_pos, :top_right, MapSet.difference(digit_1, segs_abfg) |> first())

      mapping_by_pos =
        Map.put(
          mapping_by_pos,
          :bot_right,
          MapSet.delete(digit_1, mapping_by_pos.top_right) |> first()
        )

      # finding top_left
      mapping_by_pos =
        Map.put(
          mapping_by_pos,
          :top_left,
          MapSet.intersection(segs_abfg, digit_4)
          |> MapSet.delete(mapping_by_pos.bot_right)
          |> first()
        )

      # finding mid
      mapping_by_pos =
        Map.put(
          mapping_by_pos,
          :mid,
          digit_4
          |> MapSet.delete(mapping_by_pos.top_right)
          |> MapSet.delete(mapping_by_pos.top_left)
          |> MapSet.delete(mapping_by_pos.bot_right)
          |> first()
        )

      # finding bot
      mapping_by_pos =
        Map.put(
          mapping_by_pos,
          :bot,
          segs_abfg
          |> MapSet.delete(mapping_by_pos.top)
          |> MapSet.delete(mapping_by_pos.top_left)
          |> MapSet.delete(mapping_by_pos.bot_right)
          |> first()
        )

      # finding bot_left
      mapping_by_pos =
        Map.put(
          mapping_by_pos,
          :bot_left,
          ["a", "b", "c", "d", "e", "f", "g"]
          |> Kernel.--(Map.values(mapping_by_pos))
          |> List.first()
        )

      map_to_digit(mapping_by_pos, digit)
    end)
    |> Enum.sum()
  end

  @spec find_digit_by_number_of_segment(list(String.t()), integer()) :: list(MapSet.t())
  defp find_digit_by_number_of_segment(pattern, number_of_segment) do
    pattern
    |> Enum.filter(fn p -> String.length(p) == number_of_segment end)
    |> Enum.map(fn p -> p |> String.codepoints() |> MapSet.new() end)
  end

  defp first(mapset) do
    mapset |> MapSet.to_list() |> List.first()
  end

  defp map_to_digit(mapping_by_pos, digit) do
    mapping_by_seg = Enum.map(mapping_by_pos, fn {k, v} -> {v, k} end) |> Enum.into(%{})

    Enum.map(digit, fn d ->
      key =
        d |> String.codepoints() |> Enum.map(fn seg -> mapping_by_seg[seg] end) |> MapSet.new()

      @mapping_by_number[key]
    end)
    |> Integer.undigits()
  end
end