Advent of code 2024 Day 15 Part 2 を Livebook で楽しむ

## はじめに

Advent of code 2024 Day 15 の Part 2 を解きます

問題文はこちら

https://adventofcode.com/2024/day/15

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

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2024/2024_day15_part2.livemd

Part 1 はこちら

https://qiita.com/RyoWakabayashi/private/29235cf487b52632d647

## セットアップ

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

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

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

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

## 入力の取得

"Advent of Code Helper" スマートセルを追加し、 Day 15 の入力を取得します

![スクリーンショット 2024-12-19 16.00.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/8ce4994f-3769-0803-ab1a-d966612c8280.png)

## 私の答え

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

<details><summary>回答</summary>

入力例で考えます
（Part 1 とは違う例です）

```elixir
small_sample_input =
  """
  #######
  #...#.#
  #.....#
  #..OO@#
  #..O..#
  #.....#
  #######
  
  <vv<<^^<<^^
  """
  |> String.trim()
```

問題文に沿って、地図の幅を倍にします

```elixir
spread_map = fn input ->
  input
  |> String.replace("#", "##")
  |> String.replace("O", "[]")
  |> String.replace(".", "..")
  |> String.replace("@", "@.")
end
```

その上で、地図と移動方向配列として読み込みます

```elixir
parse_map = fn input ->
  [map_rows, direction_rows] = String.split(input, "\n\n")

  map =
    map_rows
    |> String.split("\n")
    |> Enum.with_index()
    |> Enum.flat_map(fn {row, row_index} ->
      row
      |> String.codepoints()
      |> Enum.with_index()
      |> Enum.map(fn {mark, col_index} ->
        {{col_index, row_index}, mark}
      end)
    end)
    |> Enum.into(%{})

  directions =
    direction_rows
    |> String.replace("\n", "")
    |> String.codepoints()

  {map, directions}
end
```

```elixir
{map, directions} =
  small_sample_input
  |> spread_map.()
  |> parse_map.()
```

実行結果

```elixir
{%{
   {1, 3} => "#",
   {1, 1} => "#",
   {1, 5} => "#",
   {...} => "#",
   ...
 }, ["<", "v", "v", "<", "<", "^", "^", "<", "<", "^", "^"]}
```

ロボットの初期位置を取得します

```elixir
get_robot_point = fn map ->
  map
  |> Enum.find(fn {_, mark} -> mark == "@" end)
  |> elem(0)
end
```

```elixir
robot_point = get_robot_point.(map)
```

実行結果

```elixir
{10, 3}
```

Part 1 と基本的な考え方は同じですが、箱の幅がロボットの倍なので、箱が左右にはみ出し、隣の列の箱も巻き込んで進みます

動けるかどうかの判断は、移動しようとしている箱（"[" または "]"）の先に壁（"#"）があるか、で行います

例えば、以下の例では上に進むことができません

```
####
..[]
.[].
..[]
...@
####
```

逆に、以下のような配置では1回上に進めます

```
####
..[]
[]..
.[].
..@.
####
```

実装したモジュールは以下のものです

