欢迎光临散文网 会员登陆 & 注册

【TIS-100 攻略】TIS-NET 第 23~24 关:T20 节点模拟器、T31 节点模拟器

2022-11-14 17:03 作者:ココアお姉ちゃん  | 我要投稿

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。

我们翻阅数据手册,可以发现数据手册上有这么奇怪的两页:

手册里提到了 T20 和 T31 这两个特殊的节点,其中 T31 节点是“随机访问存储器”(Random Access Memory, RAM)节点,T20 则是一个神秘节点,我们对它的内部细节毫无所知。我们现在面临的 TIS-NET 第 23、24 关则正好要求我们手动实现这两个特殊的节点。

TIS-NET 第 23 关《T20 节点模拟器》(T20 Node Emulator)关卡展示

T20 节点有两个输入量:IN.I 和 IN.V,分别代表指令(instruction)和数值(value)。我们要为 T20 节点实现如下操作:

  • 收到 0 指令时,读入一个数值,将其存为 p;

  • 收到 1 指令时,读入一个数值,将其存为 q;

  • 收到 2 指令时,交换 p 和 q;

  • 收到 3 指令时,将 p 加上 q 并覆盖原始的 p;

  • 收到 4 指令时,将当前的 p 值送往输出口。

本关的代码如下:

IN.V 下方的两个节点接力将当前的 value 发给中央节点(mov up down, mov up left)。

IN.I 下方的节点从 IN.I 处读入一个指令 x,并将它映射为 2x+1 发给下方的中央节点(mov up acc, add acc, add 1, mov acc down)。原始指令 0、1、2、3、4 分别被映射成了 1、3、5、7、9 命令。

中央节点的 acc 用来存储 p 的值,bak 用来存储 q 的值。它随时听候上方节点的命令(jro up):

  • 收到命令 1 时,说明原始指令是 0。我们向下跳 1 行,从右边读入一个数值 v,视为 p,将其存入 acc 中(mov right acc, jmp 1);

  • 收到命令 3 时,说明原始指令是 1。我们向下跳 3 行,从右边读入一个数值 v,视为 q,将其存入 bak 中(swp, mov right acc, swp, jmp 1);

  • 收到命令 5 时,说明原始指令是 2。我们向下跳 5 行,交换 acc 和 bak(swp, jmp 1);

  • 收到命令 9 时,说明原始指令是 4。我们向下跳 9 行,将 acc 里的 p 值发给下方(mov acc down);

以上四种命令没啥好说的,都非常简单。最麻烦的是收到命令 7 时的操作,此时原始指令是 3。我们向下跳 7 行,执行令 p 加上 q 的操作。由于 bak 不能直接参与跟 acc 的运算,所以我们得先将 bak 发到左边的邻居节点处暂存(swp, jmp c, mov acc left, swp),然后再从这个左边邻居处读入 bak,累加到 acc 中(add left)。注意执行第一条 swp 指令后,要强制跳到第 12 行继续执行,因为第 10 行是收到命令 9 时的操作,收到命令 7 时不能执行。

左边的邻居节点只有一条指令:mov right right。它的读取目标和写入目标都是 right,这意味着:当右侧节点向我们写入一个数字时,我们将它暂存起来,并等待右侧节点接下来发起读请求时,将该数字原封不动地还给它。这就像在【踢皮球】:右侧节点会首先会将 bak 中值为 q 的皮球踢过来,我们先接住;然后右侧节点切换到 acc 时,会请求我们将 q 皮球踢回去,累加到 p 上。这时候我们原封不动地把 q 这个皮球踢回去就行了。

最下方的节点仅当本次命令为 9 时才工作,将上方发来的 p 值送给 OUT(mov up down)。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

TIS-NET 第 24 关《T31 节点模拟器》(T31 Node Emulator)关卡展示

终于到了我们的随机存储器了。本题需要维护一个长度为 8 的 nums 数组(索引从 0 开始,8 个元素的索引分别为 0~7),然后:

  • 当你从 IN 收到 0 时,接着从 IN 里读入两个数字 i 和 v,然后将 nums[i] 的值设为 v;

  • 当你从 IN 收到 1 时,接着从 IN 里读入一个数字 i,然后将 nums[i] 的值输出给 OUT 口。

维护一个数组——有没有觉得很眼熟?没错,之前我们求众数(传送门:【TIS-100 攻略】TIS-NET 第 10 关:序列众数计算器)以及计数排序(传送门:【TIS-100 攻略】TIS-NET 第 20 关:超长序列排序器)的时候,都维护了一个频数数组,用于记录各个元素的出现次数。之前的这两个关卡,我们已经相当于实现了一个小型的 T31 节点。本关我们则是要实现一个完整的 T31 节点。

本关的代码如下:

首先看 IN 下方的节点:

  1. 初始化长度为 8 的 nums 数组,向上方的栈中放入 8 个 0(mov 8 acc)

  2. (mov 0 left)

  3. (sub 1)

  4. (jnz 2)

  5. 接下来不断将 IN 中的数字发给中间靠右的节点(mov up down)

  6. (jmp 5)

然后看中间靠右的节点:

  1. 中间靠右的节点首先接收 0 或 1 的指令,将其放入 bak 暂存(mov up acc)

  2. (swp)

  3. 然后,题目假设数组的索引是从 0 开始的,但是我们以往维护的都是以 1 索引起始的数组。这里我们还是按既往经验来,将收到的 i 加上 1,得到真实的数组索引 j(mov 1 acc)

  4. (add up)

  5. 并发给左边(mov acc left)。

  6. 接下来,我们检查 bak 中的命令值是 0 还是 1(swp):

  7. 如果命令值为 1,则跳到第 11 行执行(jnz b)

  8. 命令值是 0(存命令)时,需要从 in 读取一个新的数值 v,并将 nums[j] 设置为 v。我们按顺序执行,给左边发送一个 1 指令(mov 1 left),

  9. 并将新的 v 值一并发送给左边(mov up left)

  10. (jmp 1)

  11. 命令值是 1(取命令)时,需要输出 nums[j] 的值。我们给左边发送一个 3 指令(mov 3 left),

  12. 并将左边传来的 nums[j] 的值发给下方(mov left down)。

接下来看中间靠左的节点:

  1. 中间靠左的节点在收到右边发来的 j 值后(mov right acc),

  2. 先令上方的栈弹栈 j 次(sav)

  3. (mov up down)

  4. (sub 1)

  5. (jnz 3)

  6. 然后从下方栈的栈顶处获得 nums[j] 的值,将其放入 acc(mov down acc)

  7. 并等待右边节点发来新的命令(jro right)。

  8. 当你收到了 1 指令时,说明要把 nums[j] 的值更新为 v。我们向下跳 1 行,将右边发来的 v 值放入下方栈的栈顶(mov right down),

  9. 然后跳到第 12 行,避开 10~11 行的逻辑互斥的代码(jmp c)。

  10. 当你收到了 3 指令时,说明要将 nums[j] 的值发给右边输出。我们向下跳 3 行,将 acc 中的 nums[j] 值发给右边(mov acc right),

  11. 然后 nums[j] 原样放回下方栈的栈顶(mov acc down)。

  12. 操作完成后,下方的栈向上弹栈 j 次(swp)

  13. (mov down up)

  14. (sub 1)

  15. (jnz d)

右下角节点没啥好说的,遇到上方传来的需要输出的值时,无脑向下传就 OK 了(mov up down)。

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

优化代码行数

我们将以上代码做一些微调,即可省去 6 行代码,同时其他两项指标也不会恶化。

  • 中间靠左的节点得到 nums[j] 的值后,将该值发往右边,同时从右边接收新的 nums[j] 的值,既存又取

  • 中间靠右的节点在收到指令 0(存指令)时,先丢弃掉左边返回的 nums[j],然后将读到的 v 值发给左边,作为新的 nums[j]

  • 中间靠右的节点在收到指令 1(取指令)时,将左边返回的 nums[j] 存入 acc,向下方发一份后,将原始的 nums[j] 传回去,相当于让左边节点把 nums[j] 仍设置成之前的值

优化后的代码如下:

上方和下方节点的代码没有任何变化。然后看中间靠右的节点:

  1. 中间靠右的节点首先接收 0 或 1 的指令,将其放入【acc】暂存(mov up acc)。

  2. 然后将上方发来的 i 值【直接发给左边】(mov up left),由左边去计算对应的 j 值。

  3. 接下来,我们检查【acc】中的命令值是 0 还是 1:若命令值是 1,跳到第 7 行执行(jnz 7)。

  4. 命令值是 0(存命令)时,我们丢弃掉左边发来的 nums[j] 的值(mov left acc),

  5. 然后将新的 v 值发给左边,作为新的 nums[j](mov up left)

  6. (jmp 1)

  7. 命令值是 1(取命令)时,我们将左边返回的 nums[j] 存入 acc(mov left acc),

  8. 原样返回(mov acc left),

  9. 同时复制一份发给下方(mov acc down)。

接下来看中间靠左的节点:

  1. 首先将右方传来的 i 加上 1 映射成 j(mov 1 acc)

  2. (add right)

  3. 然后:令上方弹栈 j 次(sav)

  4. (mov up down)

  5. (sub 1)

  6. (jnz 4)

  7. 将下方栈顶的 nums[j] 发给右方(mov down right),

  8. 从右方接收新的 nums[j] 放入下方栈顶(mov right down),

  9. 令下方弹栈 j 次(swp)

  10. (mov down up)

  11. (sub 1)

  12. (jnz a)

这样的话,这个节点变成了【既存又取】的样子,和具体的存取命令解耦了。另外,由于多出了许多代码空间,所以 i 映射成 j 的操作可以由本节点亲自完成,中央靠右的节点可以不占用自己的 bak 寄存器,少执行了很多次 swp 指令,代码行数进一步减少

点击左下角的【RUN】,稍等片刻,便会弹出结算界面:

34 行代码减少到了 28 行,同时其他两项指标也没有增加。本方案完爆了上一版方案。

【TIS-100 攻略】TIS-NET 第 23~24 关:T20 节点模拟器、T31 节点模拟器的评论 (共 条)

分享到微博请遵守国家法律