【TIS-100 攻略】TIS-NET 第 20 关:超长序列排序器

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。
TIS-NET 第 20 关《超长序列排序器》(Prolonged Sequence Sorter)关卡展示

本关会给你一个以 -1 结尾的超长序列,序列中的数字都在 0~9 范围内。你需要将序列中的数字从小到大排好输出给 OUT。
排序题我们在上一章的 22 关做过,不过那一关的序列长度短(不超过 10 个数字)、取值范围大(10~99)。对于这样的序列,我们总结出了规律,适合用选择排序或插入排序算法。但是本关的数据正好反过来,取值范围小(0~9),序列长度长(38 个数字)。如果仍然使用选择排序或插入排序的话,我们的栈只有 15 格空间,是无法完成任务的。但是因为本关数字的取值范围小,我们可以使用计数排序来完成。
如果一个序列的取值范围也大,序列长度也长的话,怎么排序呢?答案是:除非增加栈的数量,如果还是只有两个栈,那么无解。
回顾一下计数排序的过程:










以下代码假设提供的 nums 一定在 0~9 的范围内,并以 -1 结尾:
那么,我们现在根据示意图和 C 语言代码,写出本关的 TIS-100 代码:

首先看右上角的节点:
右上角的节点首先将上方栈里塞入 10 个 0,用于初始化 f 数组(mov 10 acc)
(mov 0 left)
(sub 1)
(jnz 2)
然后进入第一阶段:每从上方收到一个 num,我们都要将 f[num] 加上 1。由于 num 从 0 开始,f[num] 是 f 数组的第 num+1 个元素,所以我们将数字 num 改写成 num+1(mov 1 acc)
(add up)
并发给下方(mov acc down)。
此时检查:若 num+1 不为 0,说明尚未到达 nums 数组最后的 -1,我们跳回第 5 行,继续接收(jnz 5),直到到达最后的 -1 后,向下执行,进入第二阶段。
现在暂时把目光移动到中间靠右的节点处:
中间靠右的节点是一个纯传话节点,第一阶段里,它将收到的 num+1(mov up acc)
发给左边的节点,令其将 f[num] 加上 1(mov acc left)
(jnz 1)
第二阶段里,它将上方发来的所有数字全部无脑传到右下角的节点里(mov up down)
(jmp 4)
中间靠左的节点用来维护频数数组 f:
它随时等待右边传来 num+1(mov right acc)。
如果 num+1 为 0,即 num 为 -1,则跳回第 1 行,什么都不做(jez 1)。
接下来的代码就是将 f 数组的第 num+1 个元素 f[num] 加上 1,和之前《众数》题里的代码大同小异(传送门:【TIS-100 攻略】TIS-NET 第 10 关:序列众数计算器)。这里就长话短说了:上方弹栈 num+1 次后(sav)
(mov up down)
(sub 1)
(jnz 4)
下方栈顶里的数字即为 f[num],将其(mov down acc)
加上 1 后(add 1)
放回原处(mov acc down)。
处理完成后,下方往回弹栈 num+1 次(swp)
(mov down up)
(sub 1)
(jnz b)
以上便是第一阶段的全部工作。现在进入第二阶段,我们将目光回到上方节点。第一阶段里,上方节点的 acc 用来存储 num+1 的值,到了第二阶段,acc 就要改为存储 num 了。进入第二阶段的条件是 num+1 = 0,所以进入第二阶段后,上方节点的 acc 一定是 0,正好是第一个 num 的值。接下来:
9. 上方节点将每个 num(mov acc down)
10. 以及对应的 f[num] 都发到中间靠右的节点里(mov left down),
11. 然后判断:如果 num 到达了 9(sub 9),
12. 则跳到第 15 行执行(jez f),
13. 否则令 num 加 1(add 10),
14. 并跳回第 9 行继续发送下一组 num 和 f[num](jmp 9)。
15. 直到 num 到达 9 后,向下发送最终的 -1(mov -1 down)。
中间靠右的节点将每一组 num 和对应的 f[num] 都发到右下角的输出节点里(mov up down, jmp 4)。最后是右下角的输出节点:
右下角的节点收到 num 后(mov up acc),检查是否是负数。
是负数时,说明收到了 -1,直接跳到第 7 行输出这个 -1(jlz 7, mov acc down)。
若不是负数,则说明接下来还会收到对应的 f[num]。我们将 num 暂时存入 bak(swp),
然后从上方接收 f[num](mov up acc)。
f[num] 为 0 时,表示序列中没有出现过当前的 num,直接跳回第 1 行接收下一组数据(jez 1);
f[num] 不为 0 时,每输出一个 num(swp)
(mov acc down)
(swp)
就将 f[num] 减去 1(sub 1)。
只要 f[num] 没有减到 0,就跳回第 6 行继续输出 num(jnz 6),直到 f[num] 减到 0 后,跳回第 1 行,接收下一组数据。
点击左下角的【RUN】,稍等片刻,便会弹出结算界面:
