【TIS-100 攻略】TIS-NET 第 12 關(guān):測(cè)試圖 3

本文首發(fā)于 B 站《TIS-100》文集(https://www.bilibili.com/read/readlist/rl626023)。原創(chuàng)不易,轉(zhuǎn)載請(qǐng)注明出處。
TIS-NET 第 12 關(guān)《測(cè)試圖 3》(Image Pattern 3)關(guān)卡展示

回字有四種寫法,你知道嗎?本關(guān)要畫的回字就是其中一種寫法哦~
開個(gè)玩笑。本關(guān)要畫的回字其實(shí)是由 10 條水平線和 8 條垂直線構(gòu)成的。我們分兩步來完成,第一步把所有的水平線畫出來,這些水平線依次為:
以 (0, 0) 起始,長度為 30 的水平線;
以 (2, 2) 起始,長度為 26 的水平線;
以 (4, 4) 起始,長度為 22 的水平線;
以 (6, 6) 起始,長度為 18 的水平線;
以 (8, 8) 起始,長度為 14 的水平線;
以 (8, 9) 起始,長度為 14 的水平線;
以 (6, 11) 起始,長度為 18 的水平線;
以 (4, 13) 起始,長度為 22 的水平線;
以 (2, 15) 起始,長度為 26 的水平線;
以 (0, 17) 起始,長度為 30 的水平線。
水平線的長度 w 和起始點(diǎn)的 x 坐標(biāo)呈現(xiàn)這樣一種對(duì)應(yīng)關(guān)系:

因此我們將一個(gè)節(jié)點(diǎn)配置為提供起始 (x, y) 坐標(biāo)的無限流,畫圖節(jié)點(diǎn)向 image 模塊傳出起始 (x, y) 后,根據(jù) x 的值計(jì)算出 w 的值,并發(fā)送 w 次 3(白色)后,發(fā)送一次 -1,即可完成一條水平線的繪制。畫水平線的代碼如下:

中央節(jié)點(diǎn)提供的是水平線起始坐標(biāo) (x, y) 的無限流,前 10 行很好理解,依次提供了 (0, 17)、(2, 15)、(4, 13)、(6, 11)、(8, 9) 這幾個(gè)起始點(diǎn)(mov 0 down, mov 17 down, mov 2 down, mov 15 down, mov 4 down, mov 13 down, mov 6 down, mov 11 down, mov 8 down, mov 9 down)。第 11~15 行則提供的是 (8, 8)、(6, 6)、(4, 4)、(2, 2)、(0, 0) 這幾個(gè)起始點(diǎn):首先將 acc 置為初始值 8(mov 8 acc),然后每向下發(fā)送兩次 acc(mov acc down, mov acc down),就將 acc 減去 2 后(sub 2)跳回到第 12 行繼續(xù)發(fā)(jmp c)。當(dāng) acc 減到 -2 后,下方的畫圖節(jié)點(diǎn)會(huì)發(fā)現(xiàn)自己收到了一個(gè)負(fù)數(shù),就會(huì)停止畫水平線。然后看下方的畫圖節(jié)點(diǎn):
下方的畫圖節(jié)點(diǎn)首先從中央節(jié)點(diǎn)接收一個(gè)起始 x 坐標(biāo)(mov up acc),
如果收到的是負(fù)數(shù),說明水平線畫完了,跳到第 13 行(jlz d)。
收到的不是負(fù)數(shù)時(shí),將起始 x 坐標(biāo)(mov acc down)
和接下來收到的起始 y 坐標(biāo)一并發(fā)給下方的 image 輸出(mov up down)。
接下來我們根據(jù) x 來計(jì)算 w:因?yàn)?w = 30 - 2x,所以我們首先將 x 乘以 -2(add acc)
(neg)
然后再加上 30(add 30),便得到了 w 的值。
第 8~10 行的循環(huán)是為了給下方發(fā)送 w 次 3(白色)的(mov 3 down)
(sub 1)
(jnz 8)
發(fā)完 w 次 3 后,發(fā)送一個(gè) -1 停止繪制本條水平線(mov -1 down),
然后跳回第一行(jmp 1),接收下一組起始 (x, y)。
直到收到的起始 x 變?yōu)樨?fù)數(shù)后,跳到第 13 行,準(zhǔn)備執(zhí)行后續(xù)的畫垂直線的任務(wù)。
點(diǎn)擊左下角的【RUN】,檢查階段性成果:

