【TIS-100 攻略】TIS-NET 第 11 关:序列标准化

本文首发于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原创不易,转载请注明出处。
TIS-NET 第 11 关《序列标准化》(Sequence Normalizer)关卡展示

本关的 IN 会提供给你一些以 -1 结尾的序列,你需要找到序列中的最小值,并将序列中的每个值都减去这个最小值后输出。由于最小值自身也要减去最小值,所以输出序列中会出现 0,因此本关的序列改为以 -1 结尾。
这一关不算太难,查找一个序列中的最小值,我们早在第 11 关的时候就做过了(传送门:【TIS-100 攻略】第 10~11 关:信号模式检测器、序列峰值检测器),现在只不过是在第 11 关的基础上,将整个序列存储下来,并在输出的时候整体减去这个最小值就 OK 了。
只是这里要注意,存储在栈里的数据,取出的时候会变成逆序,因此我们第一遍取的时候要先把所有的数字倒入第二个栈,然后从第二个栈再取一遍,逆序就变回成正序了。
本关我们分两步来完成:第一步,将序列中的所有数字放入栈中,同时找出序列中的最小值。

首先看 IN 下方的节点:
IN 下方的节点首先给右边的栈里塞一个 0 哨兵(mov 0 right),
然后读入序列里的第一个数时(mov up acc),
将它给下方节点(mov acc down)
和右边的栈各传一份(mov acc right)。
从第二个数开始(mov up acc),
照旧复制一份传到栈里(mov acc right),但和下方通讯的行为稍有变化:
读入的数是负数时,跳到第 12 行执行(jlz c);
读入的数是正数时,给下方发送一个 1 信号(mov 1 down),
并将刚才读入的数向下发送两遍(mov acc down)
(mov acc down)
然后跳回第 5 行继续从 IN 读入更多数字(jmp 5)。
直到读入了负数后,向下方发送一个 5 信号(mov 5 down)。
接下来的工作是下一步要做的,我们暂时搁置,暂且先看中央节点:
中央靠左的节点首先从上方收到序列里的第一个数,将它放入 acc,暂时视为序列里的最小数 min(mov up acc),
然后开始听从上方节点的指令(jro up)。
上方节点收到正数时,会给我们发送一个 1 信号,我们往下跳 1 行,将历史最小 min 和挑战者 num 做差值运算(sub up),并判定:
如果 min - num > 0,说明 min > num,挑战者小过了历史最小,挑战者获胜,跳回第 1 行,将挑战者置为新的历史最小(jgz 1, mov up acc);
如果 min - num <= 0,说明 min <= num,挑战者没有小过历史最小,历史最小获胜,我们加回一个 num,将 acc 由 min - num 还原成原始 min(add up)
(jmp 2)
上方节点收到负数时,说明到达了序列结尾,会给我们发送一个 5 信号,我们向下跳 5 行,到达第 7 行,然后接下来的工作是下一步要做的,我们暂时搁置。
以上,我们便完成了第一步。点击左下角的【RUN】,检查阶段性成果是否正确:

栈里存入了底部的哨兵 0 和包括 -1 在内的整个序列,中央靠左的节点的 acc 的值是 56,的确是这个序列里的最小值。阶段性成果是没有问题的。现在开始做第二步:将序列中的所有数字都减去这个最小值并输出。

左边两个节点画圈的地方是我做了改动的地方。首先,上方节点接收到 IN 里的 -1 时,会给中间靠左的节点发一个 5 信号(mov 5 down),但发完信号后还不能立刻重新开始工作,需要等待右边栈里的数字全部取出来后才能开始把下一个序列里的数字放进去。所以此时上方节点开始等待下方节点传来一个同步信号(mov down nil),传的是什么值不重要,关键是我收到这个信号后才能开始下一轮的工作。
然后中间靠左的节点就由第 2 行跳到了第 7 行,将当前序列里的 min 发给右边(mov acc right)。此时目光暂时暂时移动到中间靠右的节点处:
它将左边发来的 min 直接传下去(mov left down),
然后将上方栈中的所有数字,包括栈顶的 -1 和栈底的 0,都塞到右边的栈里,将原先顺序存储的序列逆序读出来放入右侧的栈里(mov up acc)
(mov acc right)
(jnz 2)我们把从原始栈里逆序读出来的序列塞入另一个栈后,再重新读的时候,栈中的数字就又回到了顺序状态,原先最外侧的 -1 跑到了最里侧,原先最里侧的哨兵 0 跑到了最外侧。
等上方栈里的数字被清空后,我们把最外侧这个没用的 0 弹出来,作为同步信号发给左边(mov right left),中间靠左的节点顺势将收到的这个 0 发给上面(mov right up),上面收到发来的 0 后,就知道可以继续工作了。它这时候将这个作为同步信号的 0 丢弃(mov down nil),然后跳回第 1 行,开始下一轮工作。
右侧栈里最外层的 0 被弹出后,中间靠右的节点将剩下的数字(mov right acc)
全部送给下边(mov acc down),由下边将收到的数字依次减去 min 后输出,
直到遇到收尾的 -1 为止(jgz 6)。
最后是右下角的节点:
它首先会收到上方发来的 min 值(mov up acc)。
由于每次要输出的是 num - min 的值,所以我们需要持久保存的其实是 -min 的值,将这个 -min 和上方发来的每个 num 相加,得到 -min + num 并输出。因此将它取反后(neg)
保存到 bak 中(sav)。
接下来,上方每发送一个 num,就计算 -min + num 的值(add up),检查它是否是负数。
如果得到的值是负数,则跳到第 9 行执行(jlz 9)。
如果得到的值是非负数,则直接输出(mov acc down),
同时将 acc 还原成 -min 的值(swp)
(jmp 3, sav)
如此反复,直到得到了负数后,我们就知道上方传了表示序列末端的 -1,-min + (-1) 当然还是负数,此时我们就直接输出最终的 -1(mov -1 down),然后跳回第 1 行,准备从上方接收下一个序列的 min 值。
点击左下角的【RUN】,运行程序:

我们清楚地看到,看到原先存储在上方栈里的 [0, 90, 97, 56, 72, 83, -1] 倒入到右边的栈后,变成了 [-1, 83, 72, 56, 97, 90, 0],顺序完全颠倒了过来,但是如果我们再读一遍,顺序就又回归正常的 [0, 90, 97, 56, 72, 83, -1],除了最开始的 0 和最后的 -1,剩下的所有数字都需要减去 min 后输出,-1 则是原样输出,所以只有开头的 0 是没用的,把它当作同步信号,让上方节点开启下一轮工作是恰到好处的。
右下角节点的 acc 和 bak 则是早就设置为了 -min(-56),等待上方传来一个又一个 num 值。

第一个序列输出是完全正常的,除最后的 -1 外,所有的数字都减去了最小值 56。
继续运行程序,稍等片刻,便会弹出结算界面:

非常意外,三项指标都在直方图的最左端!我该不会做了一个十全十美的方案吧?