2021年做的最痛苦的一道程式設計題 Advent of code 2021 day24

Ljzn發表於2022-02-17

老鐵們是否聽說過 https://adventofcode.com/ 這個網站,在每天的聖誕前夕,它就會開始連續25天釋出程式設計謎題,吸引了無數人蔘與。從我開始學程式設計那會兒,就有用這裡的題目來鍛鍊自己的程式設計能力。

2021年的題目的難度是逐漸加深的,越往後越艱難,我堅持做完了前23天的題目,直到看到第24天的題目,死磕了很久,還是想不出來。

題目大概是這樣:有一種計算單元,具有w,x,y,z四個暫存器,支援以下幾種指令:

  • inp a(讀取使用者輸入的數字,儲存於暫存器a)
  • add a b (a與b之和儲存於a。b可以是數字或暫存器)
  • mul a b (...乘積...)
  • mod a b (...餘數...)
  • div a b (...整除...)
  • eql a b (如果 a 等於 b,為 1,否則為0。結果儲存於a。b可以是數字或暫存器)

然後給定一串指令,令使用者輸入一串1~9的數字,使暫存器z的結果等於0. 求使用者輸入的數字按順序組成的十進位制數的最大和最小值。

嘗試解題

一開始我想方設法優化給定的指令,比如 add a 0mul a 1可以直接省略,最後發現優化完了還是很長一串,沒什麼卵用。

再後來想到 div a b 時如果 a < b 則為 0。加上我們知道單個輸入的範圍是 1..9,往後可以根據數字的範圍來進行優化,最後得到 z 的範圍。

卡在這裡沒辦法進展下去了,一個月之後,我終於忍不了了,在 reddit 上檢視了一下大家分享的解題方法。主要的方法是逆向分析給定的指令,從而得出對於輸入值的限制條件。看到這裡我有點失望,雖然逆向分析也很酷,但是 AOC 之前的題目很少有對輸入做假設的,我更希望使用一種通用的解法,能夠適用於任意的指令列表。

終於,我看到了某位大神的解法,完美滿足了我的需求。

解法

主要的思路和我之前是一樣的,即分析每一次計算的結果的最大和最小值。最牛的地方是大神不是隻分析一次,而是每一次 inp a 指令讀取完一個使用者輸入的值,就再重新分析一次計算結果的範圍。設 i(n) 是第n個 inp 指令讀取的使用者輸入。例如,當我們沒有給定 i(1) 的值時,i(1)的範圍是 {1, 9},最後分析出來 z 的範圍可能就是一個很大的區間。但我們給定 i(1) 為一個常數,最後分析出來的 z 的範圍就有可能會小很多。

以此類推,我們每給定一個 i 值,就做一次分析,如果 z 的範圍不包括0,我們就知道這次的 i 的序列沒必要繼續下去。反之,就可以繼續給下一個 i 的值。

以下是完整的程式碼

inputs = File.read!("inputs/d24.dat")

defmodule S do
  @moduledoc """
  Thanks ephemient's excellent answer! Rewrote from https://github.com/ephemient/aoc2021/blob/main/rs/src/day24.rs .
  """

  @doc """
  Parse instructions.
  """
  def parse(str) do
    str
    |> String.split("\n")
    |> Enum.map(fn line ->
      case String.split(line, " ") do
        [h | t] ->
          {parse_op(h), parse_args(t)}
      end
    end)
  end

  defp parse_op(op) when op in ~w(inp add mul div mod eql), do: String.to_atom(op)

  defp parse_args(list) do
    list
    |> Enum.map(fn x ->
      if x in ~w(w x y z) do
        String.to_atom(x)
      else
        String.to_integer(x)
      end
    end)
  end

  def new_alu, do: %{w: 0, x: 0, y: 0, z: 0}

  @nothing :nothing

  def check_range(ins, alu) do
    alu =
      for {r, v} <- alu, into: %{} do
        {r, {v, v}}
      end

    alu =
      ins
      |> Enum.reduce_while(alu, fn inst, alu ->
        case inst do
          {:inp, [lhs]} ->
            {:cont, %{alu | lhs => {1, 9}}}

          {op, [lhs, rhs]} ->
            {a, b} = alu[lhs]
            {c, d} = alu[rhs] || {rhs, rhs}

            lhs_range =
              case op do
                :add ->
                  {a + c, b + d}

                :mul ->
                  Enum.min_max([a * c, a * d, b * c, b * d])

                :div ->
                  cond do
                    c > 0 ->
                      {div(a, d), div(b, c)}

                    d < 0 ->
                      {div(b, d), div(a, c)}

                    true ->
                      @nothing
                  end

                :mod ->
                  if c > 0 and c == d do
                    if b - a + 1 < c and rem(a, c) <= rem(b, c) do
                      {rem(a, c), rem(b, c)}
                    else
                      {0, c - 1}
                    end
                  else
                    @nothing
                  end

                :eql ->
                  cond do
                    a == b and c == d and a == c ->
                      {1, 1}

                    a <= d and b >= c ->
                      {0, 1}

                    true ->
                      {0, 0}
                  end
              end

            case lhs_range do
              {a, b} ->
                {:cont, %{alu | lhs => {a, b}}}

              @nothing ->
                {:halt, @nothing}
            end
        end
      end)

    case alu do
      @nothing ->
        @nothing

      %{z: {a, b}} ->
        a <= 0 and b >= 0
    end
  end

  def solve([], _, prefix, alu) do
    if alu.z == 0 do
      prefix
    else
      nil
    end
  end

  def solve([inst | rest], nums, prefix, alu) do
    IO.inspect(prefix, label: "prefix")

    case inst do
      {:inp, [lhs]} ->
        nums
        |> Enum.find_value(fn num ->
          alu = %{alu | lhs => num}

          if check_range(rest, alu) != false do
            solve(rest, nums, 10 * prefix + num, alu)
          else
            nil
          end
        end)

      {op, [lhs, rhs]} ->
        a = alu[lhs]
        b = alu[rhs] || rhs

        result =
          case op do
            :add -> a + b
            :mul -> a * b
            :div -> div(a, b)
            :mod -> rem(a, b)
            :eql -> if(a == b, do: 1, else: 0)
          end

        solve(rest, nums, prefix, %{alu | lhs => result})
    end
  end
end

# test

insts =
  inputs
  |> S.parse()

# part 1
S.solve(insts, Enum.to_list(9..1), 0, S.new_alu())
|> IO.inspect()

# part 2
S.solve(insts, Enum.to_list(1..9), 0, S.new_alu())
|> IO.inspect()

相關文章