老铁们是否听说过 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 0
,mul 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()
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。