LoginSignup
8
1

闘魂Elixir ── Advent of code 2023 Day 10 Part 2 を Livebook で楽しむ

Last updated at Posted at 2023-12-27

$\huge{元氣ですかーーーーッ!!!}$
$\huge{元氣があればなんでもできる!}$

$\huge{闘魂とは己に打ち克つこと。}$
$\huge{そして闘いを通じて己の魂を磨いていく}$
$\huge{ことだと思います}$

はじめに

@torifukukaiou さんの パク リスペクト記事です

Elixir Livebook で Advent of Code 2023 の問題を解いてみます

実装したノートブックはこちら

問題はこちら

Part 1 はこちら

セットアップ

Kino AOC をインストールします

Mix.install([
  {:kino_aoc, "~> 0.1.5"}
])

Kino AOC の使い方はこちらを参照

入力の取得

Day 10 の入力を取得します

スクリーンショット 2023-12-27 13.11.13.png

私の答え

私の答えです。
折りたたんでおきます。
▶を押して開いてください。

details

回答用のモジュールです

入力を扱いやすい形にするための parse と、回答を作るための resolve 関数を持っています

defmodule Resolver do
  @pipe_map %{
    "-" => %{left: true, right: true},
    "|" => %{up: true, down: true},
    "F" => %{down: true, right: true},
    "7" => %{down: true, left: true},
    "L" => %{up: true, right: true},
    "J" => %{up: true, left: true},
    "S" => %{start: true, up: true, down: true, left: true, right: true}
  }

  @next_direction %{
    up: :down,
    down: :up,
    left: :right,
    right: :left,
  }

  def parse(input) do
    input
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.reduce(%{}, fn {line, row_index}, acc ->
      line_maze =
        line
        |> String.codepoints()
        |> Enum.with_index()
        |> Enum.into(%{}, fn {symbol, col_index} ->
          {
            {row_index, col_index},
            %{
              symbol: symbol,
              route: Map.get(@pipe_map, symbol, nil)
            }
          }
        end)
        |> Map.filter(fn {_, symbol} -> !is_nil(symbol.route) end)

      Map.merge(acc, line_maze)
    end)
  end

  def resolve(maze) do
    {start_position, start_pipe} =
      maze
      |> Enum.filter(fn {_, pipe} ->
        Map.has_key?(pipe.route, :start)
      end)
      |> hd()

    main_loop = get_main_loop(start_position, start_pipe, maze)

    IO.inspect(main_loop)

    {top_edge, bottom_edge} =
      main_loop
      |> Enum.map(fn {{row, _}, _} -> row end)
      |> then(fn rows ->
        {Enum.min(rows), Enum.max(rows)}
      end)

    {left_edge, right_edge} =
      main_loop
      |> Enum.map(fn {{_, col}, _} -> col end)
      |> then(fn cols ->
        {Enum.min(cols), Enum.max(cols)}
      end)

    IO.inspect({top_edge, bottom_edge, left_edge, right_edge})

    top_edge..bottom_edge
    |> Enum.reduce(0, fn row, row_acc ->
      {_, col_sum, _} =
        left_edge..right_edge
        |> Enum.reduce({nil, 0, false}, fn col, {pre_stack, pre_count, pre_in_loop} ->
          symbol = Map.get(main_loop, {row, col}, nil)
          {across, stack} = across?(symbol, pre_stack)
          in_loop = if across, do: !pre_in_loop, else: pre_in_loop
          if is_nil(symbol) and in_loop do
            {stack, pre_count + 1, in_loop}
          else
            {stack, pre_count, in_loop}
          end
        end)
      IO.inspect(col_sum)
      row_acc + col_sum
    end)
  end

  defp get_main_loop(start_position, start_pipe, maze) do
    Stream.iterate(0, & &1 + 1)
    |> Enum.reduce_while(
      {start_position, start_pipe, :start, [], nil},
      fn index, {pre_position, pre_pipe, pre_direction, loop_list, start_direction} ->
        {next_position, next_pipe, next_direction} =
          search_next(pre_position, pre_pipe, pre_direction, maze)
    
        loop_list = [{next_position, next_pipe.symbol} | loop_list]
    
        if Map.has_key?(next_pipe.route, :start) do
          start_symbol = get_symbol(start_direction, next_direction)
          {_, loop_list} = List.pop_at(loop_list, 0)
          {:halt, [{start_position, start_symbol} | loop_list]}
        else
          start_direction =
            if index == 0, do: next_direction, else: start_direction
          {:cont, {next_position, next_pipe, next_direction, loop_list, start_direction}}
        end
      end
    )
    |> Enum.into(%{})
  end

  defp search_next(position, pipe, pre_direction, maze) do
    [:up, :down, :left, :right]
    |> Enum.filter(& (&1 != pre_direction))
    |> Enum.map(fn direction ->
      get_next(position, pipe, direction, maze)
    end)
    |> Enum.filter(fn {_, next_pipe, _} ->
      !is_nil(next_pipe)
    end)
    |> hd()
  end

  defp get_next({row_index, col_index}, pipe, direction, maze) when is_map_key(pipe.route, direction) do
    next_position =
      case direction do
        :up -> {row_index - 1, col_index}
        :down -> {row_index + 1, col_index}
        :left -> {row_index, col_index - 1}
        :right -> {row_index, col_index + 1}
      end

    next_direction = Map.get(@next_direction, direction)

    next_pipe =
      maze
      |> Map.get(next_position)
      |> check_next(next_direction)

    {next_position, next_pipe, next_direction}
  end
  defp get_next(_, _, _, _), do: {nil, nil, nil}

  defp check_next(nil, _), do: nil
  defp check_next(next_pipe, next_direction) when is_map_key(next_pipe.route, next_direction) do
    next_pipe
  end
  defp check_next(_, _), do: nil

  defp get_symbol(:up, :up), do: "|"
  defp get_symbol(:down, :down), do: "|"
  defp get_symbol(:left, :left), do: "-"
  defp get_symbol(:right, :right), do: "-"
  defp get_symbol(:down, :right), do: "L"
  defp get_symbol(:down, :left), do: "J"
  defp get_symbol(:up, :right), do: "F"
  defp get_symbol(:up, :left), do: "7"
  defp get_symbol(:right, :down), do: "F"
  defp get_symbol(:left, :down), do: "7"
  defp get_symbol(:right, :up), do: "J"
  defp get_symbol(:left, :up), do: "L"

  defp across?(nil, stack), do: {false, stack}
  defp across?("|", _), do: {true, nil}
  defp across?(symbol, nil), do: {false, symbol}
  defp across?("-", stack), do: {false, stack}
  defp across?("J", "F"), do: {true, nil}
  defp across?("7", "L"), do: {true, nil}
  defp across?("7", "F"), do: {false, nil}
  defp across?("J", "L"), do: {false, nil}