```elixir
defmodule Robot do
  def move({map, robot_point}, direction) do
    moving_points = search_end_point(robot_point, direction, map, [])

    if Enum.any?(hd(moving_points), &(elem(&1, 1) == "#")) do
      {map, robot_point}
    else
      {
        go_next(robot_point, direction, moving_points, map),
        get_next_point(robot_point, direction)
      }
    end
  end

  defp go_next(robot_point, direction, moving_points, map) do
    current_points_group = Enum.reverse(moving_points)
    next_points_group = (tl(moving_points) ++ [[{robot_point, "@"}]]) |> Enum.reverse()

    current_points_group
    |> Enum.zip(next_points_group)
    |> Enum.reduce(map, fn {current_points, next_points}, acc_map ->
      current_points
      |> Enum.reduce(acc_map, fn current_point, acc_acc_map ->
        {{cx, _}, _} = current_point

        next_point =
          Enum.find(next_points, fn {{nx, _}, _} ->
            if Enum.member?(["^", "v"], direction) do
              cx == nx
            else
              true
            end
          end)

        mark =
          case next_point do
            nil -> "."
            _ -> elem(next_point, 1)
          end

        Map.put(acc_acc_map, elem(current_point, 0), mark)
      end)
    end)
    |> Map.put(robot_point, ".")
  end

  def search_end_point({rx, ry} = robot_point, direction, map, acc) do
    head =
      case acc do
        [] -> [{robot_point, "@"}]
        _ -> hd(acc)
      end

    next_points =
      head
      |> Enum.filter(fn {_, mark} -> mark != "." end)
      |> Enum.map(fn {{ax, _}, _} ->
        get_next_point({ax, ry}, direction)
      end)

    next_row =
      Enum.flat_map(next_points, fn point ->
        if Enum.member?(["^", "v"], direction) do
          case Map.get(map, point) do
            "#" ->
              [{point, Map.get(map, point)}]

            "." ->
              [{point, Map.get(map, point)}]

            "]" ->
              {px, py} = point
              [{{px - 1, py}, "["}, {point, "]"}]

            "[" ->
              {px, py} = point
              [{point, "["}, {{px + 1, py}, "]"}]
          end
        else
          [{point, Map.get(map, point)}]
        end
      end)
      |> Enum.sort()
      |> Enum.uniq()

    next_acc = [next_row | acc]

    cond do
      Enum.any?(next_points, &(Map.get(map, &1) == "#")) ->
        next_acc

      Enum.all?(next_points, &(Map.get(map, &1) == ".")) ->
        next_acc

      Enum.member?(["^", "v"], direction) ->
        robot_point = {rx, if(direction == "^", do: ry - 1, else: ry + 1)}
        search_end_point(robot_point, direction, map, next_acc)

      true ->
        robot_point = hd(next_points)
        search_end_point(robot_point, direction, map, next_acc)
    end
  end

  defp get_next_point({px, py}, direction) do
    case direction do
      "^" -> {px, py - 1}
      "v" -> {px, py + 1}
      "<" -> {px - 1, py}
      ">" -> {px + 1, py}
    end
  end

  def get_map_size(map) do
    {
      map |> Enum.map(fn {{x, _}, _} -> x end) |> Enum.max() |> Kernel.+(1),
      map |> Enum.map(fn {{_, y}, _} -> y end) |> Enum.max() |> Kernel.+(1)
    }
  end

  def display_map(map, {tx, ty}) do
    0..(ty - 1)
    |> Enum.map(fn y ->
      0..(tx - 1)
      |> Enum.map(fn x ->
        Map.get(map, {x, y})
      end)
      |> Enum.join()
    end)
    |> Enum.join("\n")
  end
end
```

まずは初期状態を視覚化します

```elixir
map_size = Robot.get_map_size(map)
```

```elixir
map
|> Robot.display_map(map_size)
|> Kino.Text.new(terminal: true)
```

実行結果

```
##############
##......##..##
##..........##
##....[][]@.##
##....[]....##
##..........##
##############
```

ロボットを移動させてみましょう

```elixir
{moved_map, _} =
  directions
  |> Enum.reduce({map, robot_point}, fn direction, {acc_map, acc_robot_point} ->
    {acc_map, acc_robot_point} = Robot.move({acc_map, acc_robot_point}, direction)

    IO.puts(direction)
    acc_map |> Robot.display_map(map_size) |> IO.puts()
    IO.puts("")

    {acc_map, acc_robot_point}
  end)

moved_map
|> Robot.display_map(map_size)
|> Kino.Text.new(terminal: true)
```

標準出力

```
<
##############
##......##..##
##..........##
##...[][]@..##
##....[]....##
##..........##
##############

v
##############
##......##..##
##..........##
##...[][]...##
##....[].@..##
##..........##
##############

v
##############
##......##..##
##..........##
##...[][]...##
##....[]....##
##.......@..##
##############

<
##############
##......##..##
##..........##
##...[][]...##
##....[]....##
##......@...##
##############

<
##############
##......##..##
##..........##
##...[][]...##
##....[]....##
##.....@....##
##############

^
##############
##......##..##
##...[][]...##
##....[]....##
##.....@....##
##..........##
##############

^
##############
##......##..##
##...[][]...##
##....[]....##
##.....@....##
##..........##
##############

<
##############
##......##..##
##...[][]...##
##....[]....##
##....@.....##
##..........##
##############

<
##############
##......##..##
##...[][]...##
##....[]....##
##...@......##
##..........##
##############

^
##############
##......##..##
##...[][]...##
##...@[]....##
##..........##
##..........##
##############

^
##############
##...[].##..##
##...@.[]...##
##....[]....##
##..........##
##..........##
##############
```

