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

<b><font color="red">$\huge{元氣ですかーーーーッ！！！}$</font></b>
<b><font color="red">$\huge{元氣があればなんでもできる！}$</font></b>

<b><font color="red">$\huge{闘魂とは己に打ち克つこと。}$</font></b>
<b><font color="red">$\huge{そして闘いを通じて己の魂を磨いていく}$</font></b>
<b><font color="red">$\huge{ことだと思います}$</font></b>

https://qiita.com/advent-calendar/2023/elixir

## はじめに

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

https://qiita.com/torifukukaiou/items/5361b81407b15e7c2f00

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

https://adventofcode.com/2023

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

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2023/2023_day10_part2.livemd

問題はこちら

https://adventofcode.com/2023/day/10

Part 1 はこちら

https://qiita.com/RyoWakabayashi/items/9a22c987a7fe8c5a221b

## セットアップ

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

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

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

https://qiita.com/RyoWakabayashi/items/3d3abcf9134cee4dff05

## 入力の取得

Day 10 の入力を取得します

![スクリーンショット 2023-12-27 13.11.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/ef3ed983-1eeb-c7ca-6bd0-f31b0af9a005.png)

## 私の答え

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

<details><summary>details</summary>

回答用のモジュールです

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

```elixir
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.
.....
```

パース結果
```elixir
%{
  {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"` です

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

```elixir
%{
  {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"` は横切っていません（ループの縁に沿った）

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

</details>

## まとめ

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

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