【TIS-100 攻略】TIS-NET 第 11 關(guān):序列標(biāo)準(zhǔn)化

本文首發(fā)于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原創(chuàng)不易,轉(zhuǎn)載請注明出處。
TIS-NET 第 11 關(guān)《序列標(biāo)準(zhǔn)化》(Sequence Normalizer)關(guān)卡展示

本關(guān)的 IN 會提供給你一些以 -1 結(jié)尾的序列,你需要找到序列中的最小值,并將序列中的每個值都減去這個最小值后輸出。由于最小值自身也要減去最小值,所以輸出序列中會出現(xiàn) 0,因此本關(guān)的序列改為以 -1 結(jié)尾。
這一關(guān)不算太難,查找一個序列中的最小值,我們早在第 11 關(guān)的時候就做過了(傳送門:【TIS-100 攻略】第 10~11 關(guān):信號模式檢測器、序列峰值檢測器),現(xiàn)在只不過是在第 11 關(guān)的基礎(chǔ)上,將整個序列存儲下來,并在輸出的時候整體減去這個最小值就 OK 了。
只是這里要注意,存儲在棧里的數(shù)據(jù),取出的時候會變成逆序,因此我們第一遍取的時候要先把所有的數(shù)字倒入第二個棧,然后從第二個棧再取一遍,逆序就變回成正序了。
本關(guān)我們分兩步來完成:第一步,將序列中的所有數(shù)字放入棧中,同時找出序列中的最小值。

首先看 IN 下方的節(jié)點:
IN 下方的節(jié)點首先給右邊的棧里塞一個 0 哨兵(mov 0 right),
然后讀入序列里的第一個數(shù)時(mov up acc),
將它給下方節(jié)點(mov acc down)
和右邊的棧各傳一份(mov acc right)。
從第二個數(shù)開始(mov up acc),
照舊復(fù)制一份傳到棧里(mov acc right),但和下方通訊的行為稍有變化:
讀入的數(shù)是負(fù)數(shù)時,跳到第 12 行執(zhí)行(jlz c);
讀入的數(shù)是正數(shù)時,給下方發(fā)送一個 1 信號(mov 1 down),
并將剛才讀入的數(shù)向下發(fā)送兩遍(mov acc down)
(mov acc down)
然后跳回第 5 行繼續(xù)從 IN 讀入更多數(shù)字(jmp 5)。
直到讀入了負(fù)數(shù)后,向下方發(fā)送一個 5 信號(mov 5 down)。
接下來的工作是下一步要做的,我們暫時擱置,暫且先看中央節(jié)點:
中央靠左的節(jié)點首先從上方收到序列里的第一個數(shù),將它放入 acc,暫時視為序列里的最小數(shù) min(mov up acc),
然后開始聽從上方節(jié)點的指令(jro up)。
上方節(jié)點收到正數(shù)時,會給我們發(fā)送一個 1 信號,我們往下跳 1 行,將歷史最小 min 和挑戰(zhàn)者 num 做差值運(yùn)算(sub up),并判定:
如果 min - num > 0,說明 min > num,挑戰(zhàn)者小過了歷史最小,挑戰(zhàn)者獲勝,跳回第 1 行,將挑戰(zhàn)者置為新的歷史最?。╦gz 1, mov up acc);
如果 min - num <= 0,說明 min <= num,挑戰(zhàn)者沒有小過歷史最小,歷史最小獲勝,我們加回一個 num,將 acc 由 min - num 還原成原始 min(add up)
(jmp 2)
上方節(jié)點收到負(fù)數(shù)時,說明到達(dá)了序列結(jié)尾,會給我們發(fā)送一個 5 信號,我們向下跳 5 行,到達(dá)第 7 行,然后接下來的工作是下一步要做的,我們暫時擱置。
以上,我們便完成了第一步。點擊左下角的【RUN】,檢查階段性成果是否正確:

棧里存入了底部的哨兵 0 和包括 -1 在內(nèi)的整個序列,中央靠左的節(jié)點的 acc 的值是 56,的確是這個序列里的最小值。階段性成果是沒有問題的。現(xiàn)在開始做第二步:將序列中的所有數(shù)字都減去這個最小值并輸出。

