Advent of code 2015 Day 15 Part 1 を Livebook で楽しむ

## はじめに

Advent of code 2024 の準備として、過去回の Advent of code 2015 を Livebook で楽しみます

本記事では Day 15 の Part 1 を解きます

問題文はこちら

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

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

https://github.com/RyoWakabayashi/elixir-learning/blob/main/livebooks/aoc/2015/2015_day15_part1.livemd

## セットアップ

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

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

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

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

いちいち四則演算するのが面倒になってきたので、 Nx による行列演算を導入します

https://github.com/elixir-nx/nx/tree/main/nx

## 入力の取得

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

![スクリーンショット 2024-11-26 18.39.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1485835/9758fa80-5e38-7dae-c986-1d866e0c03e4.png)

## 私の答え

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

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

まず正規表現で入力をパースします

```elixir
Regex.named_captures(
  ~r/(?<ingredient>[a-zA-Z]+): .+ (?<capacity>\-*\d+), .+ (?<durability>\-*\d+), .+ (?<flavor>\-*\d+), .+ (?<texture>\-*\d+), .+ (?<calories>\-*\d+)/,
  "Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8"
)
```

実行結果

```elixir
%{
  "calories" => "8",
  "capacity" => "-1",
  "durability" => "-2",
  "flavor" => "6",
  "ingredient" => "Butterscotch",
  "texture" => "3"
}
```

入力を行列に変換する関数を用意します

```elixir
get_ingredients_tensor = fn rows ->
  rows
  |> Enum.map(fn row ->
    Regex.named_captures(
      ~r/(?<ingredient>[a-zA-Z]+): .+ (?<capacity>\-*\d+), .+ (?<durability>\-*\d+), .+ (?<flavor>\-*\d+), .+ (?<texture>\-*\d+), .+ (?<calories>\-*\d+)/,
      row
    )
    |> then(fn %{"capacity" => capacity, "durability" => durability, "flavor" => flavor, "texture" => texture} ->
      [
        String.to_integer(capacity),
        String.to_integer(durability),
        String.to_integer(flavor),
        String.to_integer(texture)
      ]
    end)
  end)
  |> Nx.tensor()
end
```

入力例を使って試します

```elixir
ingredients =
  [
    "Butterscotch: capacity -1, durability -2, flavor 6, texture 3, calories 8",
    "Cinnamon: capacity 2, durability 3, flavor -2, texture -1, calories 3"
  ]
  |> get_ingredients_tensor.()
```

実行結果

```elixir
#Nx.Tensor<
  s32[2][4]
  [
    [-1, -2, 6, 3],
    [2, 3, -2, -1]
  ]
>
```

バタースコッチを小さじ44杯、シナモンを小さじ56杯のレシピを定義します

```elixir
nums = Nx.tensor([[44, 56]])
```

材料とレシピの内積により、各プロパティのスコアを計算することができます

```elixir
Nx.dot(nums, ingredients)
```

実行結果

```elixir
#Nx.Tensor<
  s32[1][4]
  [
    [68, 80, 152, 76]
  ]
>
```

最終的なスコアは各プロパティのスコアを全てかけたものです

```elixir
nums
|> Nx.dot(ingredients)
|> Nx.product(axes: [1])
```

実行結果

```elixir
#Nx.Tensor<
  s32[1]
  [62842880]
>
```

出力例と同じ値になりました

パズルの入力を行列に変換します

```elixir
ingredients =
  puzzle_input
  |> String.split("\n")
  |> get_ingredients_tensor.()
```

実行結果

```elixir
#Nx.Tensor<
  s32[4][4]
  [
    [5, -1, 0, 0],
    [-1, 3, 0, 0],
    [0, -1, 4, 0],
    [-1, 0, 0, 2]
  ]
>
```

各材料の分量が合計 100 になる組み合わせを生成します

```elixir
all_combinations =
  for sprinkles <- 0..100,
      peanut_butter <- 0..100,
      frosting <- 0..100,
      sugar <- 0..100,
      sprinkles + peanut_butter + frosting + sugar == 100 do
    [sprinkles, peanut_butter, frosting, sugar]
  end
```

実行結果

```elixir
[
  [0, 0, 0, 100],
  [0, 0, 1, 99],
  [0, 0, 2, 98],
  [0, 0, 3, 97],
  ...
]
```

各組み合わせについて、プロパティ毎のスコアを計算します

```elixir
all_combinations
|> Nx.tensor()
|> Nx.dot(ingredients)
```

実行結果

```elixir
#Nx.Tensor<
  s32[176851][4]
  [
    [-100, 0, 0, 200],
    [-99, -1, 4, 198],
    [-98, -2, 8, 196],
    ...
  ]
>
```

スコアがマイナスになっている場合は 0 にするため、 0 と `Nx.max()` を取ります

```elixir
all_combinations
|> Nx.tensor()
|> Nx.dot(ingredients)
|> Nx.max(0)
```

実行結果

```elixir
#Nx.Tensor<
  s32[176851][4]
  [
    [0, 0, 0, 200],
    [0, 0, 4, 198],
    [0, 0, 8, 196],
    ...
  ]
>
```

各組み合わせ毎に合計スコアを計算します

```elixir
all_combinations
|> Nx.tensor()
|> Nx.dot(ingredients)
|> Nx.max(0)
|> Nx.product(axes: [1])
```

実行結果

```elixir
#Nx.Tensor<
  s32[176851]
  [0, 0, 0, 0, 0, 0, 0, ...]
>
```

最後に最大値を求めます

```elixir
all_combinations
|> Nx.tensor()
|> Nx.dot(ingredients)
|> Nx.max(0)
|> Nx.product(axes: [1])
|> Nx.reduce_max()
```

</details>

## まとめ

いよいよ行列演算を使い出して、難易度の上昇を感じますね

Part 2 はこちら

https://qiita.com/RyoWakabayashi/private/c63ee8db39f49fff14ac