The most painful programming problem in 2021

Posted by danielholmes85 on Tue, 15 Feb 2022 04:43:50 +0100

Have the old fellow heard of it? https://adventofcode.com/ This website, on the eve of Christmas every day, will start publishing programming puzzles for 25 consecutive days, attracting countless people to participate. Since I began to learn programming, I have used the topics here to exercise my programming ability.

The difficulty of the topic in 2021 is gradually deepened, and it becomes more and more difficult in the future. I insisted on completing the topic in the first 23 days until I saw the topic on the 24th day. After a long time of stumbling, I still can't think of it.

The problem is like this: there is a computing unit with four registers W, x, y and Z, which supports the following instructions:

  • inp a (read the number entered by the user and save it in register a)
  • Add a b (the sum of a and b is stored in A. b can be a number or a register)
  • mul a b (... Product...)
  • mod a b (... Remainder...)
  • div a b (... Divide...)
  • eql a b (1 if a equals b, otherwise 0. The result is saved in A. b can be a number or a register)

Then, given a series of instructions, make the user input a series of numbers from 1 to 9, so that the result of register z is equal to 0 The sum of the user's input numbers is the smallest in decimal order.

Try to solve the problem

At first, I tried to optimize the given instructions, such as add a 0 and mul a 1, which can be omitted directly. Finally, I found that there was still a long string after optimization, which was useless.

Later, when you think of div a b, it is 0 if a < B. Plus we know that the range of a single input is 1 9. In the future, it can be optimized according to the range of numbers, and finally get the range of z.

I can't make progress here. After a month, I finally can't stand it. I checked the problem-solving methods shared by you on reddit. The main method is to reverse analyze the given instructions to obtain the constraints on the input value. I'm a little disappointed to see this. Although reverse analysis is also cool, there are few assumptions about input in the previous topics of AOC. I prefer to use a general solution that can be applied to any instruction list.

Finally, I saw the of some great God solution , it perfectly meets my needs.

solution

The main idea is the same as before, that is, to analyze the maximum and minimum values of the results of each calculation. The best thing is that the great God does not only analyze once, but reanalyzes the range of calculation results every time the inp a instruction reads the value entered by the user. Let i(n) be the user input read by the nth inp instruction. For example, when we do not give the value of i(1), the range of i(1) is {1,9}, and the range of z finally analyzed may be a large interval. But if we give i(1) as a constant, the range of z may be much smaller.

By analogy, every time we give a value of i, we do an analysis. If the range of z does not include 0, we know that the sequence of i does not need to continue this time. On the contrary, you can continue to give the value of the next i.

The following is the complete code

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()

Topics: leetcode