問題文と同じ移動をしています

原点により近い方は必ず　"[" なので、 "[" の座標を使って計算します

```elixir
get_gps_coordinate = fn map ->
  map
  |> Enum.map(fn {{x, y}, mark} ->
    case mark do
      "[" -> x + y * 100
      _ -> 0
    end
  end)
  |> Enum.sum()
end
```

```elixir
get_gps_coordinate.(moved_map)
```

実行結果

```elixir
618
```

大きい入力例でも同様に処理してみます

まずは状態を確認します

```elixir
{map, directions} =
  """
  ##########
  #..O..O.O#
  #......O.#
  #.OO..O.O#
  #..O@..O.#
  #O#..O...#
  #O..O..O.#
  #.OO.O.OO#
  #....O...#
  ##########
  
  <vv>^<v^>v>^vv^v>v<>v^v<v<^vv<<<^><<><>>v<vvv<>^v^>^<<<><<v<<<v^vv^v>^
  vvv<<^>^v^^><<>>><>^<<><^vv^^<>vvv<>><^^v>^>vv<>v<<<<v<^v>^<^^>>>^<v<v
  ><>vv>v^v^<>><>>>><^^>vv>v<^^^>>v^v^<^^>v^^>v^<^v>v<>>v^v^<v>v^^<^^vv<
  <<v<^>>^^^^>>>v^<>vvv^><v<<<>^^^vv^<vvv>^>v<^^^^v<>^>vvvv><>>v^<<^^^^^
  ^><^><>>><>^^<<^^v>>><^<v>^<vv>>v>>>^v><>^v><<<<v>>v<v<v>vvv>^<><<>^><
  ^>><>^v<><^vvv<^^<><v<<<<<><^v<<<><<<^^<v<^^^><^>>^<v^><<<^>>^v<v^v<v^
  >^>>^v>vv>^<<^v<>><<><<v<<v><>v<^vv<<<>^^v^>^^>>><<^v>>v^v><^^>>^<>vv^
  <><^^>^^^<><vvvvv^v<v<<>^v<v>v<<^><<><<><<<^^<<<^<<>><<><^^^>^^<>^>v<>
  ^^>vv<^v^v<vv>^<><v<^v>^^^>>>^^vvv^>vvv<>>>^<^>>>>>^<<^v>^vvv<>^<><<v>
  v^^>>><<^^<>>^v^<v^vv<>v^<<>^<^v^v><^<<<><<^<v><v<>vv>>v><v^<vv<>v^<<^
  """
  |> String.trim()
  |> spread_map.()
  |> parse_map.()

map_size = Robot.get_map_size(map)

map
|> Robot.display_map(map_size)
|> Kino.Text.new(terminal: true)
```

実行結果

```
####################
##....[]....[]..[]##
##............[]..##
##..[][]....[]..[]##
##....[]@.....[]..##
##[]##....[]......##
##[]....[]....[]..##
##..[][]..[]..[][]##
##........[]......##
####################
```

移動させてみます

```elixir
robot_point = get_robot_point.(map)

{moved_map, _} =
  directions
  |> Enum.reduce({map, robot_point}, fn direction, {acc_map, acc_robot_point} ->
    Robot.move({acc_map, acc_robot_point}, direction)
  end)

moved_map
|> Robot.display_map(map_size)
|> Kino.Text.new(terminal: true)
```

実行結果

```
####################
##[].......[].[][]##
##[]...........[].##
##[]........[][][]##
##[]......[]....[]##
##..##......[]....##
##..[]............##
##..@......[].[][]##
##......[][]..[]..##
####################
```

```elixir
get_gps_coordinate.(moved_map)
```

実行結果

```elixir
9021
```

実際の入力に対して実行します

```elixir
{map, directions} =
  puzzle_input
  |> spread_map.()
  |> parse_map.()

robot_point = get_robot_point.(map)
map_size = Robot.get_map_size(map)

directions
|> Enum.reduce({map, robot_point}, fn direction, {acc_map, acc_robot_point} ->
  Robot.move({acc_map, acc_robot_point}, direction)
end)
|> elem(0)
|> get_gps_coordinate.()
```

</details>

## まとめ

問題文から ChatGPT に画像を生成してもらいました

![aoc2024_day15_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/1457c337-1cc5-5e59-daf9-3f261357cfed.png)

箱の幅が2倍になるだけで、かなり難しくなりました