《代码的出现》第24天天真地涉及到实现一台机器,它实现了一种假想的汇编语言,并将一个程序作为谜题输入。然后必须执行9^14次,以搜索产生特定结果的最低和最高程序输入。
将其实现为解释器在计算上是不可行的。我做这件事是第一次,执行这件事几乎需要100年的挂钟时间。
查看第24天Code subreddit发布的解决方案,有多种解决方案,包括:
我的解决方案是修改我的解释器,使其能够处理集合,并行搜索多种可能性。它和我看到的都不一样,所以我想把它写在这里。
首先,让我们看看指定ALU的解释器。未显示解析指令的步骤。例如";mul x 0和#34;字符串被转换为Clojure表示形式[:mul:x 0]。
defn machine[input]{:memory{:x0:y0:z0:w0}:input-input}(def machine ops{:mul*:add+:div(fn[ab](int(/ab)):mod mod:eql(fn[ab](if(=ab)10))(defn advanced[machine instr](让[abc]instr获得val(fn[v](if(关键字?v)(进入机器[:内存v]))))](if:inpa)(>;machine)(在[:memory b]中关联(第一个(:input machine))(更新:input rest))(在机器[:memory b]中关联)(机器操作a)(获取值b)(获取值c‘‘‘‘‘)’)(defn运行程序[machine instrs](减少高级机器指令))
AdvancedFunction接受一台机器和一条指令,并返回一台由该指令推进的新机器。run PROGRAME(运行程序)功能通过一系列指令来减少机器。
从这里,可以修改naive解释器,使其对整数集而不是整数集进行操作。
当对两个集合(例如(+a b)执行操作时,结果是一个集合,其中包含应用于a的任何成员和b的任何成员的每个可能值。这是两个集合之间的笛卡尔运算。
;; 在Clojure记数法中,集合A+集合B,每个集合包含1,2和3(+#{1,2,3}{1,2,3});;[A0+B0,A0+B1,A0+B2],[A1+B0,A1+B1,…=>;[2 3 4][3 4 5][4 5 6];;所有可能性都连接在一起=>;[ 2 3 4 3 4 5 4 5 6] ;; 最后,他们';重新放置在一个集合中,这将删除重复项=>#{2 3 4 5 6}(*{1 2 3}{0})=>;[0][0][0]=>;[0]=>#{0}(#{1 2}{0 1 3})=>;[[0 1 0][0 0]=>;[0 1 0 0 0]=>#{ 0 1}
这样做的目的是通过固定一些输入,让其他输入浮动,从而并行地剔除搜索空间的大部分。
想象一下,我们正在寻找a+b+c+d=35,其中每个都在1-9之间,包括1-9。
(+#{1};a#{1 2 3 4 5 6 7 8 9};b#{1 2 3 4 5 6 7 8 9};c#{1 2 3 4 5 6 7 8 9});d=>#{ 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28}
假阳性是可能的。笛卡尔积对变量之间的相关性视而不见。例如,(-x x),其中x=#{12}将返回#{1 0 1},而不是精确地返回#{0}。
然而,假阴性是不可能的。如果在一个集合中找不到所需的结果,则可以保证输入的组合不会产生该结果,并且可以跳过该部分搜索空间。
出处也没有被追踪。你知道可能的结果是什么,但不知道是什么输入导致了这些结果。
机器用内存constaining#{0}初始化,这是封装在集合中的默认初始值。
defn machine[input]{:memory{:x{0}:y}:z{0}:w{0}:input-input}(def machine ops{:mul*:add+:div(fn[ab](int(/ab)):mod mod mod:eql(fn[ab](if(=ab)1 0))(defn笛卡尔运算[op-vb](in#in#for[a])(for[a])(for[b])(instr advanced machine]如果(:inp a)(>;机器(assoc in[:memory b])(first(:input machine))(update:input rest))(assoc in machine[:memory b](笛卡尔运算(machine ops a)(get val b)(get val c‘);)(defn run program[machine instrs](减少高级机器指令))
程序执行后,测试:zmemory位置是否包含值0。
在最坏的情况下,电视机的尺寸可能会变得非常大,需要一个断路器——但这不是解决第24天问题的必要功能。
现在我们已经建立了一个可以并行搜索输入的解释器,现在是讨论搜索策略的时候了。我们的输入是14位数。输入中的一些数字可能固定为单个值,而其他数字则未知,这与集合#{1 2 3 4 6 7 9}有关。
我将使用符号263XXXXXXXXXX表示一个输入,其中前三位固定为263,其余数字未知。
搜索函数会被传递一个固定号码和自由号码的列表。它会检查第一个自由数的每一个变化,以确定预期结果是否可能。如果是这样,它将该数字设置为固定值,并进一步递归。
(defn search[instrs fixed free](apply concat(filter identity)(针对[i(first free)](让[inp(concat fixed[#i}](rest free))m(运行程序(机器inp)instrs)mem(get in m[:memory:z])(当(包含?mem 0)(如果(空的话)(rest free))(conj fixed#{i})(search instrs(conj fixed#{i})(rest free#)(rest#})(rest free)))))41
为了并行执行,前三个数字的所有排列都会在线程池中独立检查。从263开始的搜索路径可能如下所示:
在Ryzen 3900X的单核上,测试917xxxxxxxxxxxin无需进一步递归即可表明此模式不会产生任何答案,需要1231ms才能执行。
这将314亿种可能性排除在搜索之外。解释器的初始标量实现大约需要0.1ms来测试单个数字。
这意味着通过set方法排除搜索空间会使我们的速度提高大约250万倍。
前三位有729个可能的值。其中,702人立即被排除在一张支票之外。其他需要递归。
谜题的第一部分要求返回预期结果的最高输入,即91397187145896。这需要75秒的时间来计算,使用所有可用的内核。
谜题的第二部分要求返回预期结果的最低输入,即41178298586991。这需要248秒来计算。
此外,可以搜索整个空间,在3486秒内检索12096个解决方案。
不错。但我们可以做得更好!在下一篇文章中,我们将尝试构建一个编译器,将ALU指令转换为Rust。