end

parse で各パイプを座標をキー、記号、ルート(どの方向に行けるか)のマップをバリューとして読み取ります

入力

.....
.S-7.
.|.|.
.L-J.
.....

パース結果

%{
  {1, 1} => %{symbol: "S", route: %{start: true, up: true, down: true, left: true, right: true}},
  {1, 2} => %{symbol: "-", route: %{left: true, right: true}},
  {1, 3} => %{symbol: "7", route: %{down: true, left: true}},
  {2, 1} => %{symbol: "|", route: %{up: true, down: true}},
  {2, 3} => %{symbol: "|", route: %{up: true, down: true}},
  {3, 1} => %{symbol: "L", route: %{up: true, right: true}},
  {3, 2} => %{symbol: "-", route: %{left: true, right: true}},
  {3, 3} => %{symbol: "J", route: %{up: true, left: true}}
}

resolve では、まずは Part 1 と同じようにしてループを探索していきます

このとき、後で使うため、スタート地点("S" 記号の地点)が実際にはどの記号だったのかを取得しておきます

上の例で言うと、スタート地点は右と下に移動できるので "F" です

こうして取得したループ(メインループ)は以下のようになっています

%{
  {1, 1} => "F",
  {1, 2} => "-",
  {1, 3} => "7",
  {2, 1} => "|",
  {2, 3} => "|",
  {3, 1} => "L",
  {3, 2} => "-",
  {3, 3} => "J"
}

座標毎に各パイプの記号を記号を持つ形です
(メインループ以外のパイプは排除されています)

次にメインループの内側の領域を取得します

座標を横方向に辿って行ったとき(縦方向でも同様ですが)、パイプを奇数回横切ったときはループの内側で、偶数回横切った時はループの外側です
ループがどのような形であってもこれは変わりません

"|" を通った時は「横切った」と言えます
"F-J""L-7" でも横切っています
"F-7""L-J" は横切っていません(ループの縁に沿った)

このルールに従い、各行についてループの中にいるときだけカウントした結果を全行合計すると答えが出ます

まとめ

Part 1 と比べて圧倒的に難しかったです

「内側とは何か?」「横切ったとは何か?」を上手く定義するのに時間がかかりました
最後に「スタート地点の実際の形を取得する」ところをなんとか乗り越えて正解できました

8
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
1