左邊兩個節(jié)點畫圈的地方是我做了改動的地方。首先,上方節(jié)點接收到 IN 里的 -1 時,會給中間靠左的節(jié)點發(fā)一個 5 信號(mov 5 down),但發(fā)完信號后還不能立刻重新開始工作,需要等待右邊棧里的數(shù)字全部取出來后才能開始把下一個序列里的數(shù)字放進(jìn)去。所以此時上方節(jié)點開始等待下方節(jié)點傳來一個同步信號(mov down nil),傳的是什么值不重要,關(guān)鍵是我收到這個信號后才能開始下一輪的工作。
然后中間靠左的節(jié)點就由第 2 行跳到了第 7 行,將當(dāng)前序列里的 min 發(fā)給右邊(mov acc right)。此時目光暫時暫時移動到中間靠右的節(jié)點處:
它將左邊發(fā)來的 min 直接傳下去(mov left down),
然后將上方棧中的所有數(shù)字,包括棧頂?shù)?-1 和棧底的 0,都塞到右邊的棧里,將原先順序存儲的序列逆序讀出來放入右側(cè)的棧里(mov up acc)
(mov acc right)
(jnz 2)我們把從原始棧里逆序讀出來的序列塞入另一個棧后,再重新讀的時候,棧中的數(shù)字就又回到了順序狀態(tài),原先最外側(cè)的 -1 跑到了最里側(cè),原先最里側(cè)的哨兵 0 跑到了最外側(cè)。
等上方棧里的數(shù)字被清空后,我們把最外側(cè)這個沒用的?0 彈出來,作為同步信號發(fā)給左邊(mov right?left),中間靠左的節(jié)點順勢將收到的這個 0 發(fā)給上面(mov right up),上面收到發(fā)來的 0 后,就知道可以繼續(xù)工作了。它這時候?qū)⑦@個作為同步信號的 0 丟棄(mov down nil),然后跳回第 1 行,開始下一輪工作。
右側(cè)棧里最外層的 0 被彈出后,中間靠右的節(jié)點將剩下的數(shù)字(mov right acc)
全部送給下邊(mov acc down),由下邊將收到的數(shù)字依次減去 min 后輸出,
直到遇到收尾的 -1 為止(jgz 6)。
最后是右下角的節(jié)點:
它首先會收到上方發(fā)來的 min 值(mov up acc)。
由于每次要輸出的是 num - min 的值,所以我們需要持久保存的其實是 -min 的值,將這個 -min 和上方發(fā)來的每個 num 相加,得到 -min + num 并輸出。因此將它取反后(neg)
保存到 bak 中(sav)。
接下來,上方每發(fā)送一個 num,就計算 -min + num 的值(add up),檢查它是否是負(fù)數(shù)。
如果得到的值是負(fù)數(shù),則跳到第 9 行執(zhí)行(jlz 9)。
如果得到的值是非負(fù)數(shù),則直接輸出(mov acc down),
同時將 acc 還原成 -min 的值(swp)
(jmp 3, sav)
如此反復(fù),直到得到了負(fù)數(shù)后,我們就知道上方傳了表示序列末端的 -1,-min + (-1) 當(dāng)然還是負(fù)數(shù),此時我們就直接輸出最終的 -1(mov -1 down),然后跳回第 1 行,準(zhǔn)備從上方接收下一個序列的 min 值。
點擊左下角的【RUN】,運(yùn)行程序:

我們清楚地看到,看到原先存儲在上方棧里的 [0, 90, 97, 56, 72, 83, -1] 倒入到右邊的棧后,變成了 [-1, 83, 72, 56, 97, 90, 0],順序完全顛倒了過來,但是如果我們再讀一遍,順序就又回歸正常的 [0, 90, 97, 56, 72, 83, -1],除了最開始的 0 和最后的 -1,剩下的所有數(shù)字都需要減去 min 后輸出,-1 則是原樣輸出,所以只有開頭的 0 是沒用的,把它當(dāng)作同步信號,讓上方節(jié)點開啟下一輪工作是恰到好處的。
右下角節(jié)點的 acc 和 bak 則是早就設(shè)置為了 -min(-56),等待上方傳來一個又一個 num 值。

第一個序列輸出是完全正常的,除最后的 -1 外,所有的數(shù)字都減去了最小值 56。
繼續(xù)運(yùn)行程序,稍等片刻,便會彈出結(jié)算界面:

非常意外,三項指標(biāo)都在直方圖的最左端!我該不會做了一個十全十美的方案吧?