9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 4

Advent of code 2015 Day 18 Part 1 & Part 2 を Livebook で楽しむ

Last updated at Posted at 2024-12-02

はじめに

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

本記事では Day 18 の Part 1 と Part 2 を解きます

問題文はこちら

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

セットアップ

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

また、今回も行列演算をしたいので Nx を使います

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

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

入力の取得

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

スクリーンショット 2024-11-30 11.49.01.png

私の答え

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

Part 1

回答

まず入力例の小さいケースから考えてみましょう

入力の文字列を行列に変換します

lights =
  """
  .#.#.#
  ...##.
  #....#
  ..#...
  #.#..#
  ####..
  """
  |> String.trim()
  |> String.split("\n")
  |> Enum.map(fn row ->
    row
    |> String.codepoints()
    |> Enum.map(fn light -> if light == "#", do: 1, else: 0 end)
  end)
  |> Nx.tensor()

実行結果

#Nx.Tensor<
  s32[6][6]
  [
    [0, 1, 0, 1, 0, 1],
    [0, 0, 0, 1, 1, 0],
    [1, 0, 0, 0, 0, 1],
    [0, 0, 1, 0, 0, 0],
    [1, 0, 1, 0, 0, 1],
    [1, 1, 1, 1, 0, 0]
  ]
>

「光っている周囲のライト数」を端でも数えたいので、上下左右に 0 を詰め込みます

pad_tensor = Nx.pad(lights, 0, [{1, 1, 0}, {1, 1, 0}])

実行結果

#Nx.Tensor<
  s32[8][8]
  [
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 1, 0, 1, 0],
    [0, 0, 0, 0, 1, 1, 0, 0],
    [0, 1, 0, 0, 0, 0, 1, 0],
    [0, 0, 0, 1, 0, 0, 0, 0],
    [0, 1, 0, 1, 0, 0, 1, 0],
    [0, 1, ...],
    ...
  ]
>

Nx.window_sum3 * 3 の範囲の合計を計算します

また、各ライトの周辺で光っているライトだけを集計するため、各 3 * 3 の範囲について中央のライトを引きます

around_on =
  pad_tensor
  |> Nx.window_sum({3, 3})
  |> Nx.subtract(lights)

実行結果

#Nx.Tensor<
  s32[6][6]
  [
    [1, 0, 3, 2, 4, 1],
    [2, 2, 3, 2, 4, 3],
    [0, 2, 2, 3, 3, 1],
    [2, 4, 1, 2, 2, 2],
    [2, 6, 4, 4, 2, 0],
    [2, 4, 3, 2, 2, 1]
  ]
>

現在光っているライトノ場合、周辺のライトが 2 以上 3 以下光っていれば、次のタイミングで光ります

current_on_next_on =
  Nx.multiply(
    lights,
    Nx.multiply(Nx.greater_equal(around_on, 2), Nx.less_equal(around_on, 3))
  )

実行結果

#Nx.Tensor<
  s32[6][6]
  [
    [0, 0, 0, 1, 0, 0],
    [0, 0, 0, 1, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 0, 0],
    [1, 0, 1, 1, 0, 0]
  ]
>

現在光っていないライトの場合、周辺のライトが 3 個光っていれば次に光ります

current_off_next_on =
  Nx.multiply(
    Nx.equal(lights, 0),
    Nx.equal(around_on, 3)
  )

実行結果

#Nx.Tensor<
  u8[6][6]
  [
    [0, 0, 1, 0, 0, 0],
    [0, 0, 1, 0, 0, 1],
    [0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0]
  ]
>

どちらかの条件に合致していれば光るので、上記二つの演算結果を足します

next_on = Nx.add(current_on_next_on, current_off_next_on)

実行結果

#Nx.Tensor<
  s32[6][6]
  [
    [0, 0, 1, 1, 0, 0],
    [0, 0, 1, 1, 0, 1],
    [0, 0, 0, 1, 1, 0],
    [0, 0, 0, 0, 0, 0],
    [1, 0, 0, 0, 0, 0],
    [1, 0, 1, 1, 0, 0]
  ]
>

出力例で示されている値と同じになりました

上記でやってきた内容を実際の入力で実行します

lights =
  puzzle_input
  |> String.split("\n")
  |> Enum.map(fn row ->
    row
    |> String.codepoints()
    |> Enum.map(fn light -> if light == "#", do: 1, else: 0 end)
  end)
  |> Nx.tensor()

100 回操作を繰り返した結果、 1 になっていた数を合計します

1..100
|> Enum.reduce(lights, fn _, acc_lights ->
  around_on =
    acc_lights
    |> Nx.pad(0, [{1, 1, 0}, {1, 1, 0}])
    |> Nx.window_sum({3, 3})
    |> Nx.subtract(acc_lights)

  current_on_next_on =
    Nx.multiply(
      acc_lights,
      Nx.multiply(Nx.greater_equal(around_on, 2), Nx.less_equal(around_on, 3))
    )

  current_off_next_on =
    Nx.multiply(
      Nx.equal(acc_lights, 0),
      Nx.equal(around_on, 3)
    )

  Nx.add(current_on_next_on, current_off_next_on)
end)
|> Nx.sum()

Part 2

回答

四隅だけは常に光っているため、その配置を表す行列を作成します

tb_lights =
  1..100
  |> Enum.map(fn index -> if index == 1 or index == 100, do: 1, else: 0 end)
  |> Nx.tensor()
  |> Nx.new_axis(1)

inner_lights = Nx.broadcast(0, {100, 98})

corner_lights =
  Nx.concatenate(
    [tb_lights, inner_lights, tb_lights],
    axis: 1
  )

パズル入力と四隅 1 の論理和を取ります

lights = Nx.logical_or(lights, corner_lights)

各回の演算時、最後に四隅 1 との論理和を取るようにします

1..100
|> Enum.reduce(lights, fn _, acc_lights ->
  around_on =
    acc_lights
    |> Nx.pad(0, [{1, 1, 0}, {1, 1, 0}])
    |> Nx.window_sum({3, 3})
    |> Nx.subtract(acc_lights)

  current_on_next_on =
    Nx.multiply(
      acc_lights,
      Nx.multiply(Nx.greater_equal(around_on, 2), Nx.less_equal(around_on, 3))
    )

  current_off_next_on =
    Nx.multiply(
      Nx.equal(acc_lights, 0),
      Nx.equal(around_on, 3)
    )

  Nx.add(current_on_next_on, current_off_next_on)
  |> Nx.logical_or(corner_lights)
end)
|> Nx.sum()

まとめ

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

lights.jpeg

行列演算で綺麗に解けるタイプの問題でした

9
0
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
9
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?