很好,所有水平線都畫出來了。接下來我們進(jìn)入第二步:畫垂直線。這些垂直線依次為:
以 (0, 1) 起始,長度為 16 的垂直線;
以 (2, 3) 起始,長度為 12 的垂直線;
以 (4, 5) 起始,長度為 8 的垂直線;
以 (6, 7) 起始,長度為 4 的垂直線;
以 (23, 7) 起始,長度為 4 的垂直線;
以 (25, 5) 起始,長度為 8 的垂直線;
以 (27, 3) 起始,長度為 12 的垂直線;
以 (29, 1) 起始,長度為 16 的垂直線。
垂直線的高度 h 和起始點(diǎn)的 y 坐標(biāo)呈現(xiàn)這樣一種對(duì)應(yīng)關(guān)系:

和畫水平線不同,垂直線不能像水平線一樣靠連續(xù)提供顏色值的方法來畫,只能一個(gè)像素一個(gè)像素畫。因此每畫一點(diǎn)都要確定實(shí)時(shí)的 (x, y) 坐標(biāo),不能像畫水平線那樣,只要知道起始的 (x, y) 坐標(biāo)就 OK 了。
繪制垂直線時(shí),需要兩個(gè)節(jié)點(diǎn)配合完成:A 節(jié)點(diǎn)用于將起始 (x, y) 坐標(biāo)發(fā)給 B 節(jié)點(diǎn),然后將高度值 h?存到自己的 acc 里。B 節(jié)點(diǎn)收到起始 (x, y) 坐標(biāo)后,先依次向 image 發(fā)送 x, y, 3, -1 這四個(gè)值后,再將 y 加上 1,并聽從 A 節(jié)點(diǎn)的指令。由于剛才已經(jīng)畫了一個(gè)點(diǎn),還需要繼續(xù)畫 h-1 個(gè)點(diǎn)才能將垂直線的所有 h?個(gè)點(diǎn)畫完,因此 A 節(jié)點(diǎn)給 B 節(jié)點(diǎn)發(fā)送 h-1 次【繼續(xù)】命令,最后發(fā)送 1 次【終止】命令,我們就成功畫出了一條垂直線。
原理就是這么簡(jiǎn)單,具體到代碼上,則如下:

首先我們看 image 上方的節(jié)點(diǎn),第 12 行和第 13 行的代碼變成了 mov left down, jmp d。由于這個(gè)節(jié)點(diǎn)已經(jīng)用掉了大量的代碼空間來畫水平線,因此垂直線已經(jīng)無法再繼續(xù)親自畫,只能委托左邊節(jié)點(diǎn)來畫。接下來,它將左下角節(jié)點(diǎn)發(fā)來的所有數(shù)字都無腦傳到 image 中(mov left down, jmp d),也就是說,接下來左下角的節(jié)點(diǎn)變成了事實(shí)上的繪圖節(jié)點(diǎn)。
左上角的節(jié)點(diǎn)被配置成了提供垂直線的起始 (x, y) 坐標(biāo)的無限流。只不過這個(gè)流做了微調(diào),先提供的是 y 坐標(biāo),后提供的是 x 坐標(biāo)。至于為什么要這樣提供,我們后面會(huì)說到。
它的前 8?行代碼很好理解,依次提供了?(29, 1)、(27, 3)、(25, 5)、(23, 7) 這幾個(gè)起始點(diǎn)(mov 1 down, mov 29 down, mov 3 down, mov 27 down, mov 5 down, mov 25 down, mov 7 down, mov 23 down)。
第 9~12 行代碼提供的則是 (6, 7)、(4, 5)、(2, 3)、(0, 1) 這幾個(gè)起始點(diǎn)。我們發(fā)現(xiàn),如果這個(gè)節(jié)點(diǎn)還按常規(guī)方法先提供 x,后提供 y 的話,那么提供的數(shù)字依次是 6、7、4、5、2、3、0、1,增量按照 1、-3、1、-3 的方式循環(huán),代碼如下:
而如果先提供 y,后提供 x 的話,提供的數(shù)字就變成了 7、6、5、4、3、2、1、0,增量恒定為 -1,代碼如下:
使用后者的提供方式,可以省去 2 行代碼行數(shù)。這就是為什么這個(gè)節(jié)點(diǎn)我們先提供 y,再提供 x 的原因。左上角節(jié)點(diǎn)的 9~12 行代碼,和以上列出來的第二個(gè)代碼塊完全一致。
中間靠左的節(jié)點(diǎn)(A 節(jié)點(diǎn))和左下角節(jié)點(diǎn)(B 節(jié)點(diǎn))配合畫垂直線。
A 節(jié)點(diǎn)首先從上方收取起始 y 坐標(biāo),先放到 acc 里暫存,用于將來計(jì)算 h(mov up acc),
再復(fù)制一份發(fā)給 B 節(jié)點(diǎn)(mov acc down)。
上方提供的 x 坐標(biāo)由于自己用不著,所以直接傳給 B 節(jié)點(diǎn)(mov up down)。
因?yàn)?h = 18 - 2y,所以我們首先將 y?乘以 -2(add acc)
(neg)
然后再加上 18,便可得到?h 的值。但是注意到,這里我們的代碼是 add 17。我前不久提了這樣一句話:由于剛才已經(jīng)畫了一個(gè)點(diǎn),還需要繼續(xù)畫 h-1 個(gè)點(diǎn)才能將垂直線的所有 h 個(gè)點(diǎn)畫完,因此 A 節(jié)點(diǎn)給 B 節(jié)點(diǎn)發(fā)送 h-1 次【繼續(xù)】命令,最后發(fā)送 1 次【終止】命令,我們就成功畫出了一條垂直線。因此我們的 acc 需要的是 h-1 的值,而不是 h 的值。因?yàn)?h-1 = 18 - 2y - 1 = 17 - 2y,所以我們這里加上的值是 17。
接下來的第 7~10?行代碼用于給 B 節(jié)點(diǎn)發(fā)送 h-1 次【繼續(xù)】(mov -7 down)
(sub 1)
(jnz 7)
和 1 次【終止】(mov -10 down)。
現(xiàn)在來看 B 節(jié)點(diǎn)。B 節(jié)點(diǎn)已經(jīng)是事實(shí)上的繪圖節(jié)點(diǎn),它右側(cè)的節(jié)點(diǎn)因?yàn)闀?huì)將所有信號(hào)原封不動(dòng)地傳給 image,所以干脆直接將右方視為 image。
它會(huì)首先從 A 節(jié)點(diǎn)處收到一條垂直線的起始 (x, y) 坐標(biāo),我們將先發(fā)來的?y 坐標(biāo)放入 bak(mov up acc)
(swp)
將后發(fā)來的?x 坐標(biāo)放入 acc(mov up acc),接下來就開始繪制垂直線。
我們依次給 image 發(fā)送 x(mov acc right)
y(swp)
(mov acc right)
3(mov 3 right)
-1(mov -1 right)這四個(gè)值,
再將 y 加上 1(add 1)
(swp)
然后聽從 A 節(jié)點(diǎn)的指令(jro up)。A 節(jié)點(diǎn)發(fā)送表示【繼續(xù)】的 -7 時(shí),我們向上跳 7 行,跳回到第 4 行繼續(xù)發(fā)送新的 x, y, 3, -1;A 節(jié)點(diǎn)發(fā)送表示【終止】的 -10 時(shí),我們向上跳 10 行,跳回到第 1 行,重新從 A 節(jié)點(diǎn)接收下一條垂直線的起始?(x, y) 坐標(biāo)。
如此,垂直線便也繪制完成了。點(diǎn)擊左下角的【RUN】,觀察輸出結(jié)果:

非常完美!以上是只差最后一個(gè)點(diǎn)的樣子。因?yàn)橛螒驒C(jī)制的原因,和答案完全對(duì)上后,程序就會(huì)自動(dòng)跳到下一個(gè)樣例,所以和答案一字不差的圖是截不出來的。繼續(xù)運(yùn)行程序,直到彈出結(jié)算界面:

優(yōu)化運(yùn)行速度
我們將代碼進(jìn)行微調(diào),即可【加量不加價(jià)】,在不增加代碼行數(shù)的前提下提高運(yùn)行速度。注意這個(gè)公式:

它也可以寫成

畫水平線時(shí),我們可以將循環(huán)次數(shù)改為 15-x,每次循環(huán)改為發(fā)送兩個(gè) 3(白色)。這樣做有兩個(gè)好處:
①x 只需要變?yōu)?-x,不需要變?yōu)?-2x,可以省去一次乘以 2 的運(yùn)算;
②循環(huán)次數(shù)減少了一半,可以節(jié)省一半的判斷次數(shù)。

以上選中的部分即為本次改動(dòng)的部分。另外左下角的第 7~10 行代碼可以適當(dāng)調(diào)整順序:

將 add 1 和 swp 這兩條自己做事的指令分別插到了 mov 3 right 和 mov -1 right 這兩條傳話指令的前面。因?yàn)橛蚁陆堑墓?jié)點(diǎn)將發(fā)來的值傳給 image 后(mov left down),需要耗費(fèi)一個(gè)周期跳轉(zhuǎn)回第 13 行(jmp d),而這一個(gè)周期里,我們的話是傳不過去的。所以我們不妨把這個(gè)時(shí)間空當(dāng)利用起來,在這一個(gè)周期里做一件自己的事,便可大幅提高運(yùn)行效率。

節(jié)點(diǎn)數(shù)和代碼行數(shù)和上一版方案一致,依然是 5 個(gè)節(jié)點(diǎn)和 62 行代碼。但運(yùn)行效率由 2156 周期提升到了 1768 周期,本方案加量不加價(jià),完爆了上一版方案。