RECAP2025: 留白

前幾天才猛然驚覺,按照慣例,是時候寫年終總結了。盯著白花花的螢幕,半天拼湊不出一句話。似乎 2025 是注定被遺忘的一年。 既然這是難以回憶難以定義的一年,不如我們留白。 歲末 有一位詩人寫過一首詩。我把它原封不動摘錄在這裡。 從一年的開始到終結 我走了多年 讓歲月彎成了弓 到處是退休者的鞋 私人的塵土 公共的垃圾 這是並不重要的一年 鐵錘閒著,而我 向以後的日子借光 瞥見一把白金尺 在鐵砧上 北島 夢 十一月,似乎記得做過一個夢。 一束刺眼的強光,手術台般,直射頭頂,似乎要剝離我的靈魂。流水線廠房裡,只能聽到工業巨獸的咆哮,什麼也聽不見。感官被屏蔽了。等到我恢復知覺,肉體已隨著傳送帶前行。傳送帶的盡頭是什麼?前方是絕對的虛無。什麼也看不見,什麼也聽不見。 這不對。驚起,我開始大喊。沒有響應。 廠房無限延伸。黑暗中,平行的傳送帶時刻不停。金屬零件整齊排列。冰冷的秩序令人膽顫心驚。當你仔細望過去,才發現那些零件竟然是人。按照不知道什麼規則分類排布在各自的傳送帶上,一聲不響。他們並沒有戴著鐐銬,但他們一動不動。 是誰在操縱? 廠房全自動化作業。頭頂的攝影機和感測器監視著一切。 我開始往反方向跑,試圖喚醒周圍的人,沒有回應。 我奔跑,傳送帶時刻不停。 我在跑。 我只能跑。 我是誰 醒來時,我忘記了我是誰。 但是,還有書籍。 我研究,我晝夜不停,我追尋答案。 一月是從未涉足的故鄉,西半球正在下雪。 二月用常識的謊言背叛了自己。 三月沿青石台階一路向上,從一數到一百。 四月在湖畔閒坐。 五月為徒勞日夜兼程。 六月是結束的開始。 七月在山間行走。 八月有些中暑。 九月十月十一月。 十二月。 留白。 我是誰? 這段歷史沒有姓名,就連編號也找不到。 也沒有書籍,似乎失去了靈魂。 午夜飛行(續) 二月,我登上午夜的飛機,反抗黑夜。惶恐穿越子午線。 也許我不應該離開,其實我應該留下。 我應該做什麼? 他登上火車,前往故鄉。故鄉是哪裡?他不知道。故鄉只存在於記憶深處,或是夢中。或許這一切都不存在。窗外是多少朝代。傾倒的電線杆,無盡的平原,高山,水壩。世界的盡頭。多少片雪花無聲落下。他什麼也不知道。一切都消散在風雨中。 你想去看看長城。你說你背負著五千年,從昨天就忍痛前行,行走在這條泥濘的荊棘路上。你從未走出這片森林,從未離開這座大山。你日夜不停。你不知道這意味著什麼。 他感受到畏懼。他似乎想起為什麼啟程。一切都由他們來定義。他們是誰?他不相信這平原竟是懸崖峭壁,他不相信噩夢和神話。他學著眾人的模樣,在淺灘上奔跑。 你追隨祖先走向大海。那艘巨艦並未帶走你的先輩,故步自封,信仰成為棺木。你的先祖曾經堅守,被背叛,被遺忘,結束自己卑微的一生。你沒有記憶,歷史選擇忘記。 他沒有犯錯,他不相信,他惶恐。他什麼也不知道,他什麼也不相信。他相信嗎?你相信嗎?這故鄉不屬於你,這大山與你無關。或許故鄉只是一個精神上的坐標。他沒有犯錯,卻要接受刑罰。什麼是正義?沒有人解釋。清楚一切,被抹去。不需要解釋。 你自認為與眾不同,你不願意接受,你試著唱反調。他們需要你接受。你非要挑戰。你發現你窒息在空氣中。 你不再做夢,你沒有主義,你無家可歸,你四海為家。你在沙漠公路上前行,你沒有追隨者。 一輪明月,你只看見殉道者的鮮血,為誰而流?他只想苟且偷生。你呼喚他午夜飛行。真理和真理之間藏著謊言。你選擇追隨祖先,追隨感覺。 他覺得沒必要相信,但他做不到。他只能在月光下趟水過河,冰冷的河水使他不得不保持緘默。而緘默是最沉痛的懦弱。 我們都是貪生怕死之輩。現狀是一潭死水。我們任人擺布。 你要去哪裡?你還相信嗎? 你不相信夢想,你不相信陳見,你不相信權力和暴力,你不相信太陽,你不相信神話,你不相信別人,你不相信規則,你不相信故鄉,你不相信過去,你不相信飛行,你不相信雲,你不相信歷史,你不相信直覺你不相信冰塊你不相信大海你不相信微塵你不相信革命你不相信政治你不相信謊言你不相信幻覺不相信收音機不相信信標不相信城牆不相信詩人不相信江山不相信愛情不相信生活不相信文化不相信天氣。你什麼都不相信。你還相信什麼?你還相信相信嗎? 真的嗎? 在錯綜複雜的迷宮中,我選擇放棄抵抗,跪在雨後的林間小道上,雙膝上沾滿了泥濘。我選擇了從一開始就選擇的一條路,選擇低下頭,俯身穿過荊棘。雖然我知道這條路通向哪裡,雖然我知道正確答案。 我選擇了恐懼。 午夜飛行,我向未知妥協。 我背叛了黎明。後悔已沒有意義。 你還是自由的嗎? 最後的特拉阿托尼 當西班牙征服者攻入特諾奇蒂特蘭 當城邦被水淹沒 當神殿之上建立起天主教教堂 夸烏特莫克成為最後的特拉阿托尼 雙足被燙傷 “阿茲特克人沒有藏金” 烈火吞噬火槍 仙人掌被水淹沒 “西班牙人懂得尊重勇敢” 絞刑架揭穿謊言 到底什麼才是真理? 征服者失眠 戰船傾倒 帝國隕落 城市裡樹立起紀念碑 文明毀滅之後永不歸來 在煙霧繚繞的黑曜石鏡中 第五個太陽疲憊地闔上眼簾 不再有搏動的心臟 餵養它日益黯淡的光芒 東方海面吹來的風 沒有帶來羽蛇神的承諾 只送來了披著鐵甲的蝗蟲 和一種無法被祭祀治癒的瘟疫 蘆葦束斷裂的聲音 淹沒在陌生銅鐘的轟鳴裡 時間之輪卡死在最後的紀元 不再轉動 在特斯科科湖乾涸的夢境下方 一條巨大的、看不見的蛇 在淤泥中緩慢地蛻皮 留下一具名叫特諾奇蒂特蘭的空殼 等待一個永遠不會升起的黎明 城市穿過阿茲特克的胸腔 永恆消失的帝國 Zócalo/Tenochtitlan 被地鐵站牌記憶 雨西湖 飛機降落在七月。我偏離只有草地的晴天,走向暴雨中的西湖。 但我仍然會想起你。夜間,高鐵在黑夜裡撕開一道縫隙,電話那頭的聲音被電流聲撕碎。追趕時光。錯位的車票。疾馳的火車。車站和夜晚都不會再等待。 我的傘禁受不了風雨。我在寶石山下的木屋裡等待雨停。 陌生的旅客,傘下的遊人。 遠山隱入蒼冥,斷橋遁入虛白。 硯台被打翻了,黑雲遮蔽錢塘的街市。湖水之下卻永遠寧靜。湖面和天空密謀了一場大霧,把保俶塔的尖頂也藏了起來。湖水之上,雨成為牢籠,成片倒下來,淋不醒夢中的人。 我在湖邊行走。這是多少次來到杭州? 雨點切斷聯結。人們只匆匆趕路。環湖的路上已經沒有行人,斷橋上卻擠滿了遊客,停滯不前。傘面下的世界縮減到了方寸之間,我們在此刻互為盲人。有誰會探頭望向灰暗的湖面和天空,感受雨點擊打在面頰上,看著石階上瀑布般的水流。逆行上山。轉頭被石壁中的佛像嚇到。石像沉睡千年,永遠都不會醒來,永遠沉醉於昨日繁華。我匆忙尋找避雨的屋簷。土腥味指引我來到山腳下的木屋。雨點浸濕了我的衣服,汗水和雨融合到一起,脊背冰冷,也如同夜晚西湖上的風。 電話中聽到你的聲音。空調吐出的寒氣比窗外的雨更凜冽,或許也是因為我全身濕透。一頓簡單的 KFC,成為暴雨中唯一的慰藉。或許你們去聚餐遊戲?但我永遠都不屬於這裡,不屬於人群和都市。 高鐵斬碎夜色和西湖的夢,西湖萬古不變,西湖的雨萬古不變。 或許一陣風來,又是暖風熏得遊人醉。 雨西湖為何而留白? 張岱是否也這樣回憶臨安? 有些事情,逝去了就不會再來。有些人逝去了,如雨入湖中,無跡可尋。 休言萬事轉頭空,未轉頭時皆夢? 旅行 我們在盛夏走向大山。記不起景德鎮的窯火,或是廬山,或是張家界,或是長沙。記憶吞噬一切,模糊。但旅行的意義,在於行走,在於一次旅行之後仍然期待下一次前行。 四月就曾騎行江畔,點幾串燒烤,吹江風。卻又在深夜彷徨,突如其來,或是有所徵兆,不知所措。 南方的小城,租一輛電動車。我開錯導航,誤入歧途。沒有責怪。重新認識你,或許今後又很難常見。用一個簡單的謊言掩蓋事實,是自我保護還是連自己也欺騙,不願意承認。但在七月的烈日之下,騎車自由遊蕩,不需要任何計劃,點一杯奶茶,走到深巷裡吃當地菜餚。或許又在山間酒醉,綠茶啤酒,看著晚霞逐漸熄滅。夜遊長沙,遁入黑夜的不止有那天的星空。乘船來到東海小島,其實並未做任何計劃,甚至是購買的最後幾張票。行走在環島海濱公路上,這裡的大海或許和往日不同。 我們為何啟程?我們何時再次啟程? 留白 · 斷章 一年的盡頭為新的一年留白。 不再偽裝勇敢,不再滿身疲憊,在清醒中假裝沉睡。 重新整理時間線,讀一些書,走一些路。 游標在空白上跳動。我試圖尋找意義,把這一年填滿。我發現我似乎失去了過去的力量,失去了語言,我試圖定義,卻發現最有力量的文字藏在那些被我刪除的段落,被我遺忘的話語,我不敢承認也不敢想的語言。最後都化為一聲嘆息,成為 12 月的一場大雪。 2025 年注定是一場大霧。那就留白。沒有人能夠定義這一切。 繼續保持清醒。 刪去一萬字,留下空白。 在這空白裡,請聽一聽你內心的聲音。

2025/12/31
articleCard.readMore

CSAPP Bomb Lab 解析

做完了 CSAPP Bomb Lab,寫一篇解析。 題目要求 運行一個二進制文件 bomb,它包括六個”階段(phase)”,每個階段要求學生通過 stdin 輸入一個特定的字串。如果輸入了預期的字串,那麼該階段被”拆除”,進入下一個階段,直到所有炸彈被成功”拆除”。否則,炸彈就會”爆炸”,列印出”BOOM!!!” 環境 這個系統是在 x86_64 Linux 上運行的,而筆者的環境是 ARM 架構的 macOS (Apple Silicon)。 弄了半天 docker,虛擬化一個 x86_64 Ubuntu 出來,結果裡面的 gdb 不能用,不想折騰。 發現 educoder 上面有環境,可以直接用,而且免費,於是就在 educoder 上面完成了本實驗。 地址:https://www.educoder.net/paths/6g398fky 前置知識 本實驗要求掌握 gdb 的一些指令。 1. 啟動與退出 (Startup & Exit) 指令縮寫描述 gdb executable-啟動 GDB 並載入可執行文件。 run [args]r開始運行程序。如果有命令行參數,跟在後面(如 r input.txt)。 quitq退出 GDB。 start-運行程序並在 main 函數的第一行自動暫停(省去手動打斷點的麻煩)。 set args ...-設置運行時的參數(在 r 之前使用)。 2. 斷點管理 (Breakpoints) 指令縮寫描述範例 break <loc>b設置斷點。支持函數名、行號、檔案名:行號。b main b 15 b file.c:20 info breakpointsi b查看當前所有斷點及其編號 (Num)。- delete <Num>d刪除指定編號的斷點。不加編號則刪除所有。d 1 disable/enable <Num>-暫時禁用或啟用某個斷點(保留配置但不生效)。disable 2 break ... if <cond>-條件斷點:僅當條件為真時才暫停(非常有用)。b 10 if i==5 3. 執行控制 (Execution Control) 指令縮寫描述區別點 nextn單步跳過。執行下一行程式碼。如果遇到函數調用,不進入函數內部,直接執行完該函數。 steps單步進入。執行下一行程式碼。如果遇到函數調用,進入函數內部逐行除錯。 continuec繼續運行,直到遇到下一個斷點或程序結束。- finish-執行直到當前函數返回。當你不小心 s 進了一個不想看的庫函數時,用這個跳出來。 until <line>u運行直到指定行號。常用於快速跳出循環。 4. 查看數據 (Inspection) 指令縮寫描述 print <var>p列印變數的值。支持表達式(如 p index + 1)。 display <var>-持續顯示。每次程序暫停時,自動列印該變數的值(適合跟蹤循環中的變數)。 info locals-列印當前棧幀中所有局部變數的值。 whatis <var>-查看變數的數據類型。 ptype <struct>-查看結構體或類的具體定義(成員列表)。 x /nfu <addr>x查看記憶體。n是數量,f是格式(x=hex, d=dec, s=str),u是單位(b=byte, w=word)。 例如:x/10xw &array (以16進制顯示數組前10個word)。 5. 堆棧與上下文 (Stack & Context) 指令縮寫描述 backtracebt查看調用棧。顯示程序崩潰時的函數調用路徑(從 main 到當前函數)。 frame <Num>f切換到指定的堆棧幀(配合 bt 看到的編號)。切換後可以用 p 查看該層函數的局部變數。 listl顯示當前行附近的原始碼。 6. 提升體驗:TUI 模式 (Text User Interface) layout src:螢幕分為兩半,上面顯示原始碼和當前執行行,下面是命令窗口。(強烈推薦) layout asm:顯示匯編代碼。 layout split:同時顯示原始碼和匯編。 反匯編 我們可以使用 objdump 直接進行反匯編,查看匯編原始碼。 1 objdump -d bomb > bomb.asm 我們可以觀察到,幾個 phase 其實是幾個函數,phase_x()。 strings 在終端輸入: 1 strings bomb 這會把 bomb 文件裡所有連續的可列印字元(ASCII)都列印出來。 Phase 1 我們先看看 phase_1 長什麼樣子,disas phase_1 1 2 3 4 5 6 7 8 9 10 Dump of assembler code for function phase_1: 0x0000000000400ee0 <+0>: sub $0x8,%rsp 0x0000000000400ee4 <+4>: mov $0x402400,%esi 0x0000000000400ee9 <+9>: callq 0x401338 <strings_not_equal> 0x0000000000400eee <+14>: test %eax,%eax 0x0000000000400ef0 <+16>: je 0x400ef7 <phase_1+23> 0x0000000000400ef2 <+18>: callq 0x40143a <explode_bomb> 0x0000000000400ef7 <+23>: add $0x8,%rsp 0x0000000000400efb <+27>: retq End of assembler dump. sub $0x8,%rsp 是設置棧幀,在這裡不用管。 mov $0x402400,%esi 和 callq 0x401338 <strings_not_equal> 似乎進行了字串的 strcmp。 接下來 je 0x400ef7 <phase_1+23> 就很明顯了,如果相等跳出炸彈。 設置斷點,b phase_1 之後運行程序,r,隨便輸入一些內容,就可以觸發斷點 以字串形式查看 0x402400 所指向的記憶體:x/s 0x402400 1 0x402400: "Border relations with Canada have never been better." 我們找到了答案。 Phase 2 還是先反匯編: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Dump of assembler code for function phase_2: 0x0000000000400efc <+0>: push %rbp 0x0000000000400efd <+1>: push %rbx 0x0000000000400efe <+2>: sub $0x28,%rsp 0x0000000000400f02 <+6>: mov %rsp,%rsi 0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers> 0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) 0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> 0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb> 0x0000000000400f15 <+25>: jmp 0x400f30 <phase_2+52> 0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax 0x0000000000400f1a <+30>: add %eax,%eax 0x0000000000400f1c <+32>: cmp %eax,(%rbx) 0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41> 0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb> 0x0000000000400f25 <+41>: add $0x4,%rbx 0x0000000000400f29 <+45>: cmp %rbp,%rbx 0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27> 0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64> 0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx 0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp 0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27> 0x0000000000400f3c <+64>: add $0x28,%rsp 0x0000000000400f40 <+68>: pop %rbx 0x0000000000400f41 <+69>: pop %rbp 0x0000000000400f42 <+70>: retq End of assembler dump. 0x0000000000400f05 <+9>: callq 0x40145c <read_six_numbers> 這裡看到 read_six_numbers 我們可以反匯編 read_six_numbers 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Dump of assembler code for function read_six_numbers: 0x000000000040145c <+0>: sub $0x18,%rsp 0x0000000000401460 <+4>: mov %rsi,%rdx 0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx 0x0000000000401467 <+11>: lea 0x14(%rsi),%rax 0x000000000040146b <+15>: mov %rax,0x8(%rsp) 0x0000000000401470 <+20>: lea 0x10(%rsi),%rax 0x0000000000401474 <+24>: mov %rax,(%rsp) 0x0000000000401478 <+28>: lea 0xc(%rsi),%r9 0x000000000040147c <+32>: lea 0x8(%rsi),%r8 0x0000000000401480 <+36>: mov $0x4025c3,%esi 0x0000000000401485 <+41>: mov $0x0,%eax 0x000000000040148a <+46>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x000000000040148f <+51>: cmp $0x5,%eax 0x0000000000401492 <+54>: jg 0x401499 <read_six_numbers+61> 0x0000000000401494 <+56>: callq 0x40143a <explode_bomb> 0x0000000000401499 <+61>: add $0x18,%rsp 0x000000000040149d <+65>: retq End of assembler dump. 看到有一行 callq 0x400bf0 <__isoc99_sscanf@plt>,調用了 sscanf 我們看一眼 $0x4025c3,x/s 0x4025c3,得到 %d %d %d %d %d %d,確實是讀了六個數字。 函數調用時,參數多於六個,就會丟到棧裡面去。我們看到: 1 2 3 4 5 6 7 8 0x0000000000401460 <+4>: mov %rsi,%rdx 0x0000000000401463 <+7>: lea 0x4(%rsi),%rcx 0x0000000000401467 <+11>: lea 0x14(%rsi),%rax 0x000000000040146b <+15>: mov %rax,0x8(%rsp) 0x0000000000401470 <+20>: lea 0x10(%rsi),%rax 0x0000000000401474 <+24>: mov %rax,(%rsp) 0x0000000000401478 <+28>: lea 0xc(%rsi),%r9 0x000000000040147c <+32>: lea 0x8(%rsi),%r8 參數順序:rdi, rsi, rdx, rcx, r8, r9,超過了六個參數。rsp 為棧頂指針,多於六個的參數存在棧上。 於是讀取的六個數字依次存為:rsi, rsi+4, rsi+8, rsi+12, rsi+16 (0x10 = 16), rsi+20 (0x14 = 20) 再回到 phase_2 1 0x0000000000400f02 <+6>: mov %rsp,%rsi 棧頂指針作為參數傳入了 read_six_numbers,因此,這六個數字應該是在 phase_2 對應棧幀的棧上 1 2 3 0x0000000000400f0a <+14>: cmpl $0x1,(%rsp) 0x0000000000400f0e <+18>: je 0x400f30 <phase_2+52> 0x0000000000400f10 <+20>: callq 0x40143a <explode_bomb> 這裡判斷棧頂元素是否是 1,也就是說第一個元素是否是 1 之後跳轉到了 0x400f30 1 2 3 4 5 6 7 8 9 10 11 12 0x0000000000400f17 <+27>: mov -0x4(%rbx),%eax 0x0000000000400f1a <+30>: add %eax,%eax 0x0000000000400f1c <+32>: cmp %eax,(%rbx) 0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41> 0x0000000000400f20 <+36>: callq 0x40143a <explode_bomb> 0x0000000000400f25 <+41>: add $0x4,%rbx 0x0000000000400f29 <+45>: cmp %rbp,%rbx 0x0000000000400f2c <+48>: jne 0x400f17 <phase_2+27> 0x0000000000400f2e <+50>: jmp 0x400f3c <phase_2+64> 0x0000000000400f30 <+52>: lea 0x4(%rsp),%rbx 0x0000000000400f35 <+57>: lea 0x18(%rsp),%rbp 0x0000000000400f3a <+62>: jmp 0x400f17 <phase_2+27> 這裡很顯然是一個循環,依次讀取六個數位(每次移動四個位元組,正好是 int 的長度) 1 2 3 0x0000000000400f1a <+30>: add %eax,%eax 0x0000000000400f1c <+32>: cmp %eax,(%rbx) 0x0000000000400f1e <+34>: je 0x400f25 <phase_2+41> 這六個數字,後一個是前一個的兩倍。 於是我們可以得到答案:1 2 4 8 16 32 我們也可以把代碼翻譯成 C 語言: 1 2 3 4 5 6 7 8 9 10 for (int i = 1; i < 6; i++) { // mov -0x4(%rbx), %eax int previous = num[i-1]; // add %eax, %eax int expected = previous + previous; // cmp %eax, (%rbx) if (num[i] != expected) { explode_bomb(); } } Phase 3 反匯編: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 Dump of assembler code for function phase_3: 0x0000000000400f43 <+0>: sub $0x18,%rsp 0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx 0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx 0x0000000000400f51 <+14>: mov $0x4025cf,%esi 0x0000000000400f56 <+19>: mov $0x0,%eax 0x0000000000400f5b <+24>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x0000000000400f60 <+29>: cmp $0x1,%eax 0x0000000000400f63 <+32>: jg 0x400f6a <phase_3+39> 0x0000000000400f65 <+34>: callq 0x40143a <explode_bomb> 0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) 0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> 0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax 0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) 0x0000000000400f7c <+57>: mov $0xcf,%eax 0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123> 0x0000000000400f83 <+64>: mov $0x2c3,%eax 0x0000000000400f88 <+69>: jmp 0x400fbe <phase_3+123> 0x0000000000400f8a <+71>: mov $0x100,%eax 0x0000000000400f8f <+76>: jmp 0x400fbe <phase_3+123> 0x0000000000400f91 <+78>: mov $0x185,%eax 0x0000000000400f96 <+83>: jmp 0x400fbe <phase_3+123> 0x0000000000400f98 <+85>: mov $0xce,%eax 0x0000000000400f9d <+90>: jmp 0x400fbe <phase_3+123> 0x0000000000400f9f <+92>: mov $0x2aa,%eax 0x0000000000400fa4 <+97>: jmp 0x400fbe <phase_3+123> 0x0000000000400fa6 <+99>: mov $0x147,%eax 0x0000000000400fab <+104>: jmp 0x400fbe <phase_3+123> 0x0000000000400fad <+106>: callq 0x40143a <explode_bomb> 0x0000000000400fb2 <+111>: mov $0x0,%eax 0x0000000000400fb7 <+116>: jmp 0x400fbe <phase_3+123> 0x0000000000400fb9 <+118>: mov $0x137,%eax 0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax 0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134> 0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb> 0x0000000000400fc9 <+134>: add $0x18,%rsp 0x0000000000400fcd <+138>: retq 看著有點複雜,觀察到 sscanf 看一眼 0x4025cf,x/s 0x4025cf,得到 %d %d,看起來是輸入了兩個整數 1 2 0x0000000000400f47 <+4>: lea 0xc(%rsp),%rcx 0x0000000000400f4c <+9>: lea 0x8(%rsp),%rdx 這兩個整數依次存為 rsp+8, rsp+c 1 2 0x0000000000400f6a <+39>: cmpl $0x7,0x8(%rsp) 0x0000000000400f6f <+44>: ja 0x400fad <phase_3+106> 這裡判斷了第一個數,如果這個數大於 7,就會引爆 1 2 0x0000000000400f71 <+46>: mov 0x8(%rsp),%eax 0x0000000000400f75 <+50>: jmpq *0x402470(,%rax,8) 我們把第一個整數存入 eax,這裡很明顯是一個 switch 的跳轉表:0x402470 + 8*rax eax 和 rax 實際上是同一個東西,前者是這個暫存器的前 32 位,後者是這個暫存器的完整 64 位,這是歷史遺留產物,實際上,還有 ax, ah, al,為了向後相容而保留。 我們來讀取 10 個,x/10x 0x402470,得到: 1 2 3 0x402470: 0x00400f7c 0x00000000 0x00400fb9 0x00000000 0x402480: 0x00400f83 0x00000000 0x00400f8a 0x00000000 0x402490: 0x00400f91 0x00000000 這是 switch 語句的跳轉表,與匯編代碼中對應。 我們隨便選一個就能得到正確答案,如,0 對應 0x00400f7c 1 2 3 4 5 6 0x0000000000400f7c <+57>: mov $0xcf,%eax 0x0000000000400f81 <+62>: jmp 0x400fbe <phase_3+123> ... 0x0000000000400fbe <+123>: cmp 0xc(%rsp),%eax 0x0000000000400fc2 <+127>: je 0x400fc9 <phase_3+134> 0x0000000000400fc4 <+129>: callq 0x40143a <explode_bomb> 第二個數和 eax 比較,相等就拆除成功 我們得到第二個數 0xcf = 207 於是,答案是 0 207 實際上,答案並不唯一,觀察代碼可以知道,每一個 switch 分支中,都對應了一個第二個整數的正確答案。 Phase 4 反編譯: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Dump of assembler code for function phase_4: 0x000000000040100c <+0>: sub $0x18,%rsp 0x0000000000401010 <+4>: lea 0xc(%rsp),%rcx 0x0000000000401015 <+9>: lea 0x8(%rsp),%rdx 0x000000000040101a <+14>: mov $0x4025cf,%esi 0x000000000040101f <+19>: mov $0x0,%eax 0x0000000000401024 <+24>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x0000000000401029 <+29>: cmp $0x2,%eax 0x000000000040102c <+32>: jne 0x401035 <phase_4+41> 0x000000000040102e <+34>: cmpl $0xe,0x8(%rsp) 0x0000000000401033 <+39>: jbe 0x40103a <phase_4+46> 0x0000000000401035 <+41>: callq 0x40143a <explode_bomb> 0x000000000040103a <+46>: mov $0xe,%edx 0x000000000040103f <+51>: mov $0x0,%esi 0x0000000000401044 <+56>: mov 0x8(%rsp),%edi 0x0000000000401048 <+60>: callq 0x400fce <func4> 0x000000000040104d <+65>: test %eax,%eax 0x000000000040104f <+67>: jne 0x401058 <phase_4+76> 0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp) 0x0000000000401056 <+74>: je 0x40105d <phase_4+81> 0x0000000000401058 <+76>: callq 0x40143a <explode_bomb> 0x000000000040105d <+81>: add $0x18,%rsp 0x0000000000401061 <+85>: retq End of assembler dump. 我們還是看到 sscanf 讀一下 0x4025cf,得到 %d %d,看起來又是讀兩個數字,分別存入 rdx, rcx 接著往下讀,jbe 0x40103a,要求 rdx <= 14 1 2 3 0x000000000040103a <+46>: mov $0xe,%edx 0x000000000040103f <+51>: mov $0x0,%esi 0x0000000000401044 <+56>: mov 0x8(%rsp),%edi 明顯在傳參,調用了 func4 我們先不急著看 func4,接著往下讀 1 2 3 4 0x000000000040104d <+65>: test %eax,%eax 0x000000000040104f <+67>: jne 0x401058 <phase_4+76> ... 0x0000000000401058 <+76>: callq 0x40143a <explode_bomb> 回顧一下暫存器知識,eax 在這裡是函數的返回值,這裡要求返回值等於 0 1 2 0x0000000000401051 <+69>: cmpl $0x0,0xc(%rsp) 0x0000000000401056 <+74>: je 0x40105d <phase_4+81> 這裡要求讀取到的第二個數是 0,算是得到了半個答案 接下來我們看 func4 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Dump of assembler code for function func4: 0x0000000000400fce <+0>: sub $0x8,%rsp 0x0000000000400fd2 <+4>: mov %edx,%eax 0x0000000000400fd4 <+6>: sub %esi,%eax 0x0000000000400fd6 <+8>: mov %eax,%ecx 0x0000000000400fd8 <+10>: shr $0x1f,%ecx 0x0000000000400fdb <+13>: add %ecx,%eax 0x0000000000400fdd <+15>: sar %eax 0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx 0x0000000000400fe2 <+20>: cmp %edi,%ecx 0x0000000000400fe4 <+22>: jle 0x400ff2 <func4+36> 0x0000000000400fe6 <+24>: lea -0x1(%rcx),%edx 0x0000000000400fe9 <+27>: callq 0x400fce <func4> 0x0000000000400fee <+32>: add %eax,%eax 0x0000000000400ff0 <+34>: jmp 0x401007 <func4+57> 0x0000000000400ff2 <+36>: mov $0x0,%eax 0x0000000000400ff7 <+41>: cmp %edi,%ecx 0x0000000000400ff9 <+43>: jge 0x401007 <func4+57> 0x0000000000400ffb <+45>: lea 0x1(%rcx),%esi 0x0000000000400ffe <+48>: callq 0x400fce <func4> 0x0000000000401003 <+53>: lea 0x1(%rax,%rax,1),%eax 0x0000000000401007 <+57>: add $0x8,%rsp 0x000000000040100b <+61>: retq End of assembler dump. 這個代碼裡面包含遞迴,我們可以手動把這段代碼翻譯到 C 語言: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 // edx = 14, esi = 0, edi = a int func4(int edi, int esi, int edx){ int mid = l + ((r-l)>>1); if(mid <= a){ if(mid==a){ return 0; } l = mid + 1; return 2*func4(a, l, r) + 1; }else{ r = mid - 1; return 2*func4(a, l, r); } } 這是二分尋找,我們很容易得到答案 a=7,於是返回 0 得到最終的答案 7 0 1 2 3 4 5 6 7 0x0000000000400fd2 <+4>: mov %edx,%eax 0x0000000000400fd4 <+6>: sub %esi,%eax 0x0000000000400fd6 <+8>: mov %eax,%ecx 0x0000000000400fd8 <+10>: shr $0x1f,%ecx 0x0000000000400fdb <+13>: add %ecx,%eax 0x0000000000400fdd <+15>: sar %eax 0x0000000000400fdf <+17>: lea (%rax,%rsi,1),%ecx 這一段代碼就是在計算 mid,非常好理解,但是有個問題:shr $0x1f,%ecx 是在做什麼? 偏置 整數除法要求向零捨入。對於正數,向下捨入;對於負數,向上捨入。除以2的冪可以用右移操作替代。 但是,對於補碼右移,很可能出現捨入錯誤。 我們進行右移的時候,其實是捨去了最低位,是一種向下取整 x = \underbrace{\sum_{i=k}^{w-1} x_i 2^i}_{\text{高位部分}} + \underbrace{\sum_{i=0}^{k-1} x_i 2^i}_{\text{低位部分}} 當我們執行右移 x >> k 時:高位部分的權重全部除以了 $2^k$,變成了整數結果。低位部分(餘數)直接被丟棄了。 對於負數而言,這一操作進行了向下取整,但我們要求對負數進行向上取整。 因此,我們需要引入偏置。 \text{對於整數 } x \text{ 和 } y(y>0),\lceil x/y \rceil = \lfloor (x+y-1)/y \rfloor 於是 (x+(1<<k)-1)>>k 得到 $\lceil x/2^k \rceil$ 也就是下面這兩行的含義 1 2 0x0000000000400fd8 <+10>: shr $0x1f,%ecx 0x0000000000400fdb <+13>: add %ecx,%eax Phase 5 我們先disas看代碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 Dump of assembler code for function phase_5: 0x0000000000401062 <+0>: push %rbx 0x0000000000401063 <+1>: sub $0x20,%rsp 0x0000000000401067 <+5>: mov %rdi,%rbx 0x000000000040106a <+8>: mov %fs:0x28,%rax 0x0000000000401073 <+17>: mov %rax,0x18(%rsp) 0x0000000000401078 <+22>: xor %eax,%eax 0x000000000040107a <+24>: callq 0x40131b <string_length> 0x000000000040107f <+29>: cmp $0x6,%eax 0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> 0x0000000000401084 <+34>: callq 0x40143a <explode_bomb> 0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112> 0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx 0x000000000040108f <+45>: mov %cl,(%rsp) 0x0000000000401092 <+48>: mov (%rsp),%rdx 0x0000000000401096 <+52>: and $0xf,%edx 0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx 0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) 0x00000000004010a4 <+66>: add $0x1,%rax 0x00000000004010a8 <+70>: cmp $0x6,%rax 0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> 0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) 0x00000000004010b3 <+81>: mov $0x40245e,%esi 0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi 0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal> 0x00000000004010c2 <+96>: test %eax,%eax 0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> 0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb> 0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1) 0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119> 0x00000000004010d2 <+112>: mov $0x0,%eax 0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41> 0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax 0x00000000004010de <+124>: xor %fs:0x28,%rax 0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140> 0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt> 0x00000000004010ee <+140>: add $0x20,%rsp 0x00000000004010f2 <+144>: pop %rbx 0x00000000004010f3 <+145>: retq End of assembler dump. 很快識別出來,這一段代碼中有兩個記憶體地址:0x4024b0 0x40245e 讀一下: 1 2 0x4024b0 <array.3449>: "maduiersnfotvbylSo you think you can stop the bomb with ctrl-c, do you?" 0x40245e: "flyers" 第一個 array.3449 是一個字串,我們就記為 a[] 上面的代碼可以分個段 1 2 3 4 5 6 7 8 9 10 11 0x0000000000401062 <+0>: push %rbx 0x0000000000401063 <+1>: sub $0x20,%rsp 0x0000000000401067 <+5>: mov %rdi,%rbx 0x000000000040106a <+8>: mov %fs:0x28,%rax 0x0000000000401073 <+17>: mov %rax,0x18(%rsp) 0x0000000000401078 <+22>: xor %eax,%eax 0x000000000040107a <+24>: callq 0x40131b <string_length> 0x000000000040107f <+29>: cmp $0x6,%eax 0x0000000000401082 <+32>: je 0x4010d2 <phase_5+112> 0x0000000000401084 <+34>: callq 0x40143a <explode_bomb> 0x0000000000401089 <+39>: jmp 0x4010d2 <phase_5+112> 這裡是前面初始化的部分,我們可以看到預留了棧空間,應該是讀取了一個字串,長度為 6,存在棧上。 1 2 3 4 5 6 7 8 9 10 11 12 0x00000000004010d2 <+112>: mov $0x0,%eax 0x00000000004010d7 <+117>: jmp 0x40108b <phase_5+41> ... 0x000000000040108b <+41>: movzbl (%rbx,%rax,1),%ecx 0x000000000040108f <+45>: mov %cl,(%rsp) 0x0000000000401092 <+48>: mov (%rsp),%rdx 0x0000000000401096 <+52>: and $0xf,%edx 0x0000000000401099 <+55>: movzbl 0x4024b0(%rdx),%edx 0x00000000004010a0 <+62>: mov %dl,0x10(%rsp,%rax,1) 0x00000000004010a4 <+66>: add $0x1,%rax 0x00000000004010a8 <+70>: cmp $0x6,%rax 0x00000000004010ac <+74>: jne 0x40108b <phase_5+41> 以上是一個 for 循環,循環 6 次,取 edx 的後四位,這是一個 0~15 的數,記為 i,於是把 a[i] 加入棧中對應位置 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) 0x00000000004010b3 <+81>: mov $0x40245e,%esi 0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi 0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal> 0x00000000004010c2 <+96>: test %eax,%eax 0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> 0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb> 0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1) 0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119> ... 0x00000000004010d9 <+119>: mov 0x18(%rsp),%rax 0x00000000004010de <+124>: xor %fs:0x28,%rax 0x00000000004010e7 <+133>: je 0x4010ee <phase_5+140> 0x00000000004010e9 <+135>: callq 0x400b30 <__stack_chk_fail@plt> 0x00000000004010ee <+140>: add $0x20,%rsp 0x00000000004010f2 <+144>: pop %rbx 0x00000000004010f3 <+145>: retq 這裡有價值的片段只有 1 2 3 4 5 6 7 8 9 0x00000000004010ae <+76>: movb $0x0,0x16(%rsp) 0x00000000004010b3 <+81>: mov $0x40245e,%esi 0x00000000004010b8 <+86>: lea 0x10(%rsp),%rdi 0x00000000004010bd <+91>: callq 0x401338 <strings_not_equal> 0x00000000004010c2 <+96>: test %eax,%eax 0x00000000004010c4 <+98>: je 0x4010d9 <phase_5+119> 0x00000000004010c6 <+100>: callq 0x40143a <explode_bomb> 0x00000000004010cb <+105>: nopl 0x0(%rax,%rax,1) 0x00000000004010d0 <+110>: jmp 0x4010d9 <phase_5+119> 這是比較字串。 我們不難發現,這道題的邏輯是查表映射:程序會把輸入字元對 16 取模得到的數值作為索引,去尋找那個長字串(maduiers…)中的字元。 為了讓最終取出來的字元拼成 flyers,我們需要反向尋找 flyers 中每個字母在表中對應的下標位置,然後構造一個輸入字串,使其每一位的 ASCII 碼模 16 後正好等於這些下標。 這個過程可以總結為: Input Char -> ASCII Hex -> AND 0xF (取後4位) -> Table Index -> Lookup Table Char -> Target “flyers” 於是我們可以得到答案 ionefg 或者 IONEFG 其實還可以有一些其他答案,留給讀者去發現 Phase 6 先看代碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 0x00000000004010f4 <+0>: push %r14 0x00000000004010f6 <+2>: push %r13 0x00000000004010f8 <+4>: push %r12 0x00000000004010fa <+6>: push %rbp 0x00000000004010fb <+7>: push %rbx 0x00000000004010fc <+8>: sub $0x50,%rsp 0x0000000000401100 <+12>: mov %rsp,%r13 0x0000000000401103 <+15>: mov %rsp,%rsi 0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers> 0x000000000040110b <+23>: mov %rsp,%r14 0x000000000040110e <+26>: mov $0x0,%r12d 0x0000000000401114 <+32>: mov %r13,%rbp 0x0000000000401117 <+35>: mov 0x0(%r13),%eax 0x000000000040111b <+39>: sub $0x1,%eax 0x000000000040111e <+42>: cmp $0x5,%eax 0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> 0x0000000000401123 <+47>: callq 0x40143a <explode_bomb> 0x0000000000401128 <+52>: add $0x1,%r12d 0x000000000040112c <+56>: cmp $0x6,%r12d 0x0000000000401130 <+60>: je 0x401153 <phase_6+95> 0x0000000000401132 <+62>: mov %r12d,%ebx 0x0000000000401135 <+65>: movslq %ebx,%rax 0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax 0x000000000040113b <+71>: cmp %eax,0x0(%rbp) 0x000000000040113e <+74>: jne 0x401145 <phase_6+81> 0x0000000000401140 <+76>: callq 0x40143a <explode_bomb> 0x0000000000401145 <+81>: add $0x1,%ebx 0x0000000000401148 <+84>: cmp $0x5,%ebx 0x000000000040114b <+87>: jle 0x401135 <phase_6+65> 0x000000000040114d <+89>: add $0x4,%r13 0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> 0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi 0x0000000000401158 <+100>: mov %r14,%rax 0x000000000040115b <+103>: mov $0x7,%ecx 0x0000000000401160 <+108>: mov %ecx,%edx 0x0000000000401162 <+110>: sub (%rax),%edx 0x0000000000401164 <+112>: mov %edx,(%rax) 0x0000000000401166 <+114>: add $0x4,%rax 0x000000000040116a <+118>: cmp %rsi,%rax 0x000000000040116d <+121>: jne 0x401160 <phase_6+108> 0x000000000040116f <+123>: mov $0x0,%esi 0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163> 0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx 0x000000000040117a <+134>: add $0x1,%eax 0x000000000040117d <+137>: cmp %ecx,%eax 0x000000000040117f <+139>: jne 0x401176 <phase_6+130> 0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148> 0x0000000000401183 <+143>: mov $0x6032d0,%edx 0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) 0x000000000040118d <+153>: add $0x4,%rsi 0x0000000000401191 <+157>: cmp $0x18,%rsi 0x0000000000401195 <+161>: je 0x4011ab <phase_6+183> 0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx 0x000000000040119a <+166>: cmp $0x1,%ecx 0x000000000040119d <+169>: jle 0x401183 <phase_6+143> 0x000000000040119f <+171>: mov $0x1,%eax 0x00000000004011a4 <+176>: mov $0x6032d0,%edx 0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130> 0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx 0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax 0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi 0x00000000004011ba <+198>: mov %rbx,%rcx 0x00000000004011bd <+201>: mov (%rax),%rdx 0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) 0x00000000004011c4 <+208>: add $0x8,%rax 0x00000000004011c8 <+212>: cmp %rsi,%rax 0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222> 0x00000000004011cd <+217>: mov %rdx,%rcx 0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201> 0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) 0x00000000004011da <+230>: mov $0x5,%ebp 0x00000000004011df <+235>: mov 0x8(%rbx),%rax 0x00000000004011e3 <+239>: mov (%rax),%eax 0x00000000004011e5 <+241>: cmp %eax,(%rbx) 0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> 0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb> 0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx 0x00000000004011f2 <+254>: sub $0x1,%ebp 0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> 0x00000000004011f7 <+259>: add $0x50,%rsp 0x00000000004011fb <+263>: pop %rbx 0x00000000004011fc <+264>: pop %rbp 0x00000000004011fd <+265>: pop %r12 0x00000000004011ff <+267>: pop %r13 0x0000000000401201 <+269>: pop %r14 0x0000000000401203 <+271>: retq 分開來看: 1 2 3 4 5 6 7 8 0x00000000004010f4 <+0>: push %r14 0x00000000004010f6 <+2>: push %r13 0x00000000004010f8 <+4>: push %r12 0x00000000004010fa <+6>: push %rbp 0x00000000004010fb <+7>: push %rbx 0x00000000004010fc <+8>: sub $0x50,%rsp 0x0000000000401100 <+12>: mov %rsp,%r13 0x0000000000401103 <+15>: mov %rsp,%rsi 這一段是設置棧幀 1 0x0000000000401106 <+18>: callq 0x40145c <read_six_numbers> 這裡讀了 6 個數字,我們在 Phase 2 已經看到,這六個數字存在從 rsp 開始的一個數組中。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 0x000000000040110b <+23>: mov %rsp,%r14 0x000000000040110e <+26>: mov $0x0,%r12d 0x0000000000401114 <+32>: mov %r13,%rbp 0x0000000000401117 <+35>: mov 0x0(%r13),%eax 0x000000000040111b <+39>: sub $0x1,%eax 0x000000000040111e <+42>: cmp $0x5,%eax 0x0000000000401121 <+45>: jbe 0x401128 <phase_6+52> 0x0000000000401123 <+47>: callq 0x40143a <explode_bomb> 0x0000000000401128 <+52>: add $0x1,%r12d 0x000000000040112c <+56>: cmp $0x6,%r12d 0x0000000000401130 <+60>: je 0x401153 <phase_6+95> 0x0000000000401132 <+62>: mov %r12d,%ebx 0x0000000000401135 <+65>: movslq %ebx,%rax 0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax 0x000000000040113b <+71>: cmp %eax,0x0(%rbp) 0x000000000040113e <+74>: jne 0x401145 <phase_6+81> 0x0000000000401140 <+76>: callq 0x40143a <explode_bomb> 0x0000000000401145 <+81>: add $0x1,%ebx 0x0000000000401148 <+84>: cmp $0x5,%ebx 0x000000000040114b <+87>: jle 0x401135 <phase_6+65> 0x000000000040114d <+89>: add $0x4,%r13 0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> 此處代碼構建了一個典型的嵌套循環結構:外層循環由 %r12d 計數,內層循環則由 %ebx 控制。 1 2 3 4 5 6 0x0000000000401117 <+35>: mov 0x0(%r13),%eax 0x000000000040111b <+39>: sub $0x1,%eax 0x000000000040111e <+42>: cmp $0x5,%eax ... 0x000000000040114d <+89>: add $0x4,%r13 0x0000000000401151 <+93>: jmp 0x401114 <phase_6+32> 首先分析外層循環:它通過 %r13 指針遍歷輸入數組,首要任務是進行邊界檢查,確保讀取到的每一個數字都小於或等於 6。 再來看內層循環: 1 2 3 4 5 6 7 8 9 0x0000000000401132 <+62>: mov %r12d,%ebx 0x0000000000401135 <+65>: movslq %ebx,%rax 0x0000000000401138 <+68>: mov (%rsp,%rax,4),%eax 0x000000000040113b <+71>: cmp %eax,0x0(%rbp) 0x000000000040113e <+74>: jne 0x401145 <phase_6+81> 0x0000000000401140 <+76>: callq 0x40143a <explode_bomb> 0x0000000000401145 <+81>: add $0x1,%ebx 0x0000000000401148 <+84>: cmp $0x5,%ebx 0x000000000040114b <+87>: jle 0x401135 <phase_6+65> 這裡從當前外層數字開始,判斷數組之後的每一個數位(int 類型,4 位元組,故 (%rsp,%rax,4) 獲得當前數字),判斷這個數字是否和外層數字相同。 於是,我們發現,這一層循環判斷輸入的每個數字是否互不相同。 總結一下,這個嵌套循環檢查我們的輸入是否是六個互不相同的小於等於 6 的數字 1 2 3 4 5 6 7 8 9 0x0000000000401153 <+95>: lea 0x18(%rsp),%rsi 0x0000000000401158 <+100>: mov %r14,%rax 0x000000000040115b <+103>: mov $0x7,%ecx 0x0000000000401160 <+108>: mov %ecx,%edx 0x0000000000401162 <+110>: sub (%rax),%edx 0x0000000000401164 <+112>: mov %edx,(%rax) 0x0000000000401166 <+114>: add $0x4,%rax 0x000000000040116a <+118>: cmp %rsi,%rax 0x000000000040116d <+121>: jne 0x401160 <phase_6+108> 這裡又有一個循環。前文已知,r14 就是 rsp,也就是棧指針。這裡遍歷每一個數 x,重新賦值,x = 7-x 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 0x000000000040116f <+123>: mov $0x0,%esi 0x0000000000401174 <+128>: jmp 0x401197 <phase_6+163> 0x0000000000401176 <+130>: mov 0x8(%rdx),%rdx 0x000000000040117a <+134>: add $0x1,%eax 0x000000000040117d <+137>: cmp %ecx,%eax 0x000000000040117f <+139>: jne 0x401176 <phase_6+130> 0x0000000000401181 <+141>: jmp 0x401188 <phase_6+148> 0x0000000000401183 <+143>: mov $0x6032d0,%edx 0x0000000000401188 <+148>: mov %rdx,0x20(%rsp,%rsi,2) 0x000000000040118d <+153>: add $0x4,%rsi 0x0000000000401191 <+157>: cmp $0x18,%rsi 0x0000000000401195 <+161>: je 0x4011ab <phase_6+183> 0x0000000000401197 <+163>: mov (%rsp,%rsi,1),%ecx 0x000000000040119a <+166>: cmp $0x1,%ecx 0x000000000040119d <+169>: jle 0x401183 <phase_6+143> 0x000000000040119f <+171>: mov $0x1,%eax 0x00000000004011a4 <+176>: mov $0x6032d0,%edx 0x00000000004011a9 <+181>: jmp 0x401176 <phase_6+130> 先讀取輸入的元素 x,如果小於等於 1,把 edx 賦值為 0x6032d0,然後把 x 放在一個臨時數組中,然後繼續到下一個元素,直到遍歷完整個數組 (0x18 = 24 = 4*6) 如果元素 x 大於 1,把 eax 賦值為 1,edx 賦值為 0x6032d0,之後執行 x-1 次 mov 0x8(%rdx),%rdx 操作 這裡疑似是鍊表,出現了記憶體地址 0x6032d0,我們來看看: 1 2 3 4 5 6 7 (gdb) x/12xg 0x6032d0 0x6032d0 <node1>: 0x000000010000014c 0x00000000006032e0 0x6032e0 <node2>: 0x00000002000000a8 0x00000000006032f0 0x6032f0 <node3>: 0x000000030000039c 0x0000000000603300 0x603300 <node4>: 0x00000004000002b3 0x0000000000603310 0x603310 <node5>: 0x00000005000001dd 0x0000000000603320 0x603320 <node6>: 0x00000006000001bb 0x0000000000000000 這裡注意,在 64 位系統中,指針占用 8 位元組(即 64 位)。 顯然是鍊表,0x8(%rdx) 代表 next 指針 故上述操作得到一個數組,設輸入數組的第 i 個數為 x,數組中第 i 個數對應鍊表中第 x 個數的地址。 1 2 3 0x00000000004011ab <+183>: mov 0x20(%rsp),%rbx 0x00000000004011b0 <+188>: lea 0x28(%rsp),%rax 0x00000000004011b5 <+193>: lea 0x50(%rsp),%rsi 這裡是一些初始化。rsi 是邊界指針,標記循環的終止。0x20 到 0x50 正好 6*8=48 1 2 3 4 5 6 7 8 0x00000000004011ba <+198>: mov %rbx,%rcx 0x00000000004011bd <+201>: mov (%rax),%rdx 0x00000000004011c0 <+204>: mov %rdx,0x8(%rcx) 0x00000000004011c4 <+208>: add $0x8,%rax 0x00000000004011c8 <+212>: cmp %rsi,%rax 0x00000000004011cb <+215>: je 0x4011d2 <phase_6+222> 0x00000000004011cd <+217>: mov %rdx,%rcx 0x00000000004011d0 <+220>: jmp 0x4011bd <phase_6+201> 這裡遍歷了我們剛才得到的鍊表地址數組。寫成 C 語言或許更好理解。 1 2 3 4 5 6 7 8 9 Node *current = node_ptrs[0]; // %rbx, %rcx 初始化 int i = 1; // 對應 %rax 指向 node_ptrs[1] while (i < 6) { Node *next_node = node_ptrs[i]; // mov (%rax), %rdx current->next = next_node; // mov %rdx, 0x8(%rcx) current = next_node; // mov %rdx, %rcx i++; // add $0x8, %rax } 這一個循環對於鍊表結構進行了修改。 1 0x00000000004011d2 <+222>: movq $0x0,0x8(%rdx) 這句話則把最後一個節點的 next 賦值為 NULL,確保鍊表結構 接下來又有一個循環: 1 2 3 4 5 6 7 8 9 0x00000000004011da <+230>: mov $0x5,%ebp 0x00000000004011df <+235>: mov 0x8(%rbx),%rax 0x00000000004011e3 <+239>: mov (%rax),%eax 0x00000000004011e5 <+241>: cmp %eax,(%rbx) 0x00000000004011e7 <+243>: jge 0x4011ee <phase_6+250> 0x00000000004011e9 <+245>: callq 0x40143a <explode_bomb> 0x00000000004011ee <+250>: mov 0x8(%rbx),%rbx 0x00000000004011f2 <+254>: sub $0x1,%ebp 0x00000000004011f5 <+257>: jne 0x4011df <phase_6+235> 遍歷鍊表,確保鍊表倒序排列。 看到這裡,我們就可以得到答案了: 1 2 3 4 5 6 7 (gdb) x/12xg 0x6032d0 0x6032d0 <node1>: 0x000000010000014c 0x00000000006032e0 0x6032e0 <node2>: 0x00000002000000a8 0x00000000006032f0 0x6032f0 <node3>: 0x000000030000039c 0x0000000000603300 0x603300 <node4>: 0x00000004000002b3 0x0000000000603310 0x603310 <node5>: 0x00000005000001dd 0x0000000000603320 0x603320 <node6>: 0x00000006000001bb 0x0000000000000000 找到鍊表值的倒序索引即可,注意值是 int 類型,只取後四位。於是可以得到 3 4 5 6 1 2 但我們還要注意,輸入進行過 7-x 操作(見上文),所以我們調整答案 4 3 2 1 6 5 最後一個 Phase 有點複雜,巧妙融合了嵌套循環校驗、數組映射變換以及鍊表重組等多種技術。 隱藏關 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 /* Hmm... Six phases must be more secure than one phase! */ input = read_line(); /* Get input */ phase_1(input); /* Run the phase */ phase_defused(); /* Drat! They figured it out! * Let me know how they did it. */ printf("Phase 1 defused. How about the next one?\n"); /* The second phase is harder. No one will ever figure out * how to defuse this... */ input = read_line(); phase_2(input); phase_defused(); printf("That's number 2. Keep going!\n"); /* I guess this is too easy so far. Some more complex code will * confuse people. */ input = read_line(); phase_3(input); phase_defused(); printf("Halfway there!\n"); /* Oh yeah? Well, how good is your math? Try on this saucy problem! */ input = read_line(); phase_4(input); phase_defused(); printf("So you got that one. Try this one.\n"); /* Round and 'round in memory we go, where we stop, the bomb blows! */ input = read_line(); phase_5(input); phase_defused(); printf("Good work! On to the next...\n"); /* This phase will never be used, since no one will get past the * earlier ones. But just in case, make this one extra hard. */ input = read_line(); phase_6(input); phase_defused(); bomb 代碼中,每一個 phase 後都運行 phase_defused。我們來看看: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 Dump of assembler code for function phase_defused: 0x00000000004015c4 <+0>: sub $0x78,%rsp 0x00000000004015c8 <+4>: mov %fs:0x28,%rax 0x00000000004015d1 <+13>: mov %rax,0x68(%rsp) 0x00000000004015d6 <+18>: xor %eax,%eax 0x00000000004015d8 <+20>: cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings> 0x00000000004015df <+27>: jne 0x40163f <phase_defused+123> 0x00000000004015e1 <+29>: lea 0x10(%rsp),%r8 0x00000000004015e6 <+34>: lea 0xc(%rsp),%rcx 0x00000000004015eb <+39>: lea 0x8(%rsp),%rdx 0x00000000004015f0 <+44>: mov $0x402619,%esi 0x00000000004015f5 <+49>: mov $0x603870,%edi 0x00000000004015fa <+54>: callq 0x400bf0 <__isoc99_sscanf@plt> 0x00000000004015ff <+59>: cmp $0x3,%eax 0x0000000000401602 <+62>: jne 0x401635 <phase_defused+113> 0x0000000000401604 <+64>: mov $0x402622,%esi 0x0000000000401609 <+69>: lea 0x10(%rsp),%rdi 0x000000000040160e <+74>: callq 0x401338 <strings_not_equal> 0x0000000000401613 <+79>: test %eax,%eax 0x0000000000401615 <+81>: jne 0x401635 <phase_defused+113> 0x0000000000401617 <+83>: mov $0x4024f8,%edi 0x000000000040161c <+88>: callq 0x400b10 <puts@plt> 0x0000000000401621 <+93>: mov $0x402520,%edi 0x0000000000401626 <+98>: callq 0x400b10 <puts@plt> 0x000000000040162b <+103>: mov $0x0,%eax 0x0000000000401630 <+108>: callq 0x401242 <secret_phase> 0x0000000000401635 <+113>: mov $0x402558,%edi 0x000000000040163a <+118>: callq 0x400b10 <puts@plt> 0x000000000040163f <+123>: mov 0x68(%rsp),%rax 0x0000000000401644 <+128>: xor %fs:0x28,%rax 0x000000000040164d <+137>: je 0x401654 <phase_defused+144> 0x000000000040164f <+139>: callq 0x400b30 <__stack_chk_fail@plt> 0x0000000000401654 <+144>: add $0x78,%rsp 0x0000000000401658 <+148>: retq 1 0x00000000004015d8 <+20>: cmpl $0x6,0x202181(%rip) # 0x603760 <num_input_strings> 這裡要求六關全部通過之後才能進入 secret_phase 我們可以設置條件斷點:b phase_defused if num_input_strings == 6 注意到: 1 0x0000000000401630 <+108>: callq 0x401242 <secret_phase> 這裡有非常多的記憶體地址,其中: 1 2 3 4 5 6 (gdb) x/s 0x402619 0x402619: "%d %d %s" (gdb) x/s 0x603870 0x603870 <input_strings+240>: "7 0" (gdb) x/s 0x402622 0x402622: "DrEvil" 判斷 Phase 4 輸入之後是否有一個字串 DrEvil,如果有,進入隱藏關! 再來看看隱藏關的代碼: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 Dump of assembler code for function secret_phase: 0x0000000000401242 <+0>: push %rbx 0x0000000000401243 <+1>: callq 0x40149e <read_line> 0x0000000000401248 <+6>: mov $0xa,%edx 0x000000000040124d <+11>: mov $0x0,%esi 0x0000000000401252 <+16>: mov %rax,%rdi 0x0000000000401255 <+19>: callq 0x400bd0 <strtol@plt> 0x000000000040125a <+24>: mov %rax,%rbx 0x000000000040125d <+27>: lea -0x1(%rax),%eax 0x0000000000401260 <+30>: cmp $0x3e8,%eax 0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42> 0x0000000000401267 <+37>: callq 0x40143a <explode_bomb> 0x000000000040126c <+42>: mov %ebx,%esi 0x000000000040126e <+44>: mov $0x6030f0,%edi 0x0000000000401273 <+49>: callq 0x401204 <fun7> 0x0000000000401278 <+54>: cmp $0x2,%eax 0x000000000040127b <+57>: je 0x401282 <secret_phase+64> 0x000000000040127d <+59>: callq 0x40143a <explode_bomb> 0x0000000000401282 <+64>: mov $0x402438,%edi 0x0000000000401287 <+69>: callq 0x400b10 <puts@plt> 0x000000000040128c <+74>: callq 0x4015c4 <phase_defused> 0x0000000000401291 <+79>: pop %rbx 0x0000000000401292 <+80>: retq End of assembler dump. 看到 strtol,知道這裡讀入了一個整數 1 2 3 4 5 0x000000000040125a <+24>: mov %rax,%rbx 0x000000000040125d <+27>: lea -0x1(%rax),%eax 0x0000000000401260 <+30>: cmp $0x3e8,%eax 0x0000000000401265 <+35>: jbe 0x40126c <secret_phase+42> 0x0000000000401267 <+37>: callq 0x40143a <explode_bomb> 要求讀取的整數小於等於 1001。注意 jbe 是無符號數的跳轉檢查,所以這裡其實也隱性限制了下限。所以嚴格的輸入限制是 [1, 1001] 之間的整數。 1 2 3 0x000000000040126c <+42>: mov %ebx,%esi 0x000000000040126e <+44>: mov $0x6030f0,%edi 0x0000000000401273 <+49>: callq 0x401204 <fun7> 傳參,進入 fun7 1 2 3 4 0x0000000000401278 <+54>: cmp $0x2,%eax 0x000000000040127b <+57>: je 0x401282 <secret_phase+64> 0x000000000040127d <+59>: callq 0x40143a <explode_bomb> 0x0000000000401282 <+64>: mov $0x402438,%edi 這裡要求 fun7 的返回值等於 2 接下來我們看看 fun7,手動分個段 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 Dump of assembler code for function fun7: 0x0000000000401204 <+0>: sub $0x8,%rsp 0x0000000000401208 <+4>: test %rdi,%rdi 0x000000000040120b <+7>: je 0x401238 <fun7+52> 0x000000000040120d <+9>: mov (%rdi),%edx 0x000000000040120f <+11>: cmp %esi,%edx 0x0000000000401211 <+13>: jle 0x401220 <fun7+28> 0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi 0x0000000000401217 <+19>: callq 0x401204 <fun7> 0x000000000040121c <+24>: add %eax,%eax 0x000000000040121e <+26>: jmp 0x40123d <fun7+57> 0x0000000000401220 <+28>: mov $0x0,%eax 0x0000000000401225 <+33>: cmp %esi,%edx 0x0000000000401227 <+35>: je 0x40123d <fun7+57> 0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi 0x000000000040122d <+41>: callq 0x401204 <fun7> 0x0000000000401232 <+46>: lea 0x1(%rax,%rax,1),%eax 0x0000000000401236 <+50>: jmp 0x40123d <fun7+57> 0x0000000000401238 <+52>: mov $0xffffffff,%eax 0x000000000040123d <+57>: add $0x8,%rsp 0x0000000000401241 <+61>: retq End of assembler dump. 遍歷當前 rdi 之後的兩個指針,遞迴,有點像二叉樹。我們來看看初始參數: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 (gdb) x/60xg 0x6030f0 0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110 0x603100 <n1+16>: 0x0000000000603130 0x0000000000000000 0x603110 <n21>: 0x0000000000000008 0x0000000000603190 0x603120 <n21+16>: 0x0000000000603150 0x0000000000000000 0x603130 <n22>: 0x0000000000000032 0x0000000000603170 0x603140 <n22+16>: 0x00000000006031b0 0x0000000000000000 0x603150 <n32>: 0x0000000000000016 0x0000000000603270 0x603160 <n32+16>: 0x0000000000603230 0x0000000000000000 0x603170 <n33>: 0x000000000000002d 0x00000000006031d0 0x603180 <n33+16>: 0x0000000000603290 0x0000000000000000 0x603190 <n31>: 0x0000000000000006 0x00000000006031f0 0x6031a0 <n31+16>: 0x0000000000603250 0x0000000000000000 0x6031b0 <n34>: 0x000000000000006b 0x0000000000603210 0x6031c0 <n34+16>: 0x00000000006032b0 0x0000000000000000 0x6031d0 <n45>: 0x0000000000000028 0x0000000000000000 0x6031e0 <n45+16>: 0x0000000000000000 0x0000000000000000 0x6031f0 <n41>: 0x0000000000000001 0x0000000000000000 0x603200 <n41+16>: 0x0000000000000000 0x0000000000000000 0x603210 <n47>: 0x0000000000000063 0x0000000000000000 0x603220 <n47+16>: 0x0000000000000000 0x0000000000000000 0x603230 <n44>: 0x0000000000000023 0x0000000000000000 0x603240 <n44+16>: 0x0000000000000000 0x0000000000000000 0x603250 <n42>: 0x0000000000000007 0x0000000000000000 0x603260 <n42+16>: 0x0000000000000000 0x0000000000000000 0x603270 <n43>: 0x0000000000000014 0x0000000000000000 0x603280 <n43+16>: 0x0000000000000000 0x0000000000000000 0x603290 <n46>: 0x000000000000002f 0x0000000000000000 0x6032a0 <n46+16>: 0x0000000000000000 0x0000000000000000 0x6032b0 <n48>: 0x00000000000003e9 0x0000000000000000 0x6032c0 <n48+16>: 0x0000000000000000 0x0000000000000000 確實是一顆二叉樹!(這裡的 60 是我試出來的) fun7 傳入的參數為 rdi 和 esi 1 2 3 4 5 6 0x0000000000401208 <+4>: test %rdi,%rdi 0x000000000040120b <+7>: je 0x401238 <fun7+52> ... 0x0000000000401238 <+52>: mov $0xffffffff,%eax 0x000000000040123d <+57>: add $0x8,%rsp 0x0000000000401241 <+61>: retq 如果遍歷到葉子結點,直接返回 0xffffffff。 1 2 3 0x000000000040120d <+9>: mov (%rdi),%edx 0x000000000040120f <+11>: cmp %esi,%edx 0x0000000000401211 <+13>: jle 0x401220 <fun7+28> 查看當前節點的值,如果值大於 esi: 1 2 3 4 5 6 7 0x0000000000401213 <+15>: mov 0x8(%rdi),%rdi 0x0000000000401217 <+19>: callq 0x401204 <fun7> 0x000000000040121c <+24>: add %eax,%eax 0x000000000040121e <+26>: jmp 0x40123d <fun7+57> ... 0x000000000040123d <+57>: add $0x8,%rsp 0x0000000000401241 <+61>: retq 訪問左子節點,返回值乘以二 如果當前節點的值和 rsi 相等: 1 2 3 4 5 6 0x0000000000401220 <+28>: mov $0x0,%eax 0x0000000000401225 <+33>: cmp %esi,%edx 0x0000000000401227 <+35>: je 0x40123d <fun7+57> ... 0x000000000040123d <+57>: add $0x8,%rsp 0x0000000000401241 <+61>: retq 直接返回 否則,訪問右子節點: 1 2 3 4 5 6 7 0x0000000000401229 <+37>: mov 0x10(%rdi),%rdi 0x000000000040122d <+41>: callq 0x401204 <fun7> 0x0000000000401232 <+46>: lea 0x1(%rax,%rax,1),%eax 0x0000000000401236 <+50>: jmp 0x40123d <fun7+57> ... 0x000000000040123d <+57>: add $0x8,%rsp 0x0000000000401241 <+61>: retq 返回值乘以二再加一 我們可以用 C 語言翻譯上述代碼: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 long fun7(struct Node *node, int target_val) { // 1. 如果節點為空 if (node == NULL) { return -1; // 對應匯編中的 mov $0xffffffff, %eax } int current_val = node->value; // mov (%rdi), %edx // 2. 如果當前節點值 > 目標值 (target_val < current_val) // 匯編邏輯:cmp %esi, %edx -> jle (跳過) -> 否則執行這裡 if (current_val > target_val) { // 遞迴調用左子節點 (偏移量 0x8) // 對應 callq fun7, 然後 add %eax, %eax return 2 * fun7(node->left, target_val); } // 3. 如果當前節點值 == 目標值 // 匯編邏輯:cmp %esi, %edx -> je (跳轉到返回0) if (current_val == target_val) { return 0; // 找到目標,返回 0 } // 4. 如果當前節點值 < 目標值 // 匯編邏輯:此時只剩下這種情況 // 遞迴調用右子節點 (偏移量 0x10) // 對應 callq fun7, 然後 lea 0x1(%rax,%rax,1) -> 2*rax + 1 return 2 * fun7(node->right, target_val) + 1; } 我們再來看看二叉樹的結構,根據: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 (gdb) x/60xg 0x6030f0 0x6030f0 <n1>: 0x0000000000000024 0x0000000000603110 0x603100 <n1+16>: 0x0000000000603130 0x0000000000000000 0x603110 <n21>: 0x0000000000000008 0x0000000000603190 0x603120 <n21+16>: 0x0000000000603150 0x0000000000000000 0x603130 <n22>: 0x0000000000000032 0x0000000000603170 0x603140 <n22+16>: 0x00000000006031b0 0x0000000000000000 0x603150 <n32>: 0x0000000000000016 0x0000000000603270 0x603160 <n32+16>: 0x0000000000603230 0x0000000000000000 0x603170 <n33>: 0x000000000000002d 0x00000000006031d0 0x603180 <n33+16>: 0x0000000000603290 0x0000000000000000 0x603190 <n31>: 0x0000000000000006 0x00000000006031f0 0x6031a0 <n31+16>: 0x0000000000603250 0x0000000000000000 0x6031b0 <n34>: 0x000000000000006b 0x0000000000603210 0x6031c0 <n34+16>: 0x00000000006032b0 0x0000000000000000 0x6031d0 <n45>: 0x0000000000000028 0x0000000000000000 0x6031e0 <n45+16>: 0x0000000000000000 0x0000000000000000 0x6031f0 <n41>: 0x0000000000000001 0x0000000000000000 0x603200 <n41+16>: 0x0000000000000000 0x0000000000000000 0x603210 <n47>: 0x0000000000000063 0x0000000000000000 0x603220 <n47+16>: 0x0000000000000000 0x0000000000000000 0x603230 <n44>: 0x0000000000000023 0x0000000000000000 0x603240 <n44+16>: 0x0000000000000000 0x0000000000000000 0x603250 <n42>: 0x0000000000000007 0x0000000000000000 0x603260 <n42+16>: 0x0000000000000000 0x0000000000000000 0x603270 <n43>: 0x0000000000000014 0x0000000000000000 0x603280 <n43+16>: 0x0000000000000000 0x0000000000000000 0x603290 <n46>: 0x000000000000002f 0x0000000000000000 0x6032a0 <n46+16>: 0x0000000000000000 0x0000000000000000 0x6032b0 <n48>: 0x00000000000003e9 0x0000000000000000 0x6032c0 <n48+16>: 0x0000000000000000 0x0000000000000000 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 graph TD N1((36)) --> N21((8)) N1 --> N22((50)) N21 --> N31((6)) N21 --> N32((22)) N22 --> N33((45)) N22 --> N34((107)) N31 --> N41((1)) N31 --> N42((7)) N32 --> N43((20)) N32 --> N44((35)) N33 --> N45((40)) N33 --> N46((47)) N34 --> N47((99)) N34 --> N48((1001)) 要求最終輸出為 2,2 = 1*2 先向左,再向右,然後找到了答案。 於是,我們得到答案 22 總結 於是,最終答案是: 1 2 3 4 5 6 7 Border relations with Canada have never been better. 1 2 4 8 16 32 0 207 7 0 DrEvil ionefg 4 3 2 1 6 5 22 最後讓 AI 生成一段小結 CSAPP Bomb Lab 是一個非常經典的實驗,它不僅是一次對匯編語言 (x86-64) 的深度練習,更是一場邏輯推理的解謎遊戲。 回顧整個拆彈過程,我們經歷了從簡單到複雜的演進: 基礎控制流:從 Phase 1 的字串比較,到 Phase 2 的循環與棧上數組操作。 高級控制流:Phase 3 展示了 switch 語句如何通過跳轉表實現,Phase 4 則通過遞迴讓我們深入理解了棧幀的生長與銷毀以及二分尋找算法。 數據操縱:Phase 5 的位運算與字元數組索引映射,考察了對指針和記憶體定址的敏感度。 數據結構:Phase 6 的鍊表重排以及隱藏關卡的二叉搜索樹(BST),讓我們看到了高級數據結構在匯編層面的具體形態(指針即地址)。 在這個過程中,gdb 是最強大的武器。熟練掌握斷點設置、暫存器查看 (i r) 和記憶體檢查 (x/) 是通關的關鍵。同時,我們也深刻體會到了編譯器最佳化的“智慧”(如利用 lea 進行算術運算、利用無符號數比較合併上下界檢查)和 C 語言與機器碼之間的映射關係。 當看到終端最終列印出 “Congratulations! You’ve defused the bomb!” 時,所有的查表、計算和堆棧分析都是值得的。希望這篇解析能對你理解計算機底層系統有所幫助。 Happy Hacking!

2025/12/20
articleCard.readMore

x64 暫存器速查表

本文匯總 x64 架構下最核心的暫存器狀態與 ABI 約定。 暫存器 x64 暫存器完美向下相容。寫入低 32 位 (如 MOV EAX, 1) 會自動清零高 32 位;但寫入 8/16 位則保留高位數據。 通用暫存器 (GPRs) 速查表 64-bit32-bit16-bit8-bit用途 (System V / 通用)易失性 RAXEAXAXAL返回值 / 累加器Caller Saved RBXEBXBXBL基址 / 通用數據Callee Saved RCXECXCXCL循環計數 / 第4參數Caller Saved RDXEDXDXDLI/O / 乘除法 / 第3參數Caller Saved RSIESISISIL源地址 / 第2參數Caller Saved RDIEDIDIDIL目的地址 / 第1參數Caller Saved RBPEBPBPBPL棧底指針 (Base Ptr)Callee Saved RSPESPSPSPL棧頂指針 (Stack Ptr)Callee Saved R8R8DR8WR8B第5參數Caller Saved R9R9DR9WR9B第6參數Caller Saved R10R10DR10WR10B臨時暫存器 (Static Chain)Caller Saved R11R11DR11WR11B臨時 / 連結暫存器Caller Saved R12-R15RxxDRxxWRxxB通用數據儲存Callee Saved 關鍵術語: Caller Saved: 調用函數前,如果還需要這些值,調用者必須自己壓棧保存。函數返回後值可能變了。 Callee Saved: 被調用的函數必須保證在返回時,這些暫存器的值和調用前一樣(通常通過 push/pop 維護)。 函數調用約定 A. System V AMD64 ABI (Linux, macOS) 傳參順序 (前6個): RDI →\rightarrow→ RSI →\rightarrow→ RDX →\rightarrow→ RCX →\rightarrow→ R8 →\rightarrow→ R9 超過6個: 壓入棧 (Stack)。 返回值: RAX (若結果為 128 位,高位在 RDX)。 System Call: 使用 syscall 指令,參數傳給 RAX (系統調用號), RDI, RSI, RDX, R10, R8, R9。 B. Microsoft x64 (Windows) 傳參順序 (前4個): RCX →\rightarrow→ RDX →\rightarrow→ R8 →\rightarrow→ R9 超過4個: 壓入棧。 Shadow Space: 調用者必須在棧上預留 32位元組 (4個暫存器寬) 的空間,即使參數很少。 返回值: RAX。 關鍵狀態標誌 控制流指令 (JMP, Jcc) 的依據。 ZF (Zero Flag): 運算結果為 0 時置位。 SF (Sign Flag): 結果為負(最高位為1)時置位。 OF (Overflow Flag): 有符號運算溢出時置位。 CF (Carry Flag): 無符號運算進位/借位時置位。 浮點與 SIMD (AVX/SSE) XMM0 - XMM15: 128位 (Legacy SSE)。 YMM0 - YMM15: 256位 (AVX,低 128 位映射到 XMM)。 ZMM0 - ZMM31: 512位 (AVX-512,低 256 位映射到 YMM)。 參數傳遞 (System V ABI): 浮點參數: 前 8 個浮點參數通過 XMM0 到 XMM7 傳遞。 浮點返回: 通過 XMM0 返回。 易失性: XMM0 - XMM15 通常都是 Caller-saved (易失的)。

2025/12/9
articleCard.readMore

CSAPP Data Lab 解析

前一段時間做完了 CSAPP 的第一個 Lab,寫一篇總結。(其實這篇文章拖了很久) CS:APP Data Lab 旨在通過一系列位操作謎題,訓練對整數和浮點數底層表示(特別是補碼和 IEEE 754 標準)的理解。要求在嚴格限制的操作符和操作數數量下,實現特定的數學或邏輯功能。 函數名 (Name)描述 (Description)難度 (Rating)最大操作數 (Max ops) bitXor(x, y)只使用 & 和 ~ 實現 x ^ y (異或)。114 tmin()返回最小的補碼整數 (Two’s complement integer)。14 isTmax(x)僅當 x 是最大的補碼整數時返回 True。110 allOddBits(x)僅當 x 的所有奇數位都為 1 時返回 True。212 negate(x)返回 -x,不使用 - 運算符。25 isAsciiDigit(x)如果 0x30 <= x <= 0x39 (即 ASCII 數字字元) 則返回 True。315 conditional(x, y, z)等同於 x ? y : z (三元運算符)。316 isLessOrEqual(x, y)如果 x <= y 返回 True,否則返回 False。324 logicalNeg(x)計算 !x (邏輯非),不使用 ! 運算符。412 howManyBits(x)用補碼表示 x 所需的最小位數。490 floatScale2(uf)對於浮點參數 f,返回 2 * f 的位級等價表示。430 floatFloat2Int(uf)對於浮點參數 f,返回 (int)f 的位級等價表示。430 floatPower2(x)對於整數 x,返回 2.0^x 的位級等價表示。430 bitXor 該題要求僅使用 ~(取反) 和 &(與),實現 ^(異或) 1 2 3 int bitXor(int x, int y) { return ~((~(x&~y))&(~((~x)&y))); } 使用 De Morgan 律,容易得到 ~(x&y) = (~x)|(~y),於是我們可以使用 ~ 和 & 實現 | 操作。 異或操作,可以表示為 x^y = (~x & y) | (x & ~y),結合 De Morgan 律,我們很容易得到最終的答案 x^y = ~((~(x&~y))&(~((~x)&y)))。 tmin 這道題很簡單,返回最小的補碼整數。回顧補碼的定義,最高位取負權,故令符號位為 1 即可。 1 2 3 int tmin(void) { return 1<<31; } isTmax 判斷 x 是否是最大的補碼。若是,返回 1;否則,返回 0。 1 2 3 4 5 int isTmax(int x) { int map = x + 1; int res = ~(map + x); return !res & (!!map); } 最大的補碼有一個性質,加一之後變成最小的補碼:0x7fffffff -> 0x80000000 而最大的補碼加上最小的補碼等於 0xffffffff 即 -1,取反之後為 0 (這裡推出 0 是為了得到返回值中的 0/1) 因此,我們可以通過 ~(x+x+1) 得到答案。 但是 -1+0 也等於 -1,即如果 x=0 時,~(x+x+1) 同樣等於 1,是一個 Corner Case。 因此,我們還需要對結果與 !!(x+1),才能得到最終的答案。(如果 x=-1,!!(x+1)=0;其餘情況均為 1) 於是我們得到最終的答案 !(~(x+x+1)) & (!!(x+1)) allOddBits 僅當 x 的所有奇數位都為 1 時返回 1 1 2 3 4 5 6 int allOddBits(int x) { int a = 0xAA; int b = (a<<8) + (a<<16) + (a <<24) + a; int bm = ~b+1; return !((x&b)+bm); } 我們做一個奇數位掩碼即可 0xAA = 0b10101010,通過左移,可以得到 a + (a<<8) + (a<<16) + (a <<24) = 0xAAAAAAAA = b 於是 x&b 取出所有奇數位,但是我們需要得到 0/1 的答案 bm = ~b + 1,得到 -b(取反加一是補碼相反數),b+(-b) = 0,再取邏輯非,就可以得到答案 negate 這道題要求不使用 - 運算符計算 -x 1 2 3 int negate(int x) { return ~x+1; } 非常簡單,根據補碼的定義得到。取反加一就是相反數。 isAsciiDigit 如果 0x30 <= x <= 0x39 (即 ASCII 數字字元) 則返回 True。 我們在這道題中不能使用 <= 這類運算符,因此,我們想到,進行減法之後取符號位的操作。 1 2 3 4 5 int isAsciiDigit(int x) { int ge_30 = !((x + (~0x30 + 1)) >> 31); int le_39 = !((0x39 + (~x + 1)) >> 31); return ge_30 & le_39; } conditional 使用位運算實現三目運算符(x ? y : z) 1 2 3 4 5 int conditional(int x, int y, int z) { int xb = !(!x); int M = ~xb + 1; return (M&y) | (~M&z); } 我們可以使用邏輯掩碼 先使用 !(!x) 將 x 轉換成 0/1,記為 xb ~xb + 1,則有 0 -> 0;1 -> -1 = 0xffffffff(掩碼,取所有位) 因此,(M&y) | (~M&z) 就是最終的答案。 如果 x = 1,M = 0xffffffff,~M = 0,取 y;否則,取 z isLessOrEqual 1 2 3 int isLessOrEqual(int x, int y) { return !((y+(~x+1))>>31); } 簡單判斷符號位即可。但是實現的是 <=,對 > 取非即可 logicalNeg 計算 !x (邏輯非),不使用 ! 運算符 1 2 3 int logicalNeg(int x) { return ((x>>31) | ((~x+1)>>31))+1; } howManyBits 計算用補碼表示 x 所需的最小位數 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int howManyBits(int x) { int fg = x>>31; x = ((~fg) & x) | (fg &(~x)); int h16 = !!(x >> 16) << 4; x >>= h16; int h8 = !!(x>>8) << 3; x >>= h8; int h4 = !!(x>>4) << 2; x >>= h4; int h2 = !!(x>>2) << 1; x>>=h2; int h1 = !!(x>>1); x>>=h1; int h0 = x; return h0 + h1 + h2 + h4 + h8 + h16 + 1; } 這道題,先選取符號位,然後計算之後的最高位即可。 為了方便計算,我們把負數補碼表示為正數,這樣就只用計算最高位的 1 在哪裡就行了 ((~fg) & x) | (fg & (~x)) 是一個條件取反操作,相當於 x = (x < 0) ? ~x : x 若 fg 為 0(正數):表達式變為 (All_1 & x) | (0 & ~x) -> x。保持不變。 若 fg 為 -1(負數):表達式變為 (0 & x) | (All_1 & ~x) -> ~x。按位取反。 這裡提醒各位,此處補碼右移是算術右移,所以負數右移得到一個所有位都為 1 的數,也就是 -1。 接下來進行位的二份尋找: 這裡的邏輯是**“分治法”**。我們有 32 位要檢查,像二分尋找一樣: 檢查高 16 位: x >> 16:如果不為 0,說明最高位的 1 在高 16 位中(即位 16-31)。 !!(...):將結果轉化為 0 或 1。如果高 16 位有數,結果為 1,否則為 0。 1<< 4:如果高 16 位有數,說明我們至少需要 16 位,即 1 << 4 = 16。 h16:這就是我們找到的基數(0 或 16)。 x >>= h16:關鍵點。如果我們確定高 16 位有數,我們將 x 右移 16 位,丟棄低 16 位,接下來的檢查只關注剛才的高 16 位。如果高 16 位全是 0,x 保持不變,我們繼續檢查原本的低 16 位。 檢查高 8 位(在剩下的 16 位範圍內): 邏輯同上。如果剩下的這部分的高 8 位有數,則 h8 = 8,並將 x 右移 8 位。 依此類推: h4:檢查剩下的 4 位中的高 2 位… (這裡代碼邏輯是一致的,檢查高4位)。 h2:檢查剩下的 4 位。 h1:檢查剩下的 2 位。 h0 = x:檢查最後剩下的 1 位。 最後,我們計算 h16+…+h0 的總和即可。這裡要注意,補碼有一個符號位,所以結果還要再 +1。 得到答案:h0 + h1 + h2 + h4 + h8 + h16 + 1 floatScale2 對於浮點參數 f,返回 2 * f 的位級等價表示 IEEE 754 我們先來回顧一下浮點數的位級表示,即 IEEE 754,這裡以 float 為例 浮點數位中有三段: Sign (s): 1 bit [31] -> 符號位 Exponent (exp): 8 bits [30:23] -> 階碼 Fraction (frac): 23 bits [22:0] -> 尾數 1 2 3 int sign = (uf >> 31) & 0x1; int exp = (uf >> 23) & 0xFF; int frac = uf & 0x7FFFFF; 對於一個浮點數的解釋,有三種情況: Case A: 非規格化 (Denormalized) 特徵:exp == 0 真實值:V=(−1)s×M×21−BiasV = (-1)^s \times M \times 2^{1-Bias}V=(−1)s×M×21−Bias 這裡 M=0.fracM = 0.fracM=0.frac (沒有隱含的 1) Case B: 規格化 (Normalized) 特徵:exp != 0 且 exp != 255 真實值:V=(−1)s×M×2exp−BiasV = (-1)^s \times M \times 2^{exp-Bias}V=(−1)s×M×2exp−Bias 這裡 M=1.fracM = 1.fracM=1.frac (有一個隱含的 1) Bias = 127 Case C: 特殊值 (Special Values) 特徵:exp == 255 (全 1) 類型: frac == 0:Infinity (無窮大) frac != 0:NaN (Not a Number) 接下來我們看這道題,這道題只需要注意分類討論就可以。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 unsigned floatScale2(unsigned uf) { unsigned s = uf >> 31; unsigned exp = (uf >> 23) & 0xFF; unsigned ff = uf & 0x7fffff; // 特殊值 (Special Values) // 如果階碼全為1 (exp == 255),表示 NaN (非數) 或 Infinity (無窮大) // 規則:NaN * 2 = NaN, Inf * 2 = Inf,直接返回原值 if (exp == 0xFF) { return uf; } // 非規格化數 (Denormalized) // 如果階碼為0,表示非規格化數,數值非常接近 0 if (exp == 0) { // 非規格化數乘以2:直接將尾數左移一位 ff <<= 1; // 檢查尾數是否溢出 (從非規格化過渡到規格化) // 如果左移後 ff 超過了 23 位能表示的最大值 (即 0x7fffff) // 說明最高位變成了 1,這個 1 應該“進位”給階碼 if (ff > 0x7fffff) { ff -= 0x800000; // 去掉溢出的那一位 (因為它現在變成了隱含的 1) exp += 1; // 階碼從 0 變為 1 (成為規格化數) } } // 規格化數 (Normalized) else { // 規格化數乘以2:直接給階碼加 1 exp += 1; // 檢查階碼上溢 (Overflow) // 如果加 1 後階碼變成了 255,說明數值太大,變成了無窮大 (Infinity) if (exp == 0xFF) { ff = 0; // 無窮大的定義是 exp=255 且 frac=0 } } return (s << 31) | (exp << 23) | (ff); }``` ## floatFloat2Int 對於浮點參數 `f`,返回 `(int)f` 的位級等價表示 ```c int floatFloat2Int(unsigned uf) { unsigned s = uf >> 31; unsigned exp = (uf >> 23) & 0xFF; unsigned ff = uf & 0x7fffff; // 處理特殊情況:NaN (非數) 或 Infinity (無窮大) // 當階碼全為 1 時。根據題目要求,越界通常返回 TMin (0x80000000) if (exp == 0xFF) { return 0x80000000u; } // 處理非規格化數 (Denormalized) // 當階碼全為 0 時,數值極小 (0.xxxx * 2^-126),轉換為 int 必定為 0 if (exp == 0) { return 0; } // 計算真實指數 E // Bias (偏置值) 是 127。 E = exp - Bias int E = (int)exp - 127; // 處理小於 1 的數 // 如果真實指數小於 0 (例如 2^-1, 2^-2),數值為 0.xxxx // 強轉 int 會向零截斷,結果為 0 if (E < 0) return 0; // 還原隱含的 1 (Restore Implicit 1) // 規格化數的真實尾數形式是 1.fffff... // 我們手動把第 23 位置 1,代表那個隱含的整數部分 "1" ff = ff | (1 << 23); // 處理溢出 (Overflow) // 如果指數 E >= 31,說明數值 magnitude >= 2^31 // int 的最大值是 2^31 - 1。 // 無論是正數溢出,還是負數正好是 TMin (-2^31) 或更小, // 按照題目規則,都返回 TMin (0x80000000) if (E >= 31) { return 0x80000000u; } // 位移對齊 (Bit Shifting) // 現在的 ff 看起來是這樣: [1]. [xxxxxx]... (1 在第 23 位) // 這相當於 1.xxxxx * 2^23 (如果在整數暫存器看) // 我們實際需要的是 1.xxxxx * 2^E if (E < 23) { // 情況 A: 指數較小 (例如 E = 20) // 我們需要將小數點右移 20 位。 // 但當前 ff 是左對齊在第 23 位的,所以需要**右移**丟棄多餘的小數位。 // 移位量 = 23 - 20 = 3 ff = ff >> (23 - E); } else { // 情況 B: 指數較大 (例如 E = 30) // 我們需要將小數點右移 30 位。 // 當前只在第 23 位,不夠,需要**左移**補零。 // 移位量 = 30 - 23 = 7 ff = ff << (E - 23); } // 處理符號 // 如果原數是負數,進行取反加一 (即 -ff) if (s) return -ff; // 原數是正數,直接返回 return ff; } floatPower2 對於整數 x,返回 2.0^x 的位級等價表示。對於這道題,計算出幾個臨界點即可。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 unsigned floatPower2(int x) { // 1. 處理下溢 (Underflow) // 最小的非規格化數是 2^(-149)。 // 計算邏輯:Min Denorm = 2^(1-Bias) * 2^(-23) = 2^(-126) * 2^(-23) = 2^(-149) // 如果 x 比這個還小,說明數值太小無法表示,直接返回 0.0 if (x < -149) return 0; // 2. 處理非規格化數 (Denormalized) // 範圍:[-149, -127] // 非規格化數的階碼 (exp) 全為 0,值公式為:M * 2^(-126) // 我們需要構建 2^x。 // 方程:2^x = (1 << shift) * 2^(-23) * 2^(-126) <-- (1<<shift)*2^-23 是尾數部分 // 2^x = 2^shift * 2^(-149) // x = shift - 149 // shift = x + 149 // 所以,我們將 1 左移 (x + 149) 位放在尾數部分 (Fraction) else if (x < -126) return 1 << (x + 149); // 3. 處理規格化數 (Normalized) // 範圍:[-126, 127] // 規格化數的值公式為:1.0 * 2^(exp - Bias) // 我們需要 2^x,尾數部分保持為 0 (即 1.0),只需要設置階碼。 // 方程:x = exp - Bias // exp = x + Bias // exp = x + 127 // 將計算出的 exp 移到階碼的位置 (第 23-30 位) else if (x <= 127) return (x + 127) << 23; // 4. 處理上溢 (Overflow) // 範圍:x > 127 // 單精度浮點數最大能表示的 2 的冪是 2^127。 // 超過這個值,返回正無窮大 (+Infinity)。 // +Inf 的表示:符號位 0,階碼全 1 (0xFF),尾數全 0。 else return (0xFF) << 23; } 小結 Data Lab 實驗使我深入理解整數(補碼)和浮點數(IEEE 754)在二進制層面的表示方法,透過使用一組極其受限的位運算符(如 ~, &, |, ^, +, <<, >>)來實現複雜的邏輯、算術、比較和類型轉換操作,從而真正掌握了位運算的技巧。 我的代碼存放在 aeilot/CSAPP-Labs。

2025/12/1
articleCard.readMore

矩陣的 Modified Gram Schmidt 方法

矩陣的 QR 分解在電腦運算中可能造成誤差,本文探討一下一種改進版本的 Gram-Schmidt 正交化方法。 經典的 Gram-Schmidt 方法可能造成數值不穩定性。在電腦中,舍入誤差可能會累積,造成得到的正交基並不具有正交性。 經典的 Gram-Schmidt 我們先來回顧一下經典的 Gram-Schmidt 方法: 1 2 3 4 5 6 for j = 1 : n v_j = x_j for k = 1 : j - 1 v_j = v_j - ( (v_k^T x_j) / (v_k^T v_k) ) * v_k endfor endfor 最終我們將 vjv_jvj​ 歸一化得到標準正交基 qj=(vj)/(∣vj∣)q_j = (v_j)/(| v_j |)qj​=(vj​)/(∣vj​∣) 。 注意:CGS 始終使用原始向量 xjx_jxj​ 與之前的基向量計算投影 數學證明 簡單進行數學證明 定義第 jjj 步生成的向量 vjv_jvj​ vj=xj−∑k=1j−1((vkTxj)/(vkTvk))vkv_j = x_j - \sum_{k=1}^{j-1} ( (v_k^T x_j) / (v_k^T v_k) ) v_kvj​=xj​−k=1∑j−1​((vkT​xj​)/(vkT​vk​))vk​ 選取任意一個之前的基向量 vmv_mvm​(其中 1≤m<j1 \le m < j1≤m<j),計算它與 vjv_jvj​ 的內積 vmTvjv_m^T v_jvmT​vj​: vmTvj=vmT(xj−∑k=1j−1((vkTxj)/(vkTvk))vk)v_m^T v_j = v_m^T ( x_j - \sum_{k=1}^{j-1} ((v_k^T x_j) / (v_k^T v_k)) v_k )vmT​vj​=vmT​(xj​−k=1∑j−1​((vkT​xj​)/(vkT​vk​))vk​) 利用內積的線性性質,將 vmTv_m^TvmT​ 乘進去: vmTvj=vmTxj−∑k=1j−1((vkTxj)/(vkTvk))(vmTvk)v_m^T v_j = v_m^T x_j - \sum_{k=1}^{j-1} ((v_k^T x_j) / (v_k^T v_k)) (v_m^T v_k)vmT​vj​=vmT​xj​−k=1∑j−1​((vkT​xj​)/(vkT​vk​))(vmT​vk​) 由於前提假設 v1,…,vj−1v_1, \dots, v_{j-1}v1​,…,vj−1​ 是兩兩正交的,所以在求和符號 ∑\sum∑ 中: 當 k≠mk \neq mk=m 時,vmTvk=0v_m^T v_k = 0vmT​vk​=0 。 當且僅當 k=mk = mk=m 時,vmTvk=vmTvm≠0v_m^T v_k = v_m^T v_m \neq 0vmT​vk​=vmT​vm​=0 。 因此,求和項中只剩下 k=mk=mk=m 這一項: vmTvj=vmTxj−((vmTxj)/(vmTvm))(vmTvm)v_m^T v_j = v_m^T x_j - ((v_m^T x_j) / (v_m^T v_m)) (v_m^T v_m)vmT​vj​=vmT​xj​−((vmT​xj​)/(vmT​vm​))(vmT​vm​) 分子分母中的標量 vmTvmv_m^T v_mvmT​vm​ 相互抵消: vmTvj=vmTxj−vmTxjv_m^T v_j = v_m^T x_j - v_m^T x_jvmT​vj​=vmT​xj​−vmT​xj​ vmTvj=0v_m^T v_j = 0vmT​vj​=0 證畢 誤差分析 如果在 CGS(經典格拉姆-施密特正交化)中計算 q2q_2q2​ 時發生誤差,導致 q1Tq2=δq_1^T q_2 = \deltaq1T​q2​=δ 是一個很小但非零的數值,這個誤差將不會在隨後的任何計算中被修正: v3=x3−(q1Tx3)q1−(q2Tx3)q2v_3 = x_3 - (q_1^T x_3)q_1 - (q_2^T x_3)q_2 v3​=x3​−(q1T​x3​)q1​−(q2T​x3​)q2​ q2Tv3=q2Tx3−(q1Tx3)δ−(q2Tx3)=−(q1Tx3)δq_2^T v_3 = q_2^T x_3 - (q_1^T x_3)\delta - (q_2^T x_3) = -(q_1^T x_3)\delta q2T​v3​=q2T​x3​−(q1T​x3​)δ−(q2T​x3​)=−(q1T​x3​)δ q1Tv3=q1Tx3−(q1Tx3)−(q2Tx3)δ=−(q2Tx3)δq_1^T v_3 = q_1^T x_3 - (q_1^T x_3) - (q_2^T x_3)\delta = -(q_2^T x_3)\deltaq1T​v3​=q1T​x3​−(q1T​x3​)−(q2T​x3​)δ=−(q2T​x3​)δ 我們可以看出 v3v_3v3​ 與 q1q_1q1​ 或 q2q_2q2​ 都不正交。 改進的 Gram-Schmidt (MGS) 1 2 3 4 5 6 7 8 9 10 for j = 1 : n v_j = x_j endfor for j = 1 : n q_j = v_j / ||v_j||_2 for k = j + 1 : n v_k = v_k - (q_j^T v_k) q_j endfor endfor 最終我們將 vjv_jvj​ 歸一化得到標準正交基 qj=(vj)/(∣vj∣)q_j = (v_j)/(| v_j |)qj​=(vj​)/(∣vj​∣) 。 區別在於,一旦算出來一個向量 qqq,立即用它去更新後面所有的向量(減去向量在 qqq 上的投影),使得後面所有的向量與這個向量 qqq 正交。 誤差分析 假設 MGS 出現:q1Tq2=δq_1^T q_2 = \deltaq1T​q2​=δ 很小但非零。 對於第三個向量 v3v_3v3​ : 初始狀態:v3(0)=x3v_3^{(0)} = x_3v3(0)​=x3​ j=1:v3(1)=v3(0)−(q1Tv3(0))q1j = 1: v_3^{(1)} = v_3^{(0)} - (q_1^T v_3^{(0)})q_1j=1:v3(1)​=v3(0)​−(q1T​v3(0)​)q1​ j=2:v3=v3(1)−(q2Tv3(1))q2j = 2: v_3 = v_3^{(1)} - (q_2^T v_3^{(1)})q_2j=2:v3​=v3(1)​−(q2T​v3(1)​)q2​ 此時 v3v_3v3​ 是第三個向量在歸一化之前的最終形式。 讓我們檢查正交性,假設不再產生其他誤差: q2Tv3=q2Tv3(1)−(q2Tv3(1))=0q_2^T v_3 = q_2^T v_3^{(1)} - (q_2^T v_3^{(1)}) = 0q2T​v3​=q2T​v3(1)​−(q2T​v3(1)​)=0 所以,我們保留了對 q2q_2q2​ 的正交性。 接下來看 q1q_1q1​: q1Tv3=q1Tv3(1)−(q2Tv3(1))δq_1^T v_3 = q_1^T v_3^{(1)} - (q_2^T v_3^{(1)})\deltaq1T​v3​=q1T​v3(1)​−(q2T​v3(1)​)δ q2Tv3(1)=q2Tv3(0)−(q1Tv3(0))δq_2^T v_3^{(1)} = q_2^T v_3^{(0)} - (q_1^T v_3^{(0)})\deltaq2T​v3(1)​=q2T​v3(0)​−(q1T​v3(0)​)δ q1Tv3(1)=q1Tv3(0)−(q1Tv3(0))=0q_1^T v_3^{(1)} = q_1^T v_3^{(0)} - (q_1^T v_3^{(0)}) = 0q1T​v3(1)​=q1T​v3(0)​−(q1T​v3(0)​)=0 由此可得: q1Tv3=−(q2Tv3(0)−(q1Tv3(0))δ)δ=−q2Tv3(0)δ+q1Tv3(0)δ2q_1^T v_3 = -(q_2^T v_3^{(0)} - (q_1^T v_3^{(0)})\delta)\delta= -q_2^T v_3^{(0)}\delta + q_1^T v_3^{(0)}\delta^2q1T​v3​=−(q2T​v3(0)​−(q1T​v3(0)​)δ)δ=−q2T​v3(0)​δ+q1T​v3(0)​δ2 由於 δ\deltaδ 非常小,δ2\delta^2δ2 就更小了。因此 q1Tv3q_1^T v_3q1T​v3​ 中的誤差和 CGS 差不多,但我們消除了 q2Tv3q_2^T v_3q2T​v3​ 中的誤差,使得誤差更小。 總結 在電腦中,由於浮點運算的特殊性,有許多數學方法需要重新考慮,儘量提高數值穩定性。這使得我們有必要重新審視我們學習過的數學知識,針對電腦的特殊性進行重新設計與優化。

2025/11/27
articleCard.readMore

聊一聊位掩碼(Bit Mask)

掩碼 (Mask) 是一種位運算技巧,它使用一個特定的值(掩碼)與目標值進行 &\mathtt{\&}& (與)、∣\mathtt{|}∣ (或)、∧\mathtt{\wedge}∧ (異或) 運算,以精確地、批次地操作、提取或檢查目標值中的一個或多個位。 基本概念 掩碼利用位運算的特性,透過設定掩碼中的特定位為 1 或 0,來控制目標值中對應位的行為。 具體來說,掩碼可以用來提取某些位的值,清除某些位的值,反轉某些位的值,或者設定某些位的值。 提取位 透過與運算(&\mathtt{\&}&)和一個掩碼,可以提取目標值中特定位置的位。例如,假設我們有一個 8 位的二進位制數 10101100,我們想提取其中的第 3 位(從右數起,0 開始計數)。我們可以使用掩碼 00000100: 1 2 3 4 10101100 (目標值) & 00000100 (掩碼) ------------ 00000100 (結果) 結果 00000100 表示第 3 位是 1。 這一技巧可以用來提取多位,比如想要提取某個數的低 4 位,可以使用掩碼 00001111。 清除位 透過與運算(&\mathtt{\&}&)和一個掩碼,可以清除目標值中特定位置的位。例如,假設我們有一個 8 位的二進位制數 10101100,我們想清除其中的第 3 位。我們可以使用掩碼 11111011: 1 2 3 4 10101100 (目標值) & 11111011 (掩碼) ------------ 10101000 (結果) 結果 10101000 表示第 3 位被清除為 0。 清除就是不提取某些位 lol 反轉位 透過異或運算(∧\mathtt{\wedge}∧)和一個掩碼,可以反轉目標值中特定位置的位。例如,假設我們有一個 8 位的二進位制數 10101100,我們想反轉其中的第 3 位。我們可以使用掩碼 00000100: 1 2 3 4 10101100 (目標值) ^ 00000100 (掩碼) ------------ 10101000 (結果) 結果 10101000 表示第 3 位被反轉。 設定位 透過或運算(∣\mathtt{|}∣)和一個掩碼,可以設定目標值中特定位置的位。例如,假設我們有一個 8 位的二進位制數 10101000,我們想設定其中的第 3 位為 1。我們可以使用掩碼 00000100: 1 2 3 4 10101000 (目標值) | 00000100 (掩碼) ------------ 10101100 (結果) 結果 10101100 表示第 3 位被設定為 1。 構造掩碼 構造合適的掩碼是使用技巧的關鍵。 單個位: 1≪n\mathtt{1 \ll n}1≪n 1≪5\mathtt{1 \ll 5}1≪5 (00100000\mathtt{00100000}00100000) 是第 5 位的掩碼。 連續低位: (1≪n)−1\mathtt{(1 \ll n) - 1}(1≪n)−1 (1≪8)−1\mathtt{(1 \ll 8) - 1}(1≪8)−1 (0xFF\mathtt{0xFF}0xFF) 是低 8 位的掩碼。 全 1 掩碼: ∼0\mathtt{\sim 0}∼0 (即 −1-1−1) 0xFFFFFFFF\mathtt{0xFFFFFFFF}0xFFFFFFFF (假設 32 位) 全 0 掩碼: 0\mathtt{0}0 條件掩碼 在 CSAPP Data Lab 中,我們有一道題目要求用位運算實現三目運算子 x ? y : z。我們可以使用條件掩碼來實現這一點。 1 2 3 4 5 int conditional(int x, int y, int z) { int mask = !!x; // mask 為 1 如果 x 非零,否則為 0 mask = ~mask + 1; // mask 為 0xFFFFFFFF 如果 x 非零,否則為 0x0 return (y & mask) | (z & ~mask); } 這段程式碼的邏輯是: 計算 mask = !!x,如果 x 非零,mask 為 1,否則為 0。 透過 mask = ~mask + 1,將 mask 轉換為全 1 (0xFFFFFFFF) 或全 0 (0x0)。 返回 (y & mask) | (z & ~mask),如果 x 非零,結果為 y,否則為 z。 總結 掩碼是一種強大的位運算技巧,可以用來精確地操作和檢查資料中的特定位。 透過合理構造掩碼,我們可以高效地實現各種位操作,如提取、清除、反轉和設定位。在實際程式設計中,掌握掩碼的使用能夠幫助我們編寫出更高效、更簡潔的程式碼。

2025/10/20
articleCard.readMore

整數溢位與未定義行為

在做 CSAPP Data Lab 的時候,關於整數溢位,遇到一些問題。 題幹 1 2 3 4 5 6 7 8 9 10 11 /*  * isTmax - returns 1 if x is the maximum, two's complement number,  *     and 0 otherwise   *   Legal ops: ! ~ & ^ | +  *   Max ops: 10  *   Rating: 1  */ int isTmax(int x) {   return 2; } 題目要求,僅僅使用運算子 ! ~ & ^ | + 來判斷一個數是否是最大的二的補碼(int 範圍內),即 0x7fffffff。如果是,輸出 1;否則,輸出 0。 思路 由於我們不能使用移位操作(很多人會直接 1<<31 - 1),可以考慮整數溢位的特殊性質。 具體地,我們有 0x7fffffff + 1 = 0x80000000,符號改變。 而 0x80000000 + 0x80000000 = 0 我們可以得到 x = 0x7fffffff 滿足 x + 1 + x + 1 = 0 而對於其他數字,假設 y = x + k 其中 k 非零,則有 y + 1 + y + 1 = 2*k 此時,我們發現,對於 y=-1 也有 y + 1 + y + 1 = 0,需要排除掉 其他情況下,非零數轉換為 bool 型別自動變為 1 我們不難寫出以下程式碼: 1 2 3 4 5 int isTmax(int x) {   int p1 = x+1;   int p2 = p1 + p1;   return !(p2) & !!(p1); } 發現問題 這段程式碼在我本地(macOS,Apple clang version 17.0.0 (clang-1700.3.19.1), Target: arm64-apple-darwin25.0.0) 上執行,使用命令 clang main.c 是沒有任何問題的。 但是,檢查到 CSAPP 提供的 Makefile,有 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 # # Makefile that builds btest and other helper programs for the CS:APP data lab # CC = gcc CFLAGS = -O -Wall LIBS = -lm all: btest fshow ishow btest: btest.c bits.c decl.c tests.c btest.h bits.h $(CC) $(CFLAGS) $(LIBS) -o btest bits.c btest.c decl.c tests.c fshow: fshow.c $(CC) $(CFLAGS) -o fshow fshow.c ishow: ishow.c $(CC) $(CFLAGS) -o ishow ishow.c # Forces a recompile. Used by the driver program. btestexplicit: $(CC) $(CFLAGS) $(LIBS) -o btest bits.c btest.c decl.c tests.c clean: rm -f *.o btest fshow ishow *~ 注意到,編譯器使用了 -O flag,即 O1 最佳化。 此時執行這段程式碼,對於 0x7fffffff 輸出 0,懷疑可能是編譯器最佳化時,假設未定義行為(整數溢位)不會發生,將 !p2 最佳化。p1 + p1 的形式過於簡單。 未定義行為 未定義行為(UB),根據 cppreference 的定義: 1 undefined behavior - There are no restrictions on the behavior of the program. 有符號整數溢位是一種常見的未定義行為。 Because correct C++ programs are free of undefined behavior, compilers may produce unexpected results when a program that actually has UB is compiled with optimization enabled. 也就是說,編譯器最佳化會對未定義行為產生意料之外的結果 cppreference 給出了一個整數溢位的例子: 1 2 3 4 5 int foo(int x) { return x + 1 > x; // either true or UB due to signed overflow } 編譯之後卻變成了 1 2 3 foo(int): mov eax, 1 ret 意思是,不管怎麼樣都輸出 1 觀察出錯程式碼 我們透過 gcc -S 輸出編譯後的彙編程式碼 1 2 3 4 5 6 7 _Z6isTmaxi: .LFB2: .cfi_startproc endbr64 movl$0, %eax ret .cfi_endproc 我們看到,編譯器直接把這個函式返回值改成了 0,不管輸入什麼,與我們的錯誤原因推斷是相同的。 修改 我們可以嘗試構造一個更復雜的、不易被簡單規則匹配的表示式,躲過 O1 級別的最佳化。 核心思路不變,仍然是利用 Tmax + 1 = Tmin 這個特性。我們來觀察一下 Tmax 和 Tmin 在二進位制下的關係: Tmax = 0x7fffffff = 0111...1111 Tmin = 0x80000000 = 1000...0000 一個非常有趣的性質是 Tmax + Tmin = -1 (0xffffffff)。 1 2 3 4 0111 1111 ... 1111 (Tmax) + 1000 0000 ... 0000 (Tmin) ------------------------- 1111 1111 ... 1111 (-1) 基於這個觀察,我們可以設計一個新的檢查方案:如果一個數 x 是 Tmax,那麼 x + (x+1) 的結果就應該是 -1。取反後 ~(-1) 則為 0。 我們可以寫出如下的修改版程式碼: 1 2 3 4 5 int isTmax(int x) { int map = x + 1; int res = ~(map + x); return !res & (!!map); } 這段程式碼的邏輯是: 計算 map = x + 1。對於 x = Tmax,這裡同樣會發生有符號溢位,map 變為 Tmin。這依然是未定義行為(UB)。 計算 res = ~(map + x)。如果 x 是 Tmax,這一步就是 ~(Tmin + Tmax),結果為 ~(-1),即 0。 return !res & (!!map)。!res 為 !0,即 1。!!map 部分和之前的版本一樣,是為了排除 x = -1 的情況(此時 map 為 0, !!map 為 0,最終返回 0)。 這段程式碼在 -O 最佳化下可能會得到正確的結果。 為什麼這個“可能”有效? 我們必須清醒地認識到,新版本的程式碼本質上沒有解決未定義行為的問題,它只是“僥倖”地繞過了當前編譯器版本的特定最佳化策略。 程式碼模式的複雜性:p1 + p1 ((x+1)+(x+1)) 是一個非常簡單直白的模式,最佳化器很容易建立一個“如果 p1 非零,則 p1+p1 結果也非零”的最佳化規則。而 ~((x+1)+x) 混合了加法和位運算,模式更復雜,可能沒有觸發編譯器中已有的、基於UB的最佳化捷徑。 最佳化的機會主義:編譯器最佳化並不是要窮盡所有的數學可能,而是應用一系列已知的高效模式。我們的新程式碼恰好不在這些常見模式的“黑名單”上。 所以,這個修改版只是一個更具迷惑性的“偽裝”。它在特定環境下能工作,但其行為是不被C語言標準所保證的,在不同的編譯器或未來的GCC版本下,它隨時可能失效。 結論:如何正確面對未定義行為 透過 isTmax 這個小小的函式,我們可以一窺C語言中未定義行為的危險性以及現代編譯器最佳化的強大。作為開發者,我們應該得到以下啟示: 不要依賴未定義行為:永遠不要編寫依賴於UB的程式碼,即使它“在你的機器上看起來能跑”。程式碼的健壯性來源於對語言標準的嚴格遵守,而非僥倖。 相信編譯器,但要驗證:編譯器非常聰明,它會嚴格按照語言規範進行最佳化。當你發現最佳化後的程式碼行為不符合你的“直覺”時,首先應該懷疑自己的程式碼是否觸碰了UB的紅線。 善用工具: 始終開啟編譯器警告 (-Wall -Wextra) 並將警告視為錯誤 (-Werror),這能幫你發現許多潛在問題。 使用執行時檢測工具,如GCC/Clang的 UndefinedBehaviorSanitizer (UBSan)。只需在編譯時加上 -fsanitize=undefined,它就能在程式執行時精確地捕獲有符號整數溢位等UB,是除錯這類問題的神器。 對於CSAPP Data Lab這道題來說,它的目的正是為了讓我們在“規則的鐐銬”下舞蹈,從而深刻理解整數表示、運算和編譯器行為。而我們在實際工程中,最安全、最清晰的寫法永遠是第一選擇。

2025/10/13
articleCard.readMore

快速排序 幾種劃分方法討論

最近在複習資料結構與演算法,聊聊快速排序的幾種劃分演算法。 快速排序思路 快速排序是一種基於分治策略的排序演算法。 對於待排序陣列,其核心操作是: 選取一個基準數pivot 將陣列分為兩塊,一塊小於等於基準數,另一塊大於等於基準數(注意 基準數作為分界點) 遞迴地對於分出來的兩個數字再次進行快速排序 不斷地進行劃分,遞迴,最後能保證整個陣列有序。 基準數的選取 一般來說,基準數有幾種選取方法: 選取第一個數或者最後一個數:這樣做,如果陣列已經排好序,則每次劃分都是基準數和剩餘數,時間複雜度升至O(n^2) 隨機選擇一個數 選取中位數:這樣做始終能對原陣列進行等分,尋找中位數是線性時間的。 下面以最無腦的方法1為例。 幾種劃分方法 主要的劃分方法有: 樸素劃分 Lomuto 劃分 Hoare 劃分 中國大陸教材中經常出現的是第三種,即 Hoare 劃分,也叫哨兵劃分。 樸素劃分 思路很無腦。直接開一個新陣列,把比 pivot 小的放左邊,比 pivot 大的放右邊,最後把陣列複製回到原陣列。需要 O(n) 的額外空間。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 const int N; // 陣列長度 int A[N]; int tmp[N]; int Native_Partition(int left, int right) { int pivot = A[left]; int idx = left; for (int i = left; i <= right; i++) { if (A[i] < pivot) tmp[idx++] = A[i]; } // 先將小於等於 pivot 的複製進入 int pid = idx; tmp[idx++] = pivot; for (int i = left; i <= right; i++) { if (A[i] > pivot) tmp[idx++] = A[i]; } // 再將大於 pivot 的複製進入 for (int i = left; i <= right; i++) { A[i] = tmp[i]; } // 複製回到 A return pid; } Lomuto 劃分 Lomuto 劃分即使用單個指標,從左到右掃描陣列,交換,維持劃分順序。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 int Lomuto_Partition(int left, int right) { int pivot = A[right]; int i = left - 1; // 維護的劃分中最右側的比 pivot 小的元素的下標 // 遍歷陣列,把所有比 pivot 小的元素全部移動到左邊 for (int j = left; j <= right - 1; j++) { // 遍歷到 right - 1 可以對 pivot 進行保護,使得 pivot 不與自己對比,使邏輯更清晰,但或許沒必要 if (A[j] < pivot) { i++; swap(A[i], A[j]); } // j 維護的是遍歷時下標,當發現一個比 pivot 小的數時,如果 i + 1 == j 則不動,否則說明之前存在一個數比 `pivot` 大需要交換,則及時進行交換 } // 把 pivot 移回來 swap(A[i + 1], A[right]); return i + 1; } Hoare 劃分 Hoare 劃分採用雙指標,進行交換。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int Hoare_Partition(int left, int right) { int pivot = A[left]; int pid = left; while (left < right) { while (left < right && A[right] >= pivot) { right--; } // 先從右往左掃描,保護 pivot while (left < right && A[left] <= pivot) { left++; } // 再從左往右掃描 swap(A[left], A[right]); // 交換不正確的數 } // 跳出迴圈時,left = right 說明找到了 pivot 的位置 swap(A[pid], A[left]); return pid; } 掃描順序對於結果有影響。pivot 在最左邊時,應該先從右往左掃描。 如果先從左往右掃,找第一個大於 pivot 的數 a,而第一個小於 pivot 的數在 a 左側,此時進行 pivot 和 A[left] 的交換,會導致交換錯誤。故應該優先保證 pivot 左側都小於等於 pivot,即先尋找第一個小於 pivot 的數,這樣交換是安全的。 快速排序演算法 對於某劃分函式 partition(int st, int end) -> pivot: int 有 1 2 3 4 5 6 7 8 9 10 11 12 const int N; // 陣列長度 int A[N]; // 待排序陣列 void qsort(int st, int end){ if(st >= end) return; // 遞迴終止條件 int pivot = partition(st, end); // 遞迴 qsort(st, pivot-1); qsort(pivot+1, end); }

2025/9/11
articleCard.readMore

等待

此生此夜不長好,明月明年何處看? 雨巷獨行,秉燭對月。我走遍了整座城市,卻再也找不到那個獨特的角落。或許只是那盞燈或那半掩的門扉,在人生之中卻像風暴裡海上的信標,似這江南春雨中雲霧朦朧處,簷下的紙燈。舊夢重溫,幼時在夏日熟睡,蟬鳴聒噪,也第一次為我定義了等待的焦慮。吊扇之下徘徊的影,是否還停留在多年前原地踟躕。這種焦慮如影隨形,如何能擺脫呢? 紀念我的第一次等待,或許也是一切不幸的開始。手術室的無影燈下,護士讓我在門口先坐著等候。手術檯上是誰?高臺之上,一個迷茫的影。望不見,只感覺平靜,右手的暗痛,倉促奔走多時後的終點。手術室。幾分鐘前在門外稱重,一小時前乘汽車到來。回憶與夢夾雜。什麼時候開始?你從一數到一百吧。那護士肯定覺得我不會,竟然用這種無聊的方式來哄騙我。到底要等多久?年幼的我真的在數數,一二三四五……不知道數了多久,忽然間沒了意識,麻醉。下一個記憶片段便是走出醫院,我或許躺在病床上被推出去,但我只記得醫院的天花板,那玻璃天窗外明媚的陽光。回家的計程車,父親的笑容,遙遠的記憶,毛毯,被石膏束縛的右手,對話,呢喃。我在哪裡?我等待了多久?那年我三歲。 後來在家旁的公交車站,那個盛夏,上小學之前,我在傍晚等待父母歸來。或許是基因編碼的本性,在黃昏來的,天昏地暗,當暮色蠶食最後一縷殘陽,當夏蟬為墜落的太陽唱起輓歌,當路燈亮起,車燈流動,我也感受到一種從未有過的感受,如同草葉被風拂過,卻難以恢復平靜,露水撒了一地,又如同臥鋪火車在午夜停止,靜悄悄的深夜裡,窗外的暴雨和風吟,下鋪的男人低聲夢語。公交車站旁是公路的起點,站後有一條河,河對岸是一眼望不盡的工廠。不知道多少車駛過,向路的盡頭極目遠眺,外婆的藍布衫在暮靄裡洇開。空氣低沉,凝固,路燈忽明忽暗。我沒有數一二三,我靜靜地聽,靜靜地看。這裡沒有人也沒有聲音,只有蟬還在一遍一遍重複著那陳年老歌,每年不變的曲目。還有風吹過暗處的森林。 後來過去多少年,還有多少年,一直等待,那種感覺,或許被稱為不安,被我帶走了,從時間的起點帶走了。為什麼空間的障礙已經被科學打破,時間的鄉愁永遠難以撫慰?那麼誰來拯救我迷失在多年之前的靈魂?誰來尋回我年幼無知,落在公交車站旁的安心? 夢遊水鄉,我在橋邊等待一個人。是誰呢?來了才知道。槳聲像細雨,低聲講述江南的故事。多少年運河,多少年前煙波畫舫,文人墨客,一壺濁酒慰平生。多少年前傳來了多少年後還會傳唱的船歌。多少年前的多少年後在傳承多少年後的多少年前的回憶。時間本無經緯,記憶自繡羅衣。那麼等待也是。今天的等待,明天的等待。今天等待明天,那明天會不會等待今天? 你要等誰?水鄉夢斷,油紙傘下,蒼老的容顏,道不盡平生的悲歡,古今都作漁樵閒話,盡付笑談中。老者喟然長嘆,似要給你講述一生走南闖北的艱辛,那也是生活。你覺得你準備就緒了,你覺得你等待多年終於等到了人生的頓悟的時刻,那老者,只求你一壺酒一文錢,便把整個下午整個人生的經驗都告訴你了。轉頭一看,卻只聽得風聲雨聲,和思緒如同奔流著的不息江河,在低聲呢喃。 你還會等待,這卷青詞從千年前汴河燈影裡便開始書寫,在一千年後也不會完結。

2025/3/29
articleCard.readMore

記夢(DeepSeek 輔助創作)

憶起一場夢 太累了 寫得很爛 使用 DeepSeek 處理文章中的一些重複的段落 青石階蜿蜒向上,盡頭處懸著座褪色的廟宇。香火薰染的簷角垂著銅鈴,風起時聲響像隔世的嘆息。你隨人潮挪動腳步,鞋底碾碎階縫裡冒出的野蕨,一千零一個背影在你前方搖晃,恍若黃泉路上執傘的魂靈。 排隊的過程中,有人給你講起民間神話地方傳說,談天說地。從子夜飲沆瀣,至天明,山霧濡溼睫毛,長庚星與殘月相望,你忽然記不起為何在此。你好像失去了感知。你不關心周圍發生什麼。你只是等待。你不知道你在等待什麼,但是既然有等待的意義,那或許是有益的吧,這你倒是相信,你在等待什麼?你不知道?你還真不知道。那你為什麼要等?別問。你幻想到達之後的場景,腦海中醞釀一切計劃,那跨過褪色門檻的剎那,即將到來的意義。老僧傳授給你畢生的幸福,你參透人生之後一定要跨過遠方的山,回到家鄉,或者去旅行,或者去修行,或者?你首先要等待。 你等待。等待一千天,一萬年,等到你白髮蒼蒼。你立在廟門口。一座平常的寺廟,什麼也沒有。 推開褪色的木門,供桌上只有半截蠟燭。蒲團裂了口,露出裡面的草絮。你是不是白等了? 但你還是不能走進寺廟。守門的老僧面色凝重,好像幾百年前就站在這裡,不允許來訪者進入,而是設定層層關卡。老僧活像一尊石獅。你於是發問: 我什麼時候能進去? 先數到一百。 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百。 不夠虔誠 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 數錯了 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 還是錯了 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 依然錯了 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 數錯的太多了 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 不行。再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 還是不行。再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十。三十一。三十二。三十三。三十四。三十五。三十六。三十七。三十八。三十九。四十四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九一百 …… 你數了千遍萬遍,又是多少個日日夜夜,直到你用盡全部氣力,奄奄一息。 我什麼時候能進去? 現在還不行。繼續數。 你為什麼要進去為什麼難道說裡面真的有你想要的嗎你為什麼在這裡為什麼要等待為什麼要數數為什麼數錯為什麼不離開為什麼不放棄為什麼石階吃掉了你的童年卻吐出露水為什麼苔蘚在靜脈裡修建廟宇為什麼數到七十九時心臟會長出年輪為什麼烏鴉總在第九十九聲啼哭裡分娩銅鈴為什麼指甲縫滲出的血珠會自己滾成佛珠為什麼山霧把肋骨醃製成經幡為什麼白髮垂落時聽見蝴蝶振翅為什麼掌紋裂開的溝壑裡遊著初生的你為什麼不離開因為腳踝已生出根鬚為什麼不放棄因為每次呼吸都在石階刻下新的等高線為什麼野蕨從眼眶鑽出時帶著乳香為什麼銅鏽味的月光正在翻譯你腐爛的夢為什麼山鬼的傳說卡在喉骨為什麼等待本身成了你唯一會誦的經文為什麼廟門推開時湧出的是你三歲時的哭聲為什麼所有答案都在脊椎骨裡結冰為什麼你還在問為什麼心跳總在測量與虛妄的距離為什麼黎明前的黑暗有鐵鏽味為什麼記憶的根系總在腐爛處萌發新芽為什麼疼痛在骨縫裡豢養螢火為什麼沉默的舌苔上結滿未爆的驚雷為什麼時間的褶皺裡藏著所有未被選擇的自己為什麼候鳥的歸途比遺言更執著為什麼傷口癒合時會聽見星體坍縮的私語為什麼孤獨有海潮的鹹澀為什麼自由的邊界生長著荊棘的經緯線為什麼呼吸成為最昂貴的抵押物為什麼月光把往事醃製成琥珀為什麼希望是永不結痂的潰瘍為什麼死亡練習曲總在早餐時播放為什麼活著需要不斷吞嚥自己碎掉的倒影為什麼答案永遠比疑問早衰為什麼我們仍在所有破碎的映象裡打撈完整的指紋為什麼宇宙的胎動與子宮的回聲共振為什麼墓碑是站立的河床為什麼末班車總在淚腺決堤時到站為什麼鐘擺切割的光陰都帶著血絲為什麼最後的救贖藏在第一個傷口結出的珍珠裡為什麼墜落的花瓣在胃裡修建天文館為什麼遷徙的候鳥總在掌紋迷路時投下陰影為什麼結痂的傷口自動生成星圖密碼為什麼潰散的晨霧要偷走瞳孔最後的焦距為什麼發芽的骨縫裡傳出潮汐的舊唱片為什麼褪色的誓言在血管豢養髮光水母為什麼腐爛的果核仍在拓印子宮胎動為什麼凍結的時針以融水形態滲入骨髓為什麼爆裂的沉默把聲帶鍛造成青銅編鐘為什麼逃亡的月光在脊背鑿刻甲骨文為什麼解體的雲層選擇在舌根重新聚合成讖語為什麼鏽蝕的刀鋒偏要收割自己種下的光芒為什麼塌縮的夢境總在黎明前孵化成帶鱗片的疑問為什麼溺亡的鐘擺仍在切割記憶的臍帶為什麼所有動詞都在抵達終點前蛻變成墓碑的副詞 為什麼在這裡為什麼要等待為什麼要數數為什麼數錯為什麼不離開為什麼不放棄為什麼…… 你繼續數。三十二遍時指甲掐破了掌心,七十五遍時數混了雨點數,第九十九遍總被烏鴉叫打斷。有天摸到滿頭白髮,才驚覺數數聲已和心跳一樣重。 再來 一二三四五六七八九十十一十二十三十四十五十六十七十八十九二十二十一二十二二十三二十四二十五二十六二十七二十八二十九三十三十一三十二三十三三十四三十五三十六三十七三十八三十九四十四十一四十二四十三四十四四十五四十六四十七四十八四十九五十五十一五十二五十三五十四五十五五十六五十七五十八五十九六十六十一六十二六十三六十四六十五六十六六十七六十八六十九七十七十一七十二七十三七十四七十五七十六七十七七十八七十九八十八十一八十二八十三八十四八十五八十六八十七八十八八十九九十九十一九十二九十三九十四九十五九十六九十七九十八九十九。 這下數對了? 錯了,更錯了,這一切盡是荒誕。 但你早已經身處寺廟之內了。你早就知道答案了。你不知道嗎? 十九是晚自習教室的舊掛鐘,二十八是鑰匙斷在鎖孔的夜晚,三十七是母親喊你吃飯的聲音,五十七是月光在異地旅館的窗簾遊蕩,六十六是第一次失戀那晚的星星。 但這寺廟裡什麼都沒有啊,為什麼還相信?你到底追求的是什麼?是相信本身還是等待本身,都沒有意義。或者說等待本身就是意義? 我不懂。你說什麼? 數數本身就是人生,人生本身就是答案。這數數和等待都不是目的啊。那什麼是目的?沒有目的?還是全是目的? 四化作你七歲掉落的乳牙卡在門檻,十三變成母親分娩時的汗珠滲進木紋,六十八正以你脊椎鈣化的速率在瓦片上結霜。三十七粒星光在左肺葉發芽,五十九滴雨珠在右腎結晶,而那個永恆缺席的"一百",原來是你初生時剪斷的臍帶。 你說什麼?可你為什麼要數數?你為什麼?啊?你喝多了吧。 當山風突然調轉方向,你發現自己的白髮正逆向生長成青絲。石階上所有碾碎的野蕨突然開始倒放人生,第一千零一個信徒的背影裡,你看見自己正從廟門倒退著走回起點,而數數聲化作一串露珠,叮叮噹噹墜入你三歲時的奶瓶。 風突然捲走破廟,你站在岔路口。左邊有人繼續爬石階,右邊有人採野花。你踩了踩腳下新冒的蕨菜,轉身朝沒人的山坡走去。 露水打溼布鞋時,你想起七歲那年追過的紅蜻蜓——它此刻正停在前方某根草葉上,翅膀載著三十年光陰。 最後,夢破碎了,醒過來。你不需要數了,你來去自由。你去哪裡? 祝我十八歲生日快樂。

2025/3/13
articleCard.readMore

午夜飛行

你乘火車回到故鄉。車廂裡昏暗的燈光,顫抖著拼寫出別離的感傷。窗外是多少山川歲月。傾倒的電線杆,望不到盡頭的平原,水壩,大江東去,沿著車門一直伸展向世界的盡頭。你所知道的只有這窗外的世界。多少片雪花落下,你不知道。你只知道故鄉只存在於記憶深處,只在夢中重現。你只知道夢中有村莊,有微笑,有炊煙冉冉升起。都消散在風雨之中。 他在多媒體螢幕上點開下一部電影。數十小時的睏倦從指尖生長到全身上下的每一根神經。惶恐在太平洋上空盤旋。為什麼不承認錯誤?為什麼匆匆下結論?什麼也不懂。他想起午夜登山,山間的雲氣,霧裡望月,日出,寒冷潮溼。手腳麻木,如浸溼在風雨裡。他還是害怕。雲間的那束暗紅色的光,在飛機窗外跳躍閃爍,令人膽寒。 你在終點站前的車站下車。小販兜售童年,你不記得是2011或是2013,火車上販賣的廉價塑膠拼圖或是動車模型。世界新生伊始,江山如畫,在那時畫卷還未展開。你以為世界燦爛光明,你相信任何人,你充滿了美好的夢想。動車還未修建,公路還未開始規劃。盤山公路,數小時的行程,炸土豆,停頓歇息,繼續上路。 他回憶為什麼要遠行。制服書寫正義,正義竟然也讓勝利者來定義,一切都由他們來定義。起初,他不相信。他在懸崖峭壁上如履平地,他不相信那些噩夢和神話。他甘願相信那些美夢和空頭支票,他默默承受,他學著眾人的模樣,在淺灘上奔跑。但他不知道生活是為了什麼,他沒有印象,只是希望向前奔跑。他認為自己有無限廣闊的天地。 你追隨祖先走向大海。那艘鉅艦未能帶走你那遠代的先輩,信仰竟成為自己的棺木。路的盡頭是一棵大樹,你不知道為何總是對之生出莫名的敬畏。你其實從未到過這裡,你從未和大山有過關聯。這大山不屬於你,你這外來者。但這是你的先祖曾紮根,曾生長的地方,曾經結束自己的一生,又曾堅守,曾被背叛。你沒有記憶,歷史的意義就是選擇遺忘,或者,被迫忘記。 他不禁戰慄,但機艙裡並不寒冷。細想從來,他什麼也沒做錯,卻要受懲罰。他不知道這正義是否是正義,他只相信自己的權利,他只相信那與生俱來的權利。無理的規則多著呢,他不理解,也沒有人解釋。或許從來都不會有人質疑。但他嚇都嚇死了。刪除一切記錄,過去,好像從未發生。他什麼都沒做錯啊。他不相信,他不理解,但很多事情都無法理解。是誰錯了?他什麼都沒做錯啊。 你說你如履薄冰,總是小心謹慎。你總是罵他不小心,但他確實沒有犯錯誤。但你也開始恍惚,你不知道現在是什麼年代。揹負著五千年重擔,只能忍痛前行,捍衛長城,或者固步自封,自鳴得意,或者這二者是在說同一件事情。你不繼續說下去,讓聽者意會,你選擇沉默。 他希望變成飛鳥,不受束縛,繞過太陽,飛向太空。他希望變成大海,衝破殘陽,回到淺灘。他不懂什麼叫愛情,他沒有經驗,他只能靠幻想。愛情比太陽更高尚,因為真實,因為自由。他回到水鄉,在烏篷船裡做那些好夢。船隨著江濤規律搖晃,或許正好押韻於江南的詩行。但多少韻律都說不出他的悵然若失,他把一切夢幻寄託於那好夢,最終都成泡影。今宵酒醒何處,楊柳岸,曉風殘月。他不懂,他對月抒懷,他抒不了懷。 你永遠都回不到過去。過去的決定就是既成的事實,你只能接受,但你有時接受不了。接受不了也只能接受,有的是方法讓你接受,溫水煮青蛙,直到你低下頭,教會你跪下來說話。你不信邪,你非要挑戰,你非要試著唱反調。你自認為高貴,自認為獨立於世間。你會發現你錯了,你會發現你窒息在空氣中。 天明前啟程,城外多少朝代。他很崇拜那位詩人,像那位哲人一樣,簡直就是聖人。但他不相信權威,不相信個人的能力能夠絕對凌駕於科學之上。一代一代的積壓,民族的形狀定型。他不敢相信,他不甘心,他非要試試。他太天真。在午夜,他還是會回想起那天,他已經下定決心。 你回不去了,你只能逆風向前。你被欺騙,你自我欺騙,也被別人欺騙。你好像在精神上被強姦了,被迫做自己不願意相信的事情,但你不得不做,你不得不背上沉重的行囊,隨著眾人一起朝一個方向行走。你只能接受。他什麼都沒做錯啊。 你發現他是貪生怕死之輩,只願意維持現狀。現狀是一潭死水,他竟然只想苟且偷生,完全聽憑別人擺佈。你呼喚他前行,午夜飛行,你不知道飛向哪裡。他什麼都沒做錯啊。你只能追隨祖先,祖先為你留下直覺和歷史,你只能選擇追隨感覺。 他在午夜渡過這條河流,彼岸是什麼,他不知道。冰冷的河水如藤蔓纏住他的小腿,刺痛他的神經,讓他不敢再往前。他必須渡河,而不是停下來。他沒有回頭路,他不願意再接受。他想起天明時看過的日出,驕陽似火,真理和真理之間只能有一個真理。真的是這樣嗎?或許不用毀滅,或許有更好的辦法。金閣寺不用毀滅。他是對的嗎? 你不再做夢,夢中只有惶恐。你放棄了信仰,你不再相信,你不再接受,你站起來,除去身上的灰塵。你要去哪裡啊?你還相信嗎? 你吶喊。你做了一個夢,夢裡你吶喊。你找不到家了,你好像沒有家。你在沙漠公路上獨自前行,這條路沒有盡頭。總算沒有人追趕你了,但你要去哪裡呢?你往前走和往後走似乎是同一個方向。你要去哪裡呢? 下了一夜的雪。他看不到故鄉,也看不到目的地。午夜飛行,他還未落地。下了一夜的雪嗎? 淺灘上,一輪明月,美好得讓人願意獻出生命。但這些都不值得拯救,地面上的人不值得拯救。他放棄了使命,不空談夢想,他先拯救自己。人生如夢。他第一次承認自己畏懼死亡。 你不相信夢想,你不相信陳見,你不相信權力和暴力,你不相信太陽,你不相信神話,你不相信別人,你不相信規則,你不相信故鄉,你不相信過去,你不相信飛行,你不相信雲,你不相信歷史,你不相信直覺你不相信冰塊你不相信大海你不相信微塵你不相信革命你不相信政治你不相信謊言你不相信幻覺不相信收音機不相信信標不相信城牆不相信詩人不相信江山不相信愛情不相信生活不相信文化不相信天氣。你什麼都不相信。你還相信什麼?你還相信相信嗎? 他沒必要相信什麼。他也不相信你,你只是午夜飛行的幻覺,只存在於過去,只屬於天空。

2025/2/22
articleCard.readMore

橋樑

紀念一段往事 橋樑 / 小滿 2024.2.1 於北京 白塔之上 寒鴉拼湊夜空 首都地鐵四號線 匆匆駛向終點 是否通往明天的美夢 橋樑之下 雪與風相伴行走 惶恐化身暮靄 使落木飄零,放逐 來自幻想的春天 冰和天空銜接 聯結了曉風殘月與 昨夜的決絕 深秋的紅葉終究飄向 冬天 記憶氾濫,凝固 月光沒能到達目的地 而是灼燒 盈虧之間的傷痕 橋樑糾結,坍塌 海風將它托起 帶回撿拾泡沫的沙灘 那日 踟躕的光線 把夕陽和頭髮 攪成了一團 我總企盼有人重建橋樑 寫給恍惚中度過的寒冷的秋冬

2025/1/7
articleCard.readMore

黎明 或 2012

舊詩一首 淺灘上 一群猴子說教 太陽未生的角落 玻璃杯破碎 火球燃燒 門外守著 枯萎的向日葵 熟稔的幽谷 光在其中窒息 然後 我嗅到了炊煙 黎明未造訪的人間 雞鳴、柴香中的生活 人在其中重生 停電的山村迴歸原始 黑夜吞噬 一盆熱汽和滾燙的水 卻是思鄉的罪因 你說:一切往事都在夢中 幻影中 我聽見—— 摩托和戒尺 喧譁的收音機 黎明在流血 傷痕滾燙 淌著東方的雲 可我卻期待黎明殞命: 獻祭給了今天 才有明天 小滿 2024/1/23 夜

2025/1/7
articleCard.readMore

RECAP2024: 水檻臥聽雨

獻給 2024,和夢中的水鄉。 2024年的盡頭,希望寫下這些文字,腦海中卻是虛空,難以回想起2024年發生的一切。這一年,太多變化,太多茫然,太多草率。選擇總是覺得被動,在一個個被動的選擇中拼湊出的2024。又想起上半年水鄉中做的一場幽夢,階前點滴的細雨,一直飄到2025的黎明。 那是夢中的水鄉,細雨中,趕向最後一班烏篷。水檻中平臥聽雨,憑欄遠眺千年前流淌而至的京杭運河,卻難以遣心。夢來的太恍惚,恍惚到2024年就如斜陽中彷徨的曲巷,猛然驚醒,才發覺這似夢非夢的都是真實的昨日。 不願在水鄉的幻想中迷失。一場雨下了三百六十五個日夜。向前,左轉,再左轉,右轉,左轉……小巷石磚,經受過多少年雨水。來不及撐起油紙素傘,也等不得細細品味這水鄉的韻味。夏天的雨退行,行人退行,青瓦退行,三兩枝竹退行。古琴洞簫在沉默中遁形,都市在水鄉迷宮的出口等待。末班車的尾燈在水霧中變得模糊,似一團硃紅的胭脂,從水鄉戲臺潑進城市的生活…… 2012 黎明在流血,傷痕裡淌出東方的雲。玻璃杯破碎,火球滾燙,淺灘上一群猴子無力地說教。這是世界末日前太陽為生的角落。 為什麼最後的蒙特祖瑪臣服於新西班牙,活人的祭祀仍在持續?為什麼古代文明衰落,瑪雅人的預言流傳至今?黎明殞命,把昨日獻祭,迎接新的光明。或有預感,2012就是2024。舊世界的終結會是新世界的開始。 太陽已將生命留給2023的白晝。你是將遭審判的囚徒,決絕將赴星月的刑場。2023年底,我化身為星光或者泡沫,去追隨你的腳步。我祈禱直覺帶領我尋覓你、追隨你。 於是我的生命化為大海,在海風中,來到這2024。 寒鴉和橋樑 白塔之上,寒鴉拼湊夜空;首都地鐵四號線,匆匆駛向終點。落葉被放逐歸根。月光沒能到達目的地,而是灼燒,盈虧之間的傷痕。那日,踟躕的光線,把夕陽和頭髮,攪成了一團。 在北京,我仍然相信2023的直覺。圓明園的湖水凍成晶瑩的鏡面。我曾相信2024年也將如冰面一樣澄澈光潔 我不能懂得十萬里長城的無奈,用不變丈量未來,繪製出新一年的藍圖。終是一汪死水。 浮沉。北京的寒鴉帶我回到那個冬季,漫步在中關村,在西單,在王府井。我不知道我的生命中是否還有北方,但那個冬天我願意相信未來。 在天壇,祈年殿,雪花一片一片在天上流浪,飄舞似無所依附。我曾寄希望於雪花,曾認為找尋到了那一片雪花。它曾穿越風雪,休憩在我的窗前。而我曾用雙手輕輕捧起,卻發現雪花溶入了冬天的夢…… 水鄉溫柔 水鄉溫柔,何處是我家? 在錯覺和現實交織的世界,繁花染遍大地,像一把火燃盡了整個春天。陽光燦爛,水鄉並未給我選擇,而是將我領向一片未知之地。我把這位置視為家,似乎找到了征途的歇腳處,找到了海邊的永不褪去的泡沫。 在烏鎮,迷失在水鄉迷宮之中;在紹興,凌晨踏上泥濘的山路,目睹黎明的再一次復甦。徹夜未眠。初夏的夜比現實更寒冷,山路崎嶇比生命更曲折。只能徹底脫離對外界的依賴,成為極夜中的微光,引導著自己前行。 黑暗中舉起手電,盤山公路上高歌。山上其實什麼也沒有啊!而我憑何找到慰藉,憑何找到光明?再次踏上蛻變之路,悟以往之不諫。我錯了。我質疑過存在的存在,懷疑既成事實的真實,並尋求顛覆不存在的鐐銬,走向歧路。 人生不是刻舟求劍,而且動態的、運動的。向前行,草木欣欣向榮,新的山巔上放聲高歌,成為太陽,照亮長夜。不再相信,卻更加相信。 午夜飛行 《新世界交響曲》慷慨激昂,卻也流露出不穩定。午夜飛行,飛向紐約、亞特蘭大和西雅圖。新大陸從天際線上升起,為何舊大陸便消失得無影無蹤。 在紐約,時代廣場和布魯克林橋,哥倫比亞大學校門外車水馬龍,90號州際公路上凌晨的寒風與自然的呼吸。浦東機場,離別有了新的形狀。 鄉愁從此有了新的定義。望向從未登陸的領土,思念從未到過的故鄉。宿命或者偶然,我又將踏上征途。此刻我卻比任何時刻都熱愛自己的土地,炎黃子孫的土地。 不斷成長,不斷蛻變。在香港漫步,棉絮一樣的霧掩蓋中環的摩天大樓,去遠郊的山上徒步旅行,石澳村的海浪與泡沫點亮盛夏;在澳門,朝陽和金沙呈現同一顏色。上海圖書館,夕陽闖入二樓大廳,講述盛夏的另一個故事——一場大雨。 晚風和暮雲,午夜飛行。 後記 末班車怕是趕不上了。2024年的後半年,在矛盾和困惑中度過,卻也大步向前走去。一個決定,一個決定未來10年的方向的決定,一個我未敢做出的決定。帷幕拉開,終究要面對,而所有的延遲都是徒勞。未來走向何處?我不知道,保留了決定的權利,而任自己在磨礪中不斷蛻變。 2025年,不需要任何人的指引,擺脫真正的束縛,自由生活。不去做那些美夢,放下所有矛盾,一切都隨水鄉的煙雨飄散去了。 仍然需要幻想,但既已理清了這一些迷霧,也應堅定向前走去。昨天與明天一樣飄渺,倒不如看著腳下的路,這近在眼前的真實。 不再隨風、隨雨、隨大海,而是跟隨自己,獨立於世間,去追尋,去探索。 向山行,向山行,翻山越嶺,沒有終點,沒有答案。 水檻臥聽雨已成為往昔。2025,我將星夜兼程,片刻不息。

2024/12/31
articleCard.readMore

太陽、潮落

太陽 請給冬日動人的悲憫 你是空有光明的夜燈 無力帶給寒天一絲暖意 太陽 請把生命留在白晝 你是將遭審判的囚徒 決絕將赴星月的刑場 太陽,偉大的事業未竟 你曾搏擊北風,怒斥冬的不公 你曾笑對雪花妖冶的輕蔑 你是荒嶺上的黃花 註定你的努力不合時宜 我在太陽中看見了我的生命 吶喊,口旁升騰的霧氣 是傳書於你的白鴿 聆聽我的呼喚,太陽 我心意已決 請與我在海邊坐待潮落

2024/1/1
articleCard.readMore

RECAP2023: 泡沫

浪宣告朗,日光下, 我試著抓住大海 —— 它 卻後退,化身成點點泡沫。 2023.8.10 《泡沫》 從夜夢中驚醒,已是黃昏。昨夜暖風似酒,酒醒時暮靄酡紅。踏上駛向未知的火車,身後,殘陽血濺寒天。瞭望窗外,時間向後劃去似溢彩流光,在這光影之中,我看見了我的即將湮滅在過往的深夜裡的2023。 2023,用一整年時間,獨立海邊,用一顆石子擊打浪花,用一雙手企圖留住海潮。海風是一位吟遊詩人,奔走四方,帶來大海另一邊的不同時空的故事——時空錯亂,吟唱中我心漸靜,雙手空空。竭盡全力,撲向大海,攥住浪花——斜陽下,徒然攥住一手細沙,日光卻告訴我這是黃金。海波泛起銀光,似星海盪漾、轉動,卻從不能裝進口袋,帶進我的生活。 可是我仍矢志不渝,在海邊,十二個月,追隨海潮啊。 盛夏在青島的碧海青天下留下自己的足跡,我的一年便停留在了那個下午的細雨之中。青島車站,至今仍然保留著殖民地時期的風貌。磚紅色的屋頂,靜立在月色海風中,沉醉在浪濤的朦朧樂聲中——那是不遠處棧橋邊的大海。意猶未盡,在清晨的細雨空濛中,嗅著雨水中海草的腥味,那時陷入囹圄。舊的秩序已然崩塌。不願面對的人群,永遠說不出的心中的話語堆積,內心深處精神的孤獨、缺氧,漸漸迷失在這片人生森林的迷霧中。 海風已熟習我一年來的故事,時間線錯亂的故事經她們之口平靜訴說。 在朝陽升起的地方,睜開雙眼,柔焦,模糊的光影。我不再遺憾去年的流星雨,在新的一年開始的時候,一頭撞向束縛我的桎梏——內心的無形的冰塊。解凍,冰流潺湲。用身體中從夜中吸收的力量,向外邁步。 高樓之上,我受盡了曾以為伴的虛偽的恆星的炙烤。我手握劍柄,斬下那帷幕,所謂的光和熱不再為我所聞。我囿於此,引來他的夥伴,試圖滲透進我的身體,闖入我的陋舍——那閃爍的光。我曾以為那是我向往的泡沫的光影。 借五月假期之餘,我回到楊浦,再會舊友。還望來時的路,始終留戀,四平路黃昏時的街燈,可是我不能再走在那路上了,可是我不能再去同濟書店,看由粉轉紫的彩霞,幻想地鐵站中的驚喜奇遇。我看真切了,似夢似幻的過往,亦似往日居家的日子裡從飄渺中飛來的隱約笛聲。 在初秋,分道揚鑣,而我也將走在我的道路上。分別的必然中,我回憶起相遇的偶然。去年在秋季的機房中相遇,如今聽見那時的耳機裡今日還在流淌的歌聲,仍不時憶起你。想起春日裡徘徊在機械零件中,春寒料峭,在車間中工作到深夜的日子。最終踟躕於做出那個出國參賽的選擇。逃過的數節課,工地辦公室裡的外賣,朝夕相處的日子。團委活動室棋盤上的故事,被偷走的象棋,“黑貓、白貓”的秘密。以至於後面的一個月,以及後面的半年,迷茫和野蠻生長。複雜的外面的世界,我不會忘記過往的緊鎖心扉。我必須選擇,於豔陽墜入大海之時,躍入海水之中。我的氣息化成海風,我把生命獻祭給大海,我的生命是大海,我得到了重生。 在天津,在北方,夕陽破碎,灑在林間枯枝之上,我第一次享受到恆星衰亡後的溫熱。這溫暖由我自己創造。我成了我自己的太陽,這功勞也不在別人。答辯之時,找回一年前許下的從容。我將消滅一切阻礙前行的聲音,我將消失在煙雨中,成為雲海中的霧氣,飄忽不定。北方沒有煙雨,只有枯枝、敗葉、寒風。 我用時間尋找鬆弛。有幸在滿隴桂雨之中遇見了別樣的杭州,在蘇州品那一碗熱霧騰騰湯麵,在前灘偶遇晚霞的裂紋。蘇子愀然對月,卻在惠州再會了蘇堤——玩月,豐、平二湖。春曉,此心安處是吾鄉?自我在四方遊歷中重新獲得定義。 那些有規律的自然的律動,是海潮給我的諄諄教誨。海潮漲落,我的心趨於平靜。靜謐的清晨,眺望大海及遠方。海是印象派的交響樂,海是詩人未寫完的詩行……細雨初歇。在中山路遇見一個少女,淺黃色漁夫帽下輕煙蔽月般看不真切的面容,緩步邁向咖啡店。她停下,把手機架在對面的牆上,小步跑向店門,在咖啡店門口的小桌上坐下,捧上一束花,調整儀態,微微一笑。她是在給自己拍照。一個人的旅行。雨後的陽光灑在咖啡館的正門邊,女孩剛才拍照的位置,玻璃檯面反射出久違的光。而人生會是一場漫長的旅行…… 在四月的隔離教室中,追隨光的腳步,唱起四月的歌聲。正午的陽光,擾亂我的思緒。驚異於一年來學業長進,這卻不是我出發時尋求的海浪。南京的長江邊,朝陽下水鳥驚飛,淺紫色天空下的中山碼頭,我們一行人在江邊打水漂。在夫子廟嘗過一鍋熱氣騰騰的粉絲,回憶中我再次來到那些重新認識自己的日子。江州的另一畔,巨輪駛來——仍遺憾從未到訪過長江大橋和橋上攜帶夏天的火車……返校之後,走廊中的光影,我恍然得知慘痛的事實。 12月的上海,遊園會上的重逢和小攤上操勞的資訊組同學。一年前的相遇,如今我也成為“學長”,卻仍然是第一次嘗試。前路漫漫,缺乏經驗指路。忘不了遊園會的人群中,科目三的表演。鍋裡的熱氣騰騰不能打動固執的土豆,在必然的失敗中,我們獲得了部分勝利。文藝匯演,接二連三的彩排,嘗試找到一個平衡點,度過漸漸熟悉的生活。舞臺聚光燈下,演出圓滿,觀眾席中飛來的熒光棒砸向謝幕演出的我們,不再寒冷。 ​學農和自己製作的魚米飯、夜晚宿舍中的休整和抽象、與技術老師閒談中結束的文藝匯演…離隊時的稻田,一隊人手持甘蔗,結隊行阡陌,夕陽無限。寒空下地平線被點亮。暮色中,農戶的秸稈燃燒,輕煙冉冉升起。黃昏已至,我是否應該回到家鄉的土屋,享受那真切屬於我的炊煙?半透明的殘陽被煙遮蔽,彷徨,自由。 海風在呢喃。請告訴我2023的答案。一事無成?一路意外收穫。 那我用何指引我的生活?前路比過往悠長,面向即將拉開的人生帷幕,缺乏力量和勇氣。 2023將盡,海風仍不時撫過我的面頰,眼瞼微潤。海潮依舊漲落。下海,最後一次試圖追上浪潮,試圖一把抓起她,質問她,請教她。海潮拒絕我天真的問詢,拒絕給予任何指引。我乞求的答案竟是泡沫。 泡沫,泡沫,2024請離開海岸,縱身跳入大海,化身大海,大海是答案,我的生命將是大海。 泡沫的銀光中,悵然若失的一年溜走了。 我卻會把泡沫視為2023最好的饋贈。 2024,相信直覺,相信自己。 Power Up. 還會繼續憶起2023年海風的無形教誨

2023/12/29
articleCard.readMore

題解 P1622 釋放囚犯

題目連結 題面 題目描述 Caima 王國中有一個奇怪的監獄,這個監獄一共有 PPP 個牢房,這些牢房一字排開,第 iii 個緊挨著第 i+1i+1i+1 個(最後一個除外)。現在正好牢房是滿的。 上級下發了一個釋放名單,要求每天釋放名單上的一個人。這可把看守們嚇得不輕,因為看守們知道,現在牢房中的 PPP 個人,可以相互之間傳話。如果某個人離開了,那麼原來和這個人能說上話的人,都會很氣憤,導致他們那天會一直大吼大叫,搞得看守很頭疼。如果給這些要發火的人吃上肉,他們就會安靜點。 輸入格式 第一行兩個整數 PPP 和 QQQ,QQQ 表示釋放名單上的人數; 第二行 QQQ 個整數,表示要釋放哪些人,保證按遞增的順序給出。 輸出格式 僅一行,表示最少要給多少人次送肉吃。 樣例 #1 樣例輸入 #1 1 2 20 3 3 6 14 樣例輸出 #1 1 35 提示 樣例說明 #1 先釋放 141414 號監獄中的罪犯,要給 111 到 131313 號監獄和 151515 到 202020 號監獄中的 191919 人送肉吃;再釋放 666 號監獄中的罪犯,要給 111 到 555 號監獄和 777 到 131313 號監獄中的 121212 人送肉吃;最後釋放 333 號監獄中的罪犯,要給 111 到 222 號監獄和 444 到 555 號監獄中的 444 人送肉吃。 資料規模與約定 對於 50%50\%50% 的資料,1≤P≤1001 \le P \le 1001≤P≤100,1≤Q≤51 \le Q \le 51≤Q≤5; 對於 100%100\%100% 的資料,1≤P≤1031 \le P \le 10^31≤P≤103,1≤Q≤1001 \le Q \le 1001≤Q≤100,Q≤PQ \le PQ≤P,保證釋放的人所在的牢房編號按遞增的順序給出。 思路 題意即求釋放列表中所有罪犯的費用之和最小,每次釋放,罪犯i到他兩邊空牢房之間的所有人,都需要消耗一塊肉。每次釋放一個罪犯,可以想到區間 DP,狀態 f[l][r] 表示從l到r號罪犯全部釋放所需要消耗的肉塊總數。 之後思考狀態轉移方程。要求最小值,f[l][r] 初始值 INT_MAX。f[l][r] = min of f[l][m-1] + f[m+1][r] + Pos[r+1] - Pos[l-1] -2 ,其中 m 屬於區間 [l,r],列舉合併點,即此次釋放的罪犯,罪犯兩側到l和r號罪犯的消耗f[l][m-1]和f[m+1][r]。Pos[i]定義i號罪犯的編號。這裡其實是一種反向的思路,m點應該是最先釋放的(?Pos[r+1] - Pos[l-1] -2即此區間兩側最近的罪犯之間的人數,減去區間兩端。即區間合併的額外費用。注意要在Pos陣列中定義Pos[Q+1]=N+1。 程式碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <cstdio> #include <cstring> #include <iostream> #include <climits> // For INT_MAX using namespace std; int Pos[110]; int f[110][110]; int main() { int P, Q; cin >> P >> Q; for(int i = 1; i<=Q; i++){ cin >> Pos[i]; } Pos[Q+1] = P+1; for(int k = 1; k<=Q; k++){ for(int i = 1; i+k-1<=Q; i++){ int y = i+k-1; f[i][y]=INT_MAX; // 初始值 for(int m = i; m<=y; m++){ f[i][y] = min(f[i][m-1] + f[m+1][y], f[i][y]); } f[i][y] += Pos[y+1] - Pos[i-1] -2; } } cout << f[1][Q] << endl; // 答案即把 1...Q 所有的罪犯全部釋放(合併) return 0; } 小結 本題難點在於建模,區間DP部分比較典型。

2023/10/18
articleCard.readMore

題解 P5888 傳球遊戲

題目連結:P5888 題面 題目背景 羊城有善蹴鞠者。會足協之杯,於校園之東北角,施兩球場,蹴鞠者站球場中,nnn 人,一球,二門,三裁判而已。觀眾團坐。少傾,但聞球場中哨聲一響,滿坐寂然,無敢譁者。 當是時,傳球聲,微微風聲,隊員疾跑聲,教練呼喊聲,拉拉隊助威聲,一時齊發,眾妙畢備。滿場觀眾無不伸頸,側目,微笑,默嘆,以為妙絕。 未幾,我球員施一長傳,彼球員截之,望我龍門衝來。 但見守門員 oql 立於門,若有所思—— 題目描述 原來他在想這麼一個問題: 場上的 nnn 個球員圍成一圈,編號從 111 到 nnn ,剛開始球在 111 號球員手中。一共 mmm 次傳球,每次傳球必須傳給一個人,但不能傳到自己手中。求第 mmm 次傳球以後傳回 111 號球員的方案數。 但他覺得這個問題太簡單了,於是加了 kkk 條限制,每條限制形如 a,ba,ba,b,表示 aaa 號球員不能將球傳給 bbb 號球員。 為了使得 oql 的注意力轉移回球場上,你需要在最短的時間內告訴他這個方案數是多少。 你只需要告訴他答案對 998244353998244353998244353 取模後的結果。 輸入格式 輸入資料包括 k+1k+1k+1 行: 第一行三個整數 n,m,kn,m,kn,m,k,分表代表球員數,傳球次數,限制條數。 接下來 kkk 行,每行兩個整數 ai,bia_i,b_iai​,bi​,表示 aia_iai​ 號球員不能將球傳給 bib_ibi​ 號球員。 資料保證不會出現不同的 i,ji,ji,j 使得 ai=aja_i=a_jai​=aj​ 且 bi=bjb_i=b_jbi​=bj​。 輸出格式 輸出一個整數,表示 mmm 輪後傳回 111 號球員的合法方案數對 998244353998244353998244353 取模後的結果。 樣例 #1 樣例輸入 #1 1 2 1 0 樣例輸出 #1 1 0 樣例 #2 樣例輸入 #2 1 3 3 0 樣例輸出 #2 1 2 樣例 #3 樣例輸入 #3 1 2 3 4 5 6 7 13 5 1 3 4 5 5 4 6 1 2 2 樣例輸出 #3 1 443723615 提示 對於 10%10\%10% 的資料,k=0k=0k=0。 對於另外 15%15\%15% 的資料,n≤500n\leq 500n≤500。 對於另外 20%20\%20% 的資料,n≤5×104n\leq 5\times 10^4n≤5×104。 對於另外 20%20\%20% 的資料,k≤300k\leq 300k≤300。 對於 100%100\%100% 的資料,1≤n≤1091\leq n\leq 10^91≤n≤109,0≤m≤2000\leq m\leq 2000≤m≤200,0≤k≤min⁡(n×(n−1),5×104)0\leq k \leq \min(n\times(n-1),5\times 10^4)0≤k≤min(n×(n−1),5×104),1≤ai,bi≤n1\leq a_i,b_i\leq n1≤ai​,bi​≤n,不保證 ai,bia_i,b_iai​,bi​ 不相等。 思路 首先看到題目,如果沒有k限制,顯然是DP。 設計狀態 f[i][j] 表示第i局遊戲傳到j的方法數,顯然滿足 DP 的要求(無後效性、最優子結構等)。 得到狀態轉移方程:f[i][j] = sum of f[i-1][k]。其中k表示能夠傳到j的所有人。 但每次遍歷k還是太麻煩,題目中顯然能夠傳到j的人數大於受限人數。所以我們採用做差的方法,使用f[i][j] = sum of f[i-1][k] - f[i-1][m],此時k表示所有人,m表示不能傳球的人。所有人的方法數和可以使用一個變數記錄。 但是我們發現,題目中n的範圍到1e9,顯然無法開這麼大的陣列去算。 思考發現,題中限制數很少,只有5e4,最壞情況(限制中,a、b均不同),存在約束的人數也只有1e5,剩下的是毫無限制的“自由人”,因此可以簡化問題,成為“自由人”、自己(即1)和“限制人”的傳球遊戲。 “自由人”可以“自傳球”,需要注意。而且這樣做之後,編號會混亂,我們需要建立一個map重新建立編號。 還有什麼最佳化方法? 滾動陣列,可以將f壓縮! 程式碼實現 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> using namespace std; #define MOD 998244353 struct Node{ int next, to; }NDS[100010]; int cnt = 0; int head[100010]; void add(int a, int b){ // 表示 a 不能收到 b 的球 NDS[cnt].to = b; NDS[cnt].next = head[a]; head[a] = cnt++; } vector<int> P; int DP[2][100010]; map<int, int> Dict; int main() { memset(head,-1,sizeof(head)); int n,m,k; cin >> n >> m >> k; for(int i = 1; i<=k; i++){ int a,b; cin >> a >> b; // remap int ida, idb; if(!(ida = Dict[a])) P.push_back(Dict[a] = ida = Dict.size()); if(!(idb = Dict[b])) P.push_back(Dict[b] = idb = Dict.size()); if(ida != idb) add(idb,ida); } // Sp: 1 -> 可能不是限制人,但是也需要單獨更新狀態! if (!Dict[1]) P.push_back(Dict[1] = Dict.size()); int Nc = Dict.size(); int Fc = n - Nc, idf = Dict.size()+1; // 記錄此次和上一次的 Sum of f int sum = 1, sum2 = 0; DP[0][Dict[1]] = 1; // 初值 -> 只能從 1 開始傳球 for(int i = 1; i<=m; i++){ int cur = i&1; // 滾動陣列 int prev = 1-cur; sum2 = sum; sum = 0; for(int j : P){ DP[cur][j] = ((sum2 - DP[prev][j])%MOD + MOD)%MOD; for(int x = head[j]; x!=-1; x=NDS[x].next){ // 排除不能傳球的人 int y = NDS[x].to; if(y==j) continue; DP[cur][j] = ((DP[cur][j] - DP[prev][y])%MOD + MOD) % MOD; } sum += DP[cur][j]; // 更新 sum sum %= MOD; } DP[cur][idf] = (1LL * sum2 * Fc % MOD - 1LL * DP[prev][idf] % MOD)%MOD; // 自傳球 sum = (sum % MOD + DP[cur][idf] % MOD) % MOD; } cout << DP[m&1][Dict[1]]%MOD << endl; // 輸出結果 return 0; } 更多注意事項 這道題我在取模的時候調了很久 -> “C++負數取模還是負數,應該將其變成正數” 小結 本題是一個動態規劃問題,難點在於模型的建立。

2023/10/17
articleCard.readMore

殘陽似火

“我的生命將是大海。” 青島車站,至今仍然保留著殖民地時期的風貌。磚紅色的屋頂,靜立在月色海風中,沉醉在浪濤的朦朧樂聲中——那是不遠處棧橋邊的大海。候車大廳靜悄悄,耳畔不時傳來廣播的聲響,也似輕煙一般飄渺,混雜著些泡麵佐料的濃香,浮向大廳的遠處。 這便是作別的日子,而這樣的日子卻總是意猶未盡。手指摩擦、轉動手中返程的車票,不時看向手錶上的時間——啟程還有一個半小時。總覺得少了點什麼,於是跑向車站內的小店,又空手而歸。大海,是大海,作別大海,託付給她的不安被打包返還。 那天清晨的細雨空濛,風卻險些從我手中奪過那把隨我走遍江南的雨傘。在濱海棧道上行走,新鮮的雨水混著海草的腥味,這是大海的饋贈,喚醒還未完全甦醒的我的靈魂。道旁的樹用綠葉為這場夏日的開場儀式奏樂,窸窣窣,窸窣窣;海岸線上巖黃色的礁石屹立在大海之濱,沉思。近來可好?我的生活正在把我拖入泥濘,這裡危機四伏,舊的秩序已然崩塌。不願面對的人群,永遠說不出的心中的話語堆積,內心深處精神的孤獨、缺氧,漸漸迷失在這片人生森林的迷霧中。這便是那些為我定製的精神危機,我必須時刻鬥爭。礁石孤立無援,守在海岸的前線,聽憑浪濤處置。 但浪濤仍是正義的。擊打在這些巨石上,唰的巨響,濺起數米的水花,仍是治癒人心的。那些有規律的自然的律動,是海潮給我的諄諄教誨。但這些無形的空洞的響聲,又有什麼資格給予我未曾得到的生命力量?海潮漲落,我的心趨於平靜。靜謐的清晨,眺望大海及遠方。海是印象派的交響樂,海是詩人未寫完的詩行……細雨初歇。 向中山路走去,已經是正午。保留殖民地時期風貌的小街——精緻的商品店、咖啡廳擠在單行道的兩側。牆邊掛滿了爬山虎,店門口的風鈴和老式腳踏車,在微風中明朗清脆,便聞到一陣咖啡的幽香,嘴邊感知到驅走今晨微寒的溫熱。在這裡小憩片刻吧。遠處走來一個少女,淺黃色漁夫帽下輕煙蔽月般看不真切的面容,緩步邁向咖啡店。便似飄散開奶香拿鐵的甘甜,飲甘醉如燻,陶醉在細雨中微酡。她停下,把手機架在對面的牆上,小步跑向店門,在咖啡店門口的小桌上坐下,捧上一束花,找到更美的姿勢,微微一笑。她是在給自己拍照。一個人的旅行也能如此!雨後的陽光灑在咖啡館的正門邊,女孩剛才拍照的位置,玻璃檯面反射出久違的光。而人生會是一場漫長的旅行…… 時候不早,於是離開這裡,想在返程前再看一次大海。海風仍撫過我的面頰,眼瞼微潤。海潮依舊漲落。下海,我試圖追上浪潮,試圖一把抓起她,質問她,請教她——我的生命需要方向。撲向大海,跌跟頭,再爬起來,一把攥著漲來的海浪,手心冰涼,卻發現是一捧金沙。海潮拒絕我熱情的問詢,拒絕救濟迷失的靈魂。我乞求的答案竟是泡沫。 阿圖姆甦醒,金紅色的殘陽,決然墜入深海。他果斷選擇了毀滅,選擇了鳳凰涅槃。殘陽如火,大海也在海浪山崩地裂的哭喊中被夕陽的鮮血染成橘紅色。隕落,金閣寺的火焰還未熄滅,碧海澄空在生命的火焰中熊熊燃燒。我想把我的生命此刻獻祭給大海,寄希望於大海,期冀在運動中重塑生命。星海燦爛在夏日的毀滅中醞釀,“夜空一無所有,為何給我安慰?”海洋的呼吸滯礙我生命的前行。 生命本就是空虛的,一切也都是徒勞。夕陽在跟大海殊死拼搏中墜亡,何嘗不是一種價值的創造?或許陽光會在另一面出現——那是遠方的新世界。收拾行囊,準備第二天的征途,風的呢喃中,忘卻生命之流中阻力與痛苦…… 車站裡寂靜無人,旅途已經完結,但人生的真正旅程甚至不能說已經開始。面對完全的未知和任由天命擺佈的無奈,路途仍然由我們規劃。 踏上列車,窗外夜空澄淨無瑕,點點星光,這是星海的水波——我把海潮帶走了,帶向世界的每個角落,帶向天涯海角,帶向每一個夜晚和清晨。 海潮汩汩不停歇……

2023/10/13
articleCard.readMore

再會

懶得寫簡介,你們自己點進去看 doge 初秋的幽風再不能喚醒, 心中將乾涸的冰流, 直到那西湖——野徑清夜的迴風 舞雪在我心中翩躚。 煙霞嶺的桂香, 飄向幽谷和雪巘, 平湖秋月和斷橋殘雪。 蘇子愀然對月, 卻在惠州再會了蘇堤—— 玩月,豐、平二湖。 春曉,此心安處是吾鄉? 再會,上海的南京路, 一年前初秋的暖夜, 我曾如御西風, 也學那道家列子 ——大都會的夜不眠。 再會,當我發覺那將逝的、已逝的, 天下的美好,絕不能 再喚起我心中一絲快慰。 棧橋上殘陽似火,我的生命在海浪、 在驟風細雨中熊熊燃燒: 生的火焰、死的火焰、 溝口的金閣寺的火焰。 再會,認真地, 再會,我親愛的朋友和敵人, 素昧平生的人。 殘酷的祛魅,意義的失去。 如今秋風與殘陽縈繞我心——這是天譴。 湖上一輪明月, 太陽將在那後山誕生。 再會,那理性的感性的、 自然的和非自然的。 再會,舊的世界和秩序…… 再會,我不願面朝大海, 再會,春暖花開。 再會,自私的我將把祝福留給自己 ——我不是數十年前那位偉大的詩人,更不會像他一樣期待明天, 今日事今日畢。 置之死地而後生, 再會,支離破碎的自己。 世界在哲人的一聲高呼中崩塌, 新的意義在廢墟中出生。 那嬰孩的啼哭,Nirvana…… 再會,為了再次相會。 那麼, 再會,朋友, 別來無恙啊。

2023/10/6
articleCard.readMore

飢餓藝術家 卡夫卡

《飢餓表演藝術家》是卡夫卡曾計劃印刷的短篇故事合集,他死後才出版。其中與合集同名的小說,也是最受他珍重的幾個短篇小說之一。 故事情節大致如下: 多年前流行著一種飢餓藝術,人們爭著來觀看。飢餓藝術家往往為了藝術的榮譽感,自覺不去進食,而公眾卻會推選看守人員,看守人員也有意給他留空能偷偷吃東西。藝術家表現出自己捱餓的能力,希望消除人們的懷疑,可總有人不以為然。飢餓表演往往40天就結束,是為了能夠保障收入最大化。經理會表現出對藝術家的讚歎,那至高無上的榮譽,人們的歡呼迎接,藝術家是不以為然的。那一切狂歡都是為了經理的營收。飢餓藝術家希望再餓一會,挑戰極限。經理用各種解釋和照片,使公眾相信他的一套說辭。公眾或許憐憫,或許懷疑,把藝術家當作江湖騙子。藝術家是不滿意的,是不被理解的。 多年之後,當大街小巷飢餓表演不再風靡,人們又去追捧馬戲團的表演,去看那籠中馴服的野獸。飢餓藝術被拋棄了?沒有人會關注那舊事物那“江湖騙子”的消亡。藝術家也許應當轉行,可他熱愛這樣的表演,告別了經理,前往大馬戲團工作。馬戲團裡,他被安置在前往獸場的交通要道。人們在休息時間,趕去觀看野獸的時候才或許會注意到他。他仍然表演,挑戰自己的極限,可惜連記錄他捱餓時間的工作人員都也離去。沒有人在意他的成績。人們更願意去看野獸,人們把這飢餓藝術說是騙人的玩意兒。 表演結束,管事發現了籠子,發現了籠子裡的藝術家。他還在堅持,他不願意進食。“我只能捱餓,我沒有別的辦法。”“因為我找不到適合自己口味的食物。假如我找到這樣的食物,請相信,我不會這樣驚動視聽,並像你和大家一樣,吃得飽飽的。”他死了,他很堅定,他要繼續餓下去。 籠子裡換上了一隻小豹,人們更愛看這隻小豹,不捨得離去…… 這就是卡夫卡《飢餓表演藝術家》的情節。靠著捱餓表演換來觀眾的藝術家,在這種表演流行的時候不被理解,當表演不再流行便被人遺忘。他卻堅持這種藝術,他認為飢餓是實現自己的方式,他找不到其他的方式,“只能捱餓,沒有別的辦法”。 “飢餓表演”、“40天捱餓”,故事荒誕,卻引人深思,道出了深刻的真理。一個人,找到了他的熱愛,實現他價值的道路,便願意為此反抗世人不解的眼光,並願意為此付出生命。一生的孤獨落寞,一生的痛苦,在實現價值的時刻都不足掛齒。這件東西對於飢餓表演藝術家來說是飢餓表演,對於卡夫卡而言便是寫作。寫作對於卡夫卡來說很重要,他認為寫作是“一種祈禱的形式”。 據說在卡夫卡去世前一個多月,他在病榻上閱讀本篇清樣時,不禁淚流滿面,可見於書中主人公發生共鳴。 卡夫卡出生於奧匈帝國。他曾受過律師這門職業的培訓,在他完成法學課程後在受聘於一家保險公司工作,任職後的空餘時間,卡夫卡開始寫短篇故事。對於工作剩餘的時間,卡夫卡經常會抱怨難有較充裕的業餘時間從事寫作,因為自己不得不將大量時間去工作。他後悔對他的Brotberuf(“日常工作”,即“生計”)投入了過多的關注。因為歐洲當時對猶太人的壓迫排擠,卡夫卡時常曾抱怨自己身為一名猶太人。他對猶太人處境的低下、被動的埋怨與不滿也對他作品的風格有影響,但卡夫卡自認為身為猶太人卻對自己沒有起多大的幫助。卡夫卡在世時並不有名,大多數作品都是去世之後發表的,他在辭世之後才變得有名。 或許卡夫卡也是這樣一位“飢餓藝術家”,用自己獨特的荒誕文學希望實現自己的價值。生前並不被大眾看好,遺囑中本希望把作品銷燬,作品卻被朋友發表,才被世人所知。 他熱愛寫作,敬畏文字,工作之餘進行自己的“飢餓表演”。 因為除了寫作,他別無他法。

2023/8/15
articleCard.readMore

Python 中的 zip() 和 enumerate()

最近要用 Python 做一些小專案,記錄一些學習心得。以及這個部落格再不更新技術文章,就變成文學部落格了,顯然和初衷相違背() zip() zip() 函式用於將可迭代的物件作為引數,將物件中對應的元素打包成一個個元組,然後返回由這些元組組成的物件。 1 2 3 4 5 6 7 8 >>> a = [1, 2, 3] >>> b = [4, 5, 6] >>> c = [4, 5, 6] >>> zipped = zip(a, b, c) >>> zipped <zip object at 0x00000278786975C0> >>> list(zipped) [(1, 4, 4), (2, 5, 5), (3, 6, 6)] 這樣做的一種應用方式是在遍歷的時候,可以同時遍歷多個陣列: 1 2 3 4 5 6 >>> for i, j, k in zip(a, b, c): ... print(i, j, k) ... 1 4 4 2 5 5 3 6 6 如果遇到陣列不等長,會以最短的長度為準。 如果要按照最長的長度,可以使用另一個函式 zip_longest(),不多贅述。 enumerate() enumerate() 函式用於將一個可遍歷的資料物件(如列表、元組或字串)組合為一個索引序列,同時列出資料和索引。 1 2 3 >>> seasons = ['Spring', 'Summer', 'Fall', 'Winter'] >>> list(enumerate(seasons)) [(0, 'Spring'), (1, 'Summer'), (2, 'Fall'), (3, 'Winter')] 這也可以在遍歷的時候使用。 注意事項 這兩個函式使用的時候,需要匯入模組 itertools,否則會報錯。

2023/8/11
articleCard.readMore

泡沫

只恨一切都成了泡沫…… 浪宣告朗,日光下, 我試著抓住大海——它 卻後退,化身成點點泡沫。 海波泛起星光閃爍, 如秋波傳情勾起那些緬邈之懷。 這次,要抓牢—— 海無情,夕有情, 斜陽下,攥住一手細沙,金黃。 奔走,奔走,向大海, 跟隨泡沫的蹤跡。 前行,浪宣告朗, 拽住浪花似TA的衣袖, 只恨,跌倒,潮退, 失去。 迷茫。 泡沫,泡沫, 如今我兩手空空。 “雖不能至,然心嚮往之。” 或許會隨波流浪, 去大海深處, 去尋覓你, 尋覓浪聲依舊。

2023/8/10
articleCard.readMore

“救救孩子……”——談魯迅和《狂人日記》

“救救孩子……” 漸弱的呼喊,魯迅在狂人的絕望中收束全篇。這種無力卻格外讓人感覺似有一股刺骨之寒,或能於鐵屋中驚起更多較清醒的“不幸者”。 鐵屋中覺醒 魯迅“到N進K學堂“,研習格致、算學之類。眼界開闊了,方才醒悟中醫或許並非治人之病的良方,而還有更多像他的父親一樣得不到科學的治療的國人。於是他赴日留學,專攻醫術。某日,他觀看的日俄戰爭的影片中,同樣是中國人,“一個綁在中間,許多站在左右,一樣是強壯的體格,而顯出麻木的神情。”“據解說,綁著的是替俄國做了軍事上的偵探,正要被日軍砍下頭顱來示眾,而圍著的便是來賞鑑這示眾的盛舉的人們。“人們歡呼。在中國,日俄的衝突而被日本人斬首示眾的中國人,中國人在旁觀,中國人在高呼”萬歲“。魯迅心中震顫,便下定決心”改變他們的精神“,”提倡文藝運動“。 這是《吶喊》自序所述。或許驅使魯迅下定如此決心的,並非如此一次觀影經歷,而是長期以來所見、所思。中國人病了,病在精神上,病得不輕。 他想尋找同志。器物救國的道路指引下,“東京的留學生很有學法政理化以至警察工業的,但沒有人治文學和美術。” 最後邀集了幾人,願出雜誌《新生》,卻在出版之前各自退出。寂寞,魯迅如是形容。沒有人贊和,沒有人反對。根本沒有反響。悲哀啊,寂寞啊。魯迅面對的是一片荒原。“這經驗使我反省,看見自己了:就是我決不是一個振臂一呼應者雲集的英雄。” 寂寞的痛苦中,魯迅在S會館抄古碑,甘願讓自己的生命暗暗地消去。這是他的麻醉,許多年,直到錢玄同來訪——此時他們正辦《新青年》,對著荒原吶喊。“你抄這些有什麼用?”“沒有什麼用。”……“我想,你可以做點文章……”錢玄同希望魯迅為《新青年》撰稿。 “假如一間鐵屋子,是絕無窗戶而萬難破毀的,裡面有許多熟睡的人們,不久都要悶死了,然而是從昏睡入死滅,並不感到就死的悲哀。現在你大嚷起來,驚起了較為清醒的幾個人,使這不幸的少數者來受無可挽救的臨終的苦楚,你倒以為對得起他們麼?” “然而幾個人既然起來,你不能說決沒有毀壞這鐵屋的希望。” 希望?希望。希望!希望照亮困頓。魯迅最終答應,於是便有了此篇《狂人日記》。 在寂寞裡賓士,吶喊…… 狂人的妥協 在序言中,魯迅寫道這狂人是他中學時良友之弟,患”迫害狂“之類,寫下的日記。”語頗錯雜無倫次,又多荒唐之言“。如今病癒,卻赴某地候補了。 全文實在荒唐,卻讓人害怕。狂人起先覺得這趙家的狗多看他兩眼,需小心。之後又覺趙貴翁和那路上的人,似怕他,又似想害他,需提防。隨即,路上孩子、街上的女人、家裡的人、哥哥……一切的人,都被他所懷疑。他覺著全村的人似乎有要吃他的陰謀。最後卻發現自己不僅是“受害者”,也是“加害者”,和他們一樣——或許妹子就是被哥哥所食,或許她的肉或在飯菜裡也被全家人食用。便發出“沒有吃過人的孩子,或者還有?”之問,是對這個吃人世界之外的淨土的追尋。 吃人並非是吃的盡是肉體,也可以是精神。吃人並非一定需要尖牙利齒,也可以用“仁義道德”取而代之。 凡事總須研究,才會明白。古來時常吃人,我也還記得,可是不甚清楚。我翻開歷史一查,這歷史沒有年代,歪歪斜斜的每葉〔頁〕上都寫著“仁義道德”幾個字。我橫豎睡不著,仔細看了半夜,才從字縫裡看出字來,滿本都寫著兩個字是“吃人”! 狂人到底還是語無倫次。易牙和桀紂並非同時代的人,《本草綱目》並不是《本草拾遺》。但吃人的歷史沒有年代。古代,有“易子而食”,易牙可以把兒子蒸了先給齊桓公;當代,打兒子可以“咬幾口出氣”,“給知縣打枷過的,也有給紳士掌過嘴的,也有衙役佔了他妻子的,也有老子娘被債主逼死的”也都可以理所應當。“去年城裡殺了犯人,還有一個生癆病的人,用饅頭蘸血舐。””大惡人,給大家打死了;幾個人便挖出他的心肝來,用油煎炒”吃,可以壯壯膽子。”從盤古開天闢地開始吃人,吃到易牙的兒子,吃到現在。“好人”可以吃“惡人”,“惡人”活該被吃。理由便是“仁義道德”。 狂人意識到了“吃人”的事實,找身邊的人,希望勸轉他們,卻被當成了“瘋子”。或許這與魯迅的寂寞之痛也是相通的。意識到自己也是食人者後,感嘆“難見真的人!”這是一個人吃人的社會,希望便給了下一代。 “救救孩子……” 可惜,抱有如此希望,已經發現瞭如此真相,在“病癒”之後,他卻候補,聽候委用,願入朝做官。這或許又是一種妥協?發現吃人真相的異類,在吃人的社會中,如此謀求生存。 在鐵屋裡醒來,又在裝睡罷了? 希望、責任,給予下一代人。 “救救孩子……” 下一代人不應受到“吃人文化”的摧殘,應當摒棄這些錯誤的保守的所謂“仁義道德”。孩子不再吃人,社會也不再會是“吃人的社會”。“救救孩子”中蘊含“孩子救救”的祈禱。 “這人肉的筵宴現在還排著,有許多人還想一直排下去。掃蕩這些食人者,掀掉這筵席,毀壞這廚房,則是現在的青年的使命。” 引用魯迅《燈下漫筆》作結。

2023/8/9
articleCard.readMore

想念

好久不見啊! 古羅馬的鬥獸場,數千年矗立在地中海沿岸的小城。濛濛細雨,如遊絲,洗濯時間長河中穿梭著的風塵僕僕的旅客。 或許想起在煙雨江南,於靉靆之下縵立,遠眺城牆之外。人們總是刻意忽視時間,以為出了這城便是鄉。城門啟,誰知,還鄉,物是人非。 鄉愁,一如浪潮襲擊海邊的漁村,無情地將我拽入不見底的深淵,黑暗的虛空。一說是時間的鄉愁,想念幼時河邊遊遺失的安心。山腳下的村莊飄來了裊裊炊煙,鄉間蒸飯和熱油下鍋的氣息,勾引我千里之外的魂魄。記憶中我還是個孩童,手握竹竿,站立在山間公路的轉角。山路十八彎,那就是我的故鄉。 外婆怎麼還沒有喊我回去吃飯?外公怎麼沒有騎著摩托從水電站回來?記得那時深夜,轟鳴聲悠長,打破夏日鳴蟬。空氣中凝固的不安的氣息解凍,隨即是柴門聞犬吠,風雪夜歸人。向山行,向山行,爺爺卻讓父親走出大山。 那天,外公載我遊遍深山,縣界的隧道、縣外的群山,白溢寨和千丈巖,國家公園。緊握他的衣袖,疾風起,敞開外公的衣裳,軍綠色的夾克。那時尚未老去,路邊點起一支菸。隨風飄,一縷長煙細,似那天的雲靄。 後來,在山上的爺爺家,那夜難眠。離了父母,幼小的孩童,素未謀面的血親。窗外螢火蟲閃爍,也像外公的煙燼,如今何處去尋那夜啊!野草微搖,農舍內燒開的熱水上水氣瀰漫。轟鳴,外公接我回去。 許多年的故事,許多年的分別,漸忘了回家的路。回不去的是那時歲月。未變的是群山。 也思念舊時老友,那時校園漫步,無話不談。花團錦簇,蜂蝶飛舞,也如我們閒行的身影。深夜的資訊,聞知重逢的良機,急行,趕向過往的煙雲中。往事如煙,往事並不如煙。初見,相視無言。熟稔地挑起熟悉的話題,冰冷中見真情。 那時或在夕陽西下之時,暮色之下慢跑。少年在微光中,希望在夢想中。天色漸暗,燈起,前路初現。後來邁步衢上,回首向來蕭瑟處,也無風雨也無晴。歸去,歸不去。校園芳草依舊,故人不再。未變。真的沒有改變嗎?變化的是時間,是韶光。逝者如斯,如今我卻思念,思念那些流水年華。 回到故鄉,重返校園,時間的鄉愁責罰無罪的我。天啊,為什麼鞭笞時間中無辜的行人,用不盡的情感之潮,澆滅我向前的火種。總要向前行,卻無法阻止自己向後看。勇氣,向著未知。失去的歲月、人、事,一切,回憶中變得絢麗。 又是一年盛夏,從故鄉乘高鐵歸來。夕陽下,原野平靜,我心難平。慢一點,遠去的大山不再停留。或許遠方還有人在思念。相思,苦澀。落日,無情。 誰怕,此心安處是吾鄉? 想念……

2023/6/10
articleCard.readMore

淺灘

秋潮漲落, 在那心的橋樑。 彼岸還有草露甘香。 春泥, 是明天的味道。 明天?回答今日的困惑—— 海的女兒, 帶來獨木扁舟, 和那風的號角。 淺灘上, 思緒是雨燕, 盤旋徘徊在碧海青天。 飛去!遠行! 散不去的是層霧迷濛。 潮起,潮落, 潮水在我腦海的淺灘起落。 潮水啊,何時平息在大海的盡頭? ——大海一刻不停,在歡唱: “生生不息……“ 秋潮帶走了我, 淺灘上, 我還是我的我, 我的生命將是大海。

2023/3/30
articleCard.readMore

蟬 · 夏

一 六月的鳴蟬嘒嘒,微弱卻唱響夜空。 路旁的小樓上,沐心倚著陽臺的欄杆,遠眺萬里澄澈的夜空。偌大的城市。車水馬龍,光線在飛馳而過的喧鬧聲中劃過眼前的夜。她又在極目遠眺。但早就沒有什麼人讓她這樣等待了,她也不再是當年的孩童了。 沐心家在郊區,上海的郊區。遠方的鐵道線路,路旁樹叢中窸窸窣窣的夏夜之歌,沐心心裡卻是另一番景象。此時她正看著稀少的來往車輛,發著呆。她喜歡這樣,在夜晚思考。但她的確沒有思考什麼。 “那些……所以……” 她似在嘀咕著。這心中湧動的問與答不小心說了出口。 臉龐淡紅,她忽地四下望,慌亂中險些將手中的筆丟到輕盈的夏季的空氣中,墜落。好在四周沒有人。畢竟那很是尷尬呢。 綠化帶上,花叢中飄來淡淡清香。 回到起居室內,牆壁上貼著幾面鏡子。夏季新雨過後,燥熱卻仍帶溼潤的空氣早已給鏡面蒙了一層紗。不知是深夜的恍惚,還是水霧幻境,亦或者是幻覺,沐心在鏡中看見了自己的倩影,似在清澈的夢境中漂浮。奇怪,明明是夏季,南方夏日,心裡實在是一絲寒冷,太涼了。鏡中,她看見了眼角帶著一些滿溢的溫熱的溼潤。或許是水汽,或許……她莞爾一笑,或是笑情緒真是捉摸不透。 但剛才面對廣袤深空,感受大地和城市脈搏,她確是有些言語未說出口。蟬鳴替她講完了口邊的千言萬語。 橫道線,柏油路,黑白漆。入夜,染上了淡藍,彷彿被水蒙蓋一般奇妙。 交通訊號燈還在閃爍。 二 淚眼迷濛中,她又開始遠望。她看見的不是山,不是海,不是墜入大地的夕陽。她看見了虛空,夜晚。星稀疏,一顆、兩顆——那是飛機。 高處風疾。她扣上毛線衫的扣子,雙手在胸前輕撫。很是清涼。觸控耳旁,髮絲不經意間滑過那手。碧發微寒。 蟬鳴還未歇。 夜晚帶她去往十多年之前。 那時,在公路道口,夾竹桃盛開的地方,外婆帶著她等待晚歸的父母。 那時,經濟發展迅速,這樣的千年古國展露出蓬勃生機。這座國際大都市,上海,自然吸引了許多投機者,創立企業。她的父母便是這樣的人,不過,他們算是後來者。不知有沒有趕上末班車了。沐心只在政治課上偶然瞭解了這些,那些政治課也不知她是如何度過時光的。 不過她記得,每個這樣的黃昏,她都和外婆在這裡等待。那時她五歲。 悶熱的天氣,潮溼的襯衫,實在是讓人喘不過氣來。夕陽把碧空染成火的顏色,高速路邊綠樹成蔭。道口有一個公交車站,卻少有行人。這是終點站。 她不在乎來往的車輛,不在乎來往的行人。她極目遠眺,希望見到一個騎摩托車的身影。那是父親。 她若有所思,發呆的習慣或許是從這一刻開始的。 蟬鳴窸窸窣窣,知了知了…… 外婆通常不會說話,這是她的性格。沐心很少瞭解到外婆的故事,卻只是知道外婆是母親從遠在山區的老家叫來照看她的。不過那時外婆也還年輕。 “媽媽她們快回來了,很快的。” “嗯……“ 等待中,夕陽已從樹梢落入地平線,斗轉星移,繁星點點。她看著來往車輛,鳴笛聲也像水流般從她耳側淌過。天空中還是有飛機,但她沒有興趣。她看向昏暗的樹林,似乎感覺有什麼可怕的東西要衝出來。那或許是外婆口中傳說的野獸,嚇唬小孩子的什麼東西。她或許從沒有信以為真。但那些恐懼是有名字的,就是她心中的不安。 陽光沉入大地,她心裡卻只是想,他們怎麼還不回來,他們什麼時候回來。從小一個人成長,也不懂得與人們交流。她很是不安。 蟬鳴越來越急了,她心煩意亂。 這時微風拂面,她的髮絲飄搖。遊絲一般的悶熱依然縈繞,擔憂陰魂不散。 但遠處騎著黑色摩托的熟悉身影到來,她心中的石頭沉下來了。蟬鳴聲不息,窸窸窣窣。她的眼淚都快落下來了。 後面的事,她的記憶便不太清晰了。但那種缺乏安全感的混亂,卻被她帶走了,直到今天。 三 她往往是沉默的,她看似少年老成,內心活動卻異常豐富。她不善與人相處,卻又渴求一種她自己也說明不清的東西。她曾在回憶中搜尋,在散步時追問。沒有答案。所以她在痛苦什麼?所以她在追尋什麼? 鐵道線上,地鐵飛馳而過打斷了她的思緒。 地鐵?初中時期,沐心就每天乘地鐵上學。地鐵飛馳而過,空氣的清澈聲響,讓她倍感親切。 曾經有一陣子,沐心確實以為自己得到了自己想要的東西。 一頭烏黑秀髮的少年,成績也是不錯呢。性格活潑,又有一些調皮。他那時在班上也很受歡迎吧。少年名叫晨星,是沐心的同班同學,性格卻完全相反。但不知什麼機緣巧合,兩個人相識了,相處還算是愉快。不過如今沐心也記不得什麼了,斷絕往來,只能嘆息人性虛偽咯。 不過,他們曾經每週的書店之旅,卻實在是難忘。 學校門口,桂花盛開。學校在上海的老城區,學校是一所老學校。放學時間,同學都圍在門口小賣店,談論最新的電影、小說,嬉戲打鬧。馬路上車流不斷,天空是淺粉色。 放學之後,他們會去快餐店買那幾元錢的小吃,也會去書店閒逛。書店邊是一所大學,學校裡樹林茂盛,那時也快到了櫻花盛開的季節。 那時,沐心的整個世界崩塌了。她只記得自己病了,心病了。冰冷的世界,冷漠的人潮,她只覺得人性冷淡,覺得世界陌生,覺得處處敵對。暗淡無光的眸子中,淚光常掛。但連淚光也是無力的。只有那嘆息聲中還能聽得出一些希望。 書店之行就是她生活中的光。 “我喜歡川端康成,我要做那樣的小說家。” “嗯,他的書確實不錯。我也喜歡。” 晨星介紹自己熱愛的數目,沐心便看到了少年朝氣,有的時候還為這種感覺感動呢。 那段時間,沐心差點自殺了。那段時間,她確實死了,但她重生了。 還記得她“回來”之後,面對十一月冰冷的夜空,在南京路上輕盈的步伐,和朋友逛街探店,臉上的歡笑。 夕陽斜照,步行街上,她感覺在微風中,她要飛起來了。 草木微動,澄空微寒。 四 蟬鳴聲不歇的夏季,沐心這次有些迷茫。 她經歷了太多,比眼前馬路上來往的車輛數多多了。無數次的等待,無數次陪伴和冷落。她還在追尋什麼,她嚮往的生活是什麼? 她自己也不知道。 獨立?自由?愛? 她缺少家庭的溫暖,缺少真實的交流。對,確實是這樣。 但她又確實不缺少什麼。 蟬鳴聲窸窸窣窣。她的思忖永無休止。 她仇恨嗎?有一點。 但她早已知道,過去的世界已經在幻夢中走向消亡。 門鈴聲響。父親夜晚回家。 她好像幻聽。“抱歉,久等了。“ 沒有聲音。 ”回來了。“ 無數次的等待,無數次的不安。 幼時曾擔心而哭泣。長大後,這一切都有了新的含義。 未來想來會不會覺得幼稚? 但在這些經歷中,她變了。 …… 蟬鳴聲不息,空氣悶熱。視窗亮起昏暗的燈火。那是遠方的村莊。她睡去了,淚光比夜空澄澈。

2023/3/25
articleCard.readMore

微風

少年的長髮,飄揚在微風明朗的五月。 氣息中還帶有一絲土壤的味道,絮飛揚,花灑地。昨夜的雨是否浸溼了嬌嫩的綠葉,沉睡的草木醒來,便是瘋狂滋長。入夏,蟬鳴。 斗轉星移的深邃海洋,飛鳥劃過夜空。空中若隱若現的漣漪,像那些出海的日子,海鷗濺起片片水花,如同敲擊冰面的冰花。 少年就這樣望。在窗邊望,在小院思。院門外雞狗聲未歇,在看不到的角落,或許行人驚醒了沉睡的生機。但哪有什麼行人?半夜三更。…… 少年要去赴那六月的邀約,回到想象中水墨山水的西湖。湖上的蓬舟,水上的浮木,船伕用船槳在水中留下的草書筆畫,是否如蘭亭的碑文。未有形先有情。濁酒一杯,徹夜不眠。湖上附著的水汽的薄膜,如蒞仙界。少年想起了少女的素娟,柔轉、飛舞,牽動的情思。然而透過西湖的霧色面紗,黑暗難以辨識的遠山上還有昏黃的燈火。山外是否有人家?山中行,耕作、飲酒、作詩,砍柴餵馬。天下遨遊,睥睨萬物。 可惜少年在那個五月出發去了遠方。那是一種生命中或許只能有一次的奇妙旅行。穿過意識深處的山野,在荒郊城外的林間小路行走,踏上天空。天空或許有侷限。囚禁在這裡,在樹林、在空間穿梭,在時間。少年困在現在,現在擺脫不了少年的束縛。莫名的苦痛,眼角的淚花。變幻莫測的時空,這個世界上必然有能容少年之地?沉入深海。 湖光山色,天快亮了。船上是少女的笑顏。粉色的天空,淺藍的湖水,純潔的白霧。碧竹編成的蓬舟,如今歷經滄桑,泛黃。忽然飄起了雪花,一片一片,粉白的澄空中,一片一片。 少女起身,船頭眺望。一望無際的湖山。少年是否長存天地之間?逍遙自在,山間村居。少女不知道,但或許少年在她中存在,誰知道呢。一段未有人訴說的故事,或許來年春天向我們講述? 少女的長髮,飄揚在微風明朗的十二月。

2023/3/12
articleCard.readMore

觀星

二月快樂! 今夜的我 沒有篝火 山歌和露水 立在窗前好似身處一片虛無 觸不可及的,是 那飄渺的月的詩行 和纏綿悱惻的繁星的歌謠, 都隨我進入那清風入夜而歸。 何歸也? 窗前的虛無, 天真的人們把他稱作“星空”。 那是人類的終極宿命, 從參宿四出發, 半人馬座、小熊座、雙魚座。 總有一個地方, 覓遍那枯腸, 也要尋得恰如其分的比喻, 傾訴跨越光年的思忖。 光年之外, 琴聲來自光年之外, 那一刻我變成了光, 光變成了宇宙。 那是 137 億年前的訊息, 今夜向我訴說, 或許如《百年孤獨》一般, 命中註定和羊皮卷, 正等待我破譯。 地球上, 詩人寫下詩行, 被天地之間的人們傳唱; 宇宙間, 銀河裡盛滿了今夜我心中的淚, 而我的空虛也有宇宙廣大。 今夜, 我當織女, 而那逝去的過往便是我的牛郎, 只恨過往再難相遇。 世事繁雜, 車水馬龍, 攪亂我 綿延至數個天文單位外的 聯結, 是同那宇宙, 和那過往的雲靄。 在世界尋不得自己的位置, 我願遠去, 去那星雲之間, 做一顆即將誕生的恆星, 光熱,照亮世間的層霧。 今夜的你 沒有篝火 山歌和露水, 兩手空空來敲我的家門, 何妨? 好在還有封存的記憶。 遠行, 去脫離著輪迴, 掙脫指數級的孤獨的束縛。 遠行, 不知東方既白。

2023/2/1
articleCard.readMore

浮塵

最近嘗試隨便亂寫點詩歌。 微塵, 浮沉, 孤身行大漠的旅人。 跨越了半個世紀的風雪, 休憩在我的窗前。 行大漠, 越風雪, 如今在太陽的怒吼下, 飛舞, 浮沉, 像個不懂事的孩子。 微塵 浮沉, 微不足道的力量, 卻敢向那太陽挑釁, 從不篤信是那太陽給了他生命和舞蹈。 跨越了半個世紀的風雪 休憩在我的窗前, 又用那剩下的半個世紀, 完成未竟的事業, 挑戰太陽, 飛舞, 浮沉。 東方的太陽, 強壯的戰士, 沉沒在西方的深海。 火種熄滅。 熄滅, 浮塵, 黑暗中舞蹈。 黑暗中孕育著新的太陽。

2023/1/27
articleCard.readMore

復活

歲月如梭;來日方長。 幻想自己的模樣,並被如今嚇一跳。是時候啟航了,這一次去更遠的地方,用更好的航海術。 光,是流水劃過我的眼眸。睜開雙眼,柔焦,模糊的光影。眼角之間,好像有什麼粘稠作一團的東西,阻礙著光線的生長。眼角還有淚痕,彷彿昨夜一場夢境初醒,卻信以為真,身臨其境,不能自拔,深陷其中而感受到的虛弱。是的,我無法抬起我的雙臂,使出全身力氣,不能控制我的雙腿。我的身體脫離了束縛,或者說,我的靈魂脫離了束縛。肉體存在於很遠的地方,要等待醒來的召喚,解除夜的夢的封鎖。在夢境裡,無法做出選擇,對著變化的世界,旋轉,眩暈。光絲在眼中生長擴散,舒展開的肌肉,雙眉。醒來之後又有一個夢,夢裡還有夢,環環相扣,人生只不過是眾多夢境中的一個,何必分清虛實。 起身,又是一個清晨,嘆息已經註定了一天的重複的單調。穿衣,刷牙,洗臉,整理,打扮,重複的早餐,三明治配牛奶。仍有氣無力開啟電腦,Telegram、微信、Visual Studio Code。很好,不出所料的沒有新資訊。手機上,微信群裡的聲音卻已經讓人厭倦,甚至讓人想要解除安裝微信,清空所有聯絡。不過是一些交流群、同學群,卻沒有勇氣,或者說沒有興趣點開那空白的輸入框。就像新建檔案卻不知道填寫什麼標題,刪除,再新建,永無休止才發現一個上午的時間早已流失;就像站立的荒蕪的水泥地上,而這水泥地正在你的腳下崩裂,墜入黑暗的深淵,在邊境之地徘徊,懸浮,卻沒有勇氣向下一看驚喜地發現離地半米,有驚無險。人或許是危險的,就像孤身一人行走在原始森林,對周圍的風吹草動擔驚受怕,哪怕只是一隻弱小的螞蟻。畢竟是一個人。過去,行走在人潮湧動的商業區,南京東路,看著來往的人流,交通警察維持秩序,店員售賣琳琅滿目的商品。燈火通明,夜空被霓虹招牌點亮,燈下嫋嫋的煙靄,路邊攤的燒烤,遠處的香味。那一個晚上或許只是一場夢。接近人潮,不安,抗拒,這樣的喧鬧只讓人心神不寧,甚至不願與那店員交易換取心儀的紀念商品。這種縈繞的思忖,反覆湧動如同這來往的不斷地人潮,腦海中,一個開啟就無法關閉的收音機,播放我聽不懂的語言,蜂鳴。緊接著是靜穆,遠處來往的地鐵。溺亡,在自己創造的聲音的海洋中。但今天,我祈禱免受折磨,免於崩潰。 正午,日光擊在窗前,我心中一顫,好像聽到了太陽的搏擊。地面是紙屑,卻像五月的碎玻璃。但那或許只是我的謊言。那天玻璃杯無故墜地破碎。那天重拾起封印的記憶。想起以前,能有一個在自己控制之內的圈子,便就能滿足,便把自己鎖起來,獨享安寧。想到往昔的隱者,自耕自食。回不去又何必在談起。於是寫下,逃離舒適圈,說起,自己不再能滿足的醜話。誰知是如今的窘迫。幸於能收穫一線庇護,一絲未來的希冀吧。由是感激。雖然有時甚至覺得上天不公,背叛了我,私自選擇了我的誕生,選擇了我的社會身份,選擇了一切,但來了總不能留下遺憾。是嗎? 手機上有了新通知?起身,彈出座位,一把抓住手機,扯下充電線,按開螢幕,劃出通知欄,卻發現是垃圾郵件。不知道在期待什麼啊!微信上,重複發出的表情,解釋說自己變了,辯解說自己開心。狡辯。也罷,往事如煙。長期行走在痛苦的邊緣,過去是否真正互相理解?不過站在當今看過去,就連最悲慘的都被珍惜。也是可笑,人類如何能做到有這麼多情感,我又如何能做到如此在意?我不在意,我要逃離那個地方,逃離這個紛繁的世界。我要回到我的世界。 冰雪融化,卻是在午後。太陽似乎要吞噬這座內心的冰城,而水流也已近乾涸。破開生活的繭,破開自己已知的世界,走向未知。此刻我是那個好奇的初生的生命,探索從未涉足的領域,社會的領域。開啟圍欄,開啟窗戶,像鳥飛向遠方。重生,或許是復活。但是生與死沒有界限,生即是死,死即是生。如此說,這是我的第5783次。手起刀落,斬斷流動的人潮,斬斷隔膜,剷除混響的收音機。我瘋了,但我沒瘋。這個世界上有太多的人,他們瘋了?當世界上所有人都不能理解我的內心,他們瘋了還是我瘋了?他們不正常還是我不正常?沒有人是對的,沒有人是錯的。切斷這個夢的天際,讓我重新控制我的身體。太久的沉寂,我需要醒來。向外界邁出新的一步,這一次步態堅定。試圖融入,試圖改變,試圖接受。永恆的不是閉塞,而是永珍更新的生氣。並慶祝我此刻的存在。是的,謝天謝地我活下來了。不說改變世界的空話,先提升自己。 還是想起某一個午後,約定去書店。那時候的內心世界陰沉,你是一束光照進了我,穿透了我,我醒了,我悟了。翻動書本,立志一起追逐天邊的太陽;在夜晚也會歌唱,圍爐夜話,談論瑣碎的小事。買一瓶奶茶,把它弄灑,還有河流上的風。五彩斑斕的世界,從痛苦的記憶中走出,感受到生活的真實。我真實存在著。你不懂,我以為你懂了。這是無意之舉。或許又有一天,午後,卻因為一點點小分歧,餐盤打翻在空中,我的悔恨從此不再離開。但那不是你,但那是我未能送上的歉意,是我欠下的一筆債,無處去還。默默祈禱自己的轉變。不在意,不評論。只談論自己,太久不奮力拼搏,如今的我要為生存,為生活反抗,反抗世界,反抗天地。並且但願我不是孤身一人。 身體中充滿力量,前進的動力,那些過去或許一起說過的話。沒有道路就創造道路,沒有條件就創造條件,沒有方法就創造方法。“騙誰呢。”但如今的確可以再拿出來,想著許諾的未來邁進了,但是想到少了點什麼。 復活?你說只是來到了下一層煉獄。 煉獄就煉獄。我喜歡這裡,因為不經受考驗,就不再會有成長。我感恩痛苦,感恩打擊;我祈禱痛苦,祈禱夜幕降臨,縱使繁星點點無法入眠,腦海中思緒不受控制,不受限制。 夜幕真的降臨了。Wish you were here. 但 You are always here 不是嗎。可能吧。但我要遠航了,去那遙遠的地方。帶上希望。復活。

2023/1/11
articleCard.readMore

【摘錄 | 轉載】普魯斯特 《追憶似水年華》第一卷 《在斯萬家那邊》(一)

摘錄一些最近正在拜讀的作品。 P46~50 就這樣,在很長一段時期內,每當我半夜夢中回憶及貢佈雷的時候,就只看到這麼一塊光明,孤零零地顯現在茫茫黑暗之中,象騰空而起的焰火,象照亮建築物一角的電光,其餘部分都沉沒在黑夜裡。這塊光明上尖下寬:下面是小客廳、餐廳、花園中幽暗小徑的開頭一截(無意中造成我哀愁的禍首斯萬先生要從那面走來)和門廳(我要由此而踏上樓梯的第一級),而攀登起來令我心碎的樓梯則構成這個不規則稜錐體的非常狹窄的錐幹;頂部是我的臥室、臥室外的過道、過道口的玻璃門,我的母親就是從那裡進來的。總之,老在晚上那個鐘點見到、同周圍事物完全隔絕、在黑暗中孤零零地顯現的,就是這麼一幕簡而又簡的佈景(等於一般老式劇本的開頭為供外省演出參考而作的佈景提示),為了重演我更衣上床的那出戏,這些道具是少得不能再少了;似乎貢佈雷只有樓上樓下,由一部小小的樓梯連線上下,似乎只有晚上七點鐘這一個時辰。說實話,倘若有人盤問我,我或許會說貢佈雷還 有別的東西,別的時辰。但,那將是我有意追憶,動腦筋才想到的一鱗半爪;而有意追憶所得到的印象並不能儲存歷歷在目的往事,反正我決不會自願地去回想貢佈雷的其他往事。它們在我的心目中其實早已死了。 永遠消亡了?可能吧。 這方面偶然的因素很多,而次要的偶然,例如我們偶然死去,往往不允許我們久久期待首要的偶然帶來的好處。 我覺得凱爾特人的信仰很合情理。他們相信,我們的親人死去之後,靈魂會被拘禁在一些下等物種的軀殼內;例如一頭野獸,一株草木,或者一件無生物,將成為他們靈魂的歸宿,我們確實以為他們已死,直到有一天——不少人碰不到這一天——我們趕巧經過某一棵樹,而樹裡偏偏拘禁著他們的靈魂。於是靈魂顫動起來,呼喚我們,我們倘若聽出他們的叫喚,禁術也就隨之破解。他們的靈魂得以解脫,他們戰勝了死亡,又回來同我們一起生活。 往事也一樣。我們想方設法追憶,總是枉費心機,絞盡腦汁都無濟於事。它藏在腦海之外,非智力所能及;它隱蔽在某件我們意想不到的物體之中(藏匿在那件物體所給予我們的感覺之中),而那件東西我們在死亡之前能否遇到,則全憑偶然,說不定我們到死都碰不到。 這已經是很多很多年前的事了,除了同我上床睡覺有關的一些情節和環境外,貢佈雷的其他往事對我來說早已化為烏有。可是有一年冬天,我回到家裡,母親見我冷成那樣,便勸我喝點茶暖暖身子。而我平時是不喝茶的,所以我先說不喝,後來不知怎麼又改變了主意。母親著人拿來一塊點心,是那種又矮又胖名叫“小瑪德萊娜”的點心,看來象是用扇貝殼那樣的點心模子做的。那天天色陰沉,而且第二天也不見得會晴朗,我的心情很壓抑,無意中舀了一勺茶送到嘴邊。起先我已掰了一塊“小瑪德萊娜”放進茶水準備泡軟後食用。帶著點心渣的那一勺茶碰到我的上顎,頓時使我混身一震,我注意到我身上發生了非同小可的變化。一種舒坦的快感傳遍全身,我感到超塵脫俗,卻不知出自何因。我只覺得人生一世,榮辱得失都清淡如水,背時遭劫亦無甚大礙,所謂人生短促,不過是一時幻覺;那情形好比戀愛發生的作用,它以一種可貴的精神充實了我。也許,這感覺並非來自外界,它本來就是我自己。我不再感到平庸、猥瑣、凡俗。這股強烈的快感是從哪裡湧出來的?我感到它同茶水和點心的滋味有關,但它又遠遠超出滋味,肯定同味覺的性質不一樣。那麼,它從何而來?又意味著什麼?哪裡才能領受到它?我喝第二口時感覺比第一口要淡薄,第三口比第二口更微乎其微。該到此為止了,飲茶的功效看來每況愈下。顯然我所追求的真實並不在於茶水之中,而在於我的內心。茶味喚醒了我心中的真實,但並不認識它,所以只能泛泛地重複幾次,而且其力道一次比一次減弱。我無法說清這種感覺究竟證明什麼,但是我只求能夠讓它再次出現,原封不動地供我受用,使我最終徹悟。我放下茶杯,轉向我的內心。只有我的心才能發現事實真相。可是如何尋找?我毫無把握,總覺得心力不逮;這顆心既是探索者,又是它應該探索的場地,而它使盡全身解數都將無濟於事。探索嗎?又不僅僅是探索:還 得創造。這顆心靈面臨著某些還 不存在的東西,只有它才能使這些東西成為現實,並把它們引進光明中來。 我又回過頭來苦思冥想:那種陌生的情境究竟是什麼?它那樣令人心醉,又那樣實實在在,然而卻沒有任何合乎邏輯的證據,只有明白無誤的感受,其它感受同它相比都失去了明顯的跡象。我要設法讓它再現風姿,我透過思索又追憶喝第一口茶時的感覺。我又體會到同樣的感覺,但沒有進一步領悟它的真相。我要思想再作努力,召回逝去的感受。為了不讓要捕捉的感受在折返時受到破壞,我排除了一切障礙,一切與此無關的雜念。我閉目塞聽,不讓自己的感官受附近聲音的影響而分散注意。可是我的思想卻枉費力氣,毫無收穫。我於是強迫它暫作我本來不許它作的鬆弛,逼它想點別的事情,讓它在作最後一次拚搏前休養生息。爾後,我先給它騰出場地,再把第一口茶的滋味送到它的跟前。這時我感到內心深處有什麼東西在顫抖,而且有所活動,象是要浮上來,好似有人從深深的海底打撈起什麼東西,我不知道那是什麼,只覺得它在慢慢升起;我感到它遇到阻力,我聽到它浮升時一路發出汩汩的聲響。 不用說,在我的內心深處搏動著的,一定是形象,一定是視覺的回憶,它同味覺聯絡在一起,試圖隨味覺而來到我的面前。只是它太遙遠、太模糊,我勉強才看到一點不陰不陽的反光,其中混雜著一股雜色斑駁、捉摸不定的漩渦;但是我無法分辨它的形狀,我無法象詢問唯一能作出解釋的知情人那樣,求它闡明它的同齡夥伴、親密朋友——味覺——所表示的含義,我無法請它告訴我這一感覺同哪種特殊場合有關,與從前的哪一個時期相連。 這渺茫的回憶,這由同樣的瞬間的吸引力從遙遙遠方來到我的內心深處,觸動、震撼和撩撥起來的往昔的瞬間,最終能不能浮升到我清醒的意識的表面?我不知道。現在我什麼感覺都沒有了,它不再往上升,也許又沉下去了;誰知道它還 會不會再從混沌的黑暗中飄浮起來?我得十次、八次地再作努力,我得俯身尋問。懦怯總是讓我們知難而退,避開豐功偉業的建樹,如今它又勸我半途而廢,勸我喝茶時乾脆只想想今天的煩惱,只想想不難消受的明天的期望。 然而,回憶卻突然出現了:那點心的滋味就是我在貢佈雷時某一個星期天早晨吃到過的“小瑪德萊娜”的滋味(因為那天我在做彌撒前沒有出門),我到萊奧妮姨媽的房內去請安,她把一塊“小瑪德萊娜”放到不知是茶葉泡的還 是椴花泡的茶水中去浸過之後送給我吃。見到那種點心,我還 想不起這件往事,等我嚐到味道,往事才浮上心頭;也許因為那種點心我常在點心盤中見過,並沒有拿來嚐嚐,它們的形象早已與貢佈雷的日日夜夜脫離,倒是與眼下的日子更關係密切;也許因為貢佈雷的往事被拋卻在記憶之外太久,已經陳跡依稀,影消形散;凡形狀,一旦消褪或者一旦黯然,便失去足以與意識會合的擴張能力,連扇貝形的小點心也不例外,雖然它的模樣豐滿肥腴、令人垂涎,雖然點心的四周還 有那麼規整、那麼一絲不苟的縐褶。但是氣味和滋味卻會在形銷之後長期存在,即使人亡物毀,久遠的往事了無陳跡,唯獨氣味和滋味雖說更脆弱卻更有生命力;雖說更虛幻卻更經久不散,更忠貞不矢,它們仍然對依稀往事寄託著回憶、期待和希望,它們以幾乎無從辨認的蛛絲馬跡,堅強不屈地支撐起整座回憶的巨廈。 雖然我當時並不知道——得等到以後才發現——為什麼那件往事竟使我那麼高興,但是我一旦品出那點心的滋味同我的姨媽給我吃過的點心的滋味一樣,她住過的那幢面臨大街的灰樓便象舞臺佈景一樣呈現在我的眼前,而且同另一幢面對花園的小樓貼在一起,那小樓是專為我的父母蓋的,位於灰樓的後面(在這以前,我歷歷在目的只有父母的小樓);隨著灰樓而來的是城裡的景象,從早到晚每時每刻的情狀,午飯前他們讓我去玩的那個廣場,我奔走過的街巷以及晴天我們散步經過的地方。就象日本人愛玩的那種遊戲一樣:他們抓一把起先沒有明顯區別的碎紙片,扔進一隻盛滿清水的大碗裡,碎紙片著水之後便伸展開來,出現不同的輪廓,泛起不同的顏色,千姿百態,變成花,變成樓閣,變成人物,而且人物都五官可辨,鬚眉畢現;同樣,那時我們家花園裡的各色鮮花,還 有斯萬先生家花園裡的奼紫嫣紅,還 有維福納河塘裡飄浮的睡蓮,還 有善良的村民和他們的小屋,還 有教堂,還 有貢佈雷的一切和市鎮周圍的景物,全都顯出形跡,並且逼真而實在,大街小巷和花園都從我的茶杯中脫穎而出。

2023/1/4
articleCard.readMore

Time - Pink Floyd - The Dark Side of the Moon

Having pored over the lyrics, I have these comments: Nobody to show us the way, nobody to tell you when to run, no chance to catch up with the sun. It’s late and you’ve been getting closer to death. So get home and see the rain. Paralyzed, missed out on the sunshine and at last forever wandering. Links YouTube | Wikipedia

2022/12/29
articleCard.readMore

【轉載】靜夜思變調

轉載一首好詩。 故鄉,留下一段回憶,過去的我在現在的我在未來的我,縈繞,懷念。可惜故鄉,可恨昨天只隔數小時,卻比月還遙遠。 畢竟,一秒鐘的距離便是到宇宙毀滅的距離了。 生命是瞬間。 原文:熊秉明 ·《靜夜思變調》 一 序 大詩人的小詩 從椽筆的毫端落出來 像一滴偶然 不能再小的小詩 而它已岸然存在 它已是我們少不了的 它在我們學母語的開始 在我們學步走向世界的開始 在所有的詩的開始 在童年預言未來成年的遠行 在故鄉預言未來遠行人的歸心 遊子將透過童年預約的鄉思 在月光裡俯仰悵望 於是聽見自己的聲音伴著土地的召喚 甘蔗田 棉花地 紅色的大河 外婆家的小橋石榴.... 織成一支魔笛的小曲 二 一個古老的詩國 有一個白髮的詩人 拈一片霜的月光 凝成一首小詩 給所有的孩子們唱 一代一代地唱 會須一飲三百杯 老詩人撈月去了 小詩留在月光裡悠揚 在故鄉悠揚 在他鄉悠揚 三 祖父的老花眼鏡邊 折射出菜油燈黃黃的火苗 床前明月光 疑是地上霜 祖父的花白鬍子裡 漏出嫋嫋烈草煙味的青煙 舉頭望明月 低頭思故鄉 爺爺,我會背了。」 眼鏡後面的眼角上有一點淚 鬍子後面的嘴裡沒有牙 孩子,玩去吧!」 四 床前明月光 疑是地 上霜 舉頭 望 望 明 明 月 低頭 思故 思故 思故鄉 床前月光 疑地上霜 舉頭明月 低頭思鄉 床前光 地上霜 望明月 思故鄉 月光 是霜 望月 思鄉 月霜 望鄉 五 姐姐 你還記得嗎? 在月光裡 你曾玲瓏地望 床前明月光 疑是...真是... 真是霜一樣的月光呵 後面的兩句呢? 我帶它們走了 走了半個世紀 走到沒有土瓜和雞棕的地方 沒有麥粑粑和過橋米線.... 舉頭望明月 低頭...舉頭... 六 床前明月光 疑是地上霜 祖父教的第一首唐詩 於是拍著雙手 踏著院子的石板 著迷地唱 明月光 地上霜 地上霜 明月光 孩子已作了祖父 過去的孩子在今天的祖父心裡 頑童一樣著迷地唱 思故鄉 思故鄉 思故鄉 思故鄉 七 月光裡的故鄉 月明瞭的故鄉 故鄉時的明月 故鄉思的月光 什麼時候起 迷作一片朦朧 鄉 即是 月 月 即是 鄉 迷迷 疑疑 望望 茫茫 注滿眼底 溢位眼外 月月鄉鄉 圓了的月啊 月了的鄉 八 在時間的那一頭 在世界的那一頭 拍著手 拉著手 孩子唱 望明月 問明月 月光光 明月鄉 在時間的這一頭 在世界的這一頭 舉頭 低頭 滿頭霜 滿頭霜 滿眼老花的月亮 滿面粼粼的月光 九 低頭思故鄉 馬鍋頭他們一定 還在橫斷山脈裡 橫斷著山的脊樑 高山的風和日敲他們的銅臉 水牛一定還在紅河水裡 輕盈地浮沉黑鐵的犄角 甘蔗的汁比紅河水更濃 煉成霜色的冰糖 每一個結晶的面都閃閃地唱 床前明月光 十 昨天母親來信說 我好 你好嗎? 我給母親回信 我好 您好嗎? 月亮是蒼白的 月亮不說話 故鄉比月 更遠 一倍 十一 疑是霜 疑是霜 悄然落在書頁上是一絲閃閃的白髮悄然 落在書頁上不再是青色的髮絲了那曾是 父親的白髮使我心驚的悄然落在書頁上 而今是我自己的白髮悄然落在書頁上而 今只有我自己的白髮了悄然落在書頁上.. 舉頭望 舉頭望 十二 低頭思故鄉 我已回去 我已回不去 我已不回去 我已在路上 秦時明月漢時關 我已渴斃 我已骨折 絲綢的路 駱駝、石頭和骷髏的路 床前明月光 十三 床前明月光 節節骨痛的床 沒有床的稻草墊 沒有稻草的泥地 沒有泥地的水門汀 水溼的枕頭 火熱的枕頭 沒有枕頭的 驚醒的失眠的 眼閃著月光 疑是地上霜 十四 舉頭望 舉頭望 中秋月從樓影后面探過來 圓了白淨的臉 圓了驚異的眼 咦 怎麼沒有月餅? 怎麼沒有栗子和梨? 怎麼沒有柿子和棗? 沒有紅的蠟燭? 沒有香爐和香? 連圓的桌子也沒有 握一卷太白詩集在背後 靜悄悄地 月亮看我 我看月亮 十五 好月光 好月光 唱一支歌兒吧 咱們唱 松花江上 唱咱們都會唱的 不行 不行 我不能唱 我不能唱 一唱聲音就嗚咽了 再唱嗓子就哽住了 三唱眼晴就什麼也看不見了 你們唱 我跟著 我的家.... 在....在 在東北松花... 江上.. 十六 這是詩 這是一首詩 這是一首中國孩子學的 第一首中國詩 大家跟我念 床前明月光 疑是地上霜 舉頭望明月 低頭思故鄉 熟麥的鬈髮 海水的眼晴 比越女更白 琳琅錯亂的回聲 太白 大概 大笑了 十七 三個孩子到中國去了 兩個大學生一箇中學生 只會說小學生的中文 第一次見到北京的老祖母 獻上什麼禮物呢? 別忘了背一首中國詩 床前 明月光 疑是 地上霜 低頭 思 思 思 全家人都笑了 九十歲的老祖母 笑出眼淚來 用寬的袖口揩著 十八 我走 我跑 我停下來 我走 我跑 我停下來 孩子 你是幹什麼啊? 媽媽 月亮老跟著我 十九 七十歲的中國人 住在赫德森河畔 解開領帶 泡一杯清茶 ──黃河之水天上來 一口鄉音未改 七十歲的中國人 放上電剃刀 顧盼兩鬢的白髮 疑是地上霜的白髮 三千丈 緣愁似個長 七十一歲的中國人 舉頭霜 低頭霜 黃河長 明月鄉 七十二歲 解開領帶 泡一杯清茶 黃河之水天上來 棄我去者不復回 七十三歲 三川雪滿魂飛苦 蜀道之難天梯石棧明月相鉤連 何茫然 催心肝 七十四 七十五 解開領帶 泡一杯黃河 朝如青絲不可留 亂我心者暮成雪 七十六 七十七 拿起電剃刀 斷水水更流 長相思 白雲間 長相思 彩雲間 七十八 黃河三千丈 何處是故鄉 朝辭去 不復回 七十九 白髮黃河天上來 奔流到海不復回 八十 不復回 不復回 黃河 黃河 天上 天上 不復回

2022/12/23
articleCard.readMore

高樓 幻夢 冰

去那意識模糊到與現實交織成如糨糊般粘稠一團的地方,看一看那座冰雪的迷宮。同樣的幻境,同樣的夢。反覆。 在不知何時,單手撐在桌上,光略顯陰暗。今日或許降了雪,冷風吹進暖氣中溫熱的室內。感覺有點悶。那就索性開窗吧!窗外的世界也沒想到那般蕭索了,路燈暗下,也幾乎見不得什麼了,只剩遠處的虛空中高樓頂上還亮著的些許紅光。但我的耳邊聽見了風的牧笛,風的薩克斯,風的交響樂。七月的原野上,遠方傳來的或許也是相似的聲音。日光滲到每一棵小草,給予大地無盡的能量。悠揚的笛聲從遠方飄來,勾引的我的魂魄。 憑窗佇立,看膩的風景,不過空蕩的街道和週而復始的交通燈。這個世界自己在運動。稀疏到幾乎沒有的車流,來到時還會細緻觀察車牌。許久之前,當我還是一個好奇的幼兒,我更會細緻地看,細緻地望,留心每一個細節。那時的光陰,幾乎都無法感知到流去的速度。如今卻今非昔比了,如今滄海卻以變為桑田了。 整日積攢下來的重負,沉重,痠痛,脹在我的腦中,堆積在我的前額,壓下我的雙眉,順著臉龐的線條,刺痛的頸椎,脊椎。之後一股股熱流湧動,我的大臂開始無力,我的小腿失去知覺,我的脊背不能承受而彎曲。痛苦地與疲憊對峙,穿過筋疲力盡的峭壁,來到寧靜與永恆的內心森林,直到握筆都似有些抖動。我如一位即將消逝為塵土的老者,精神即將消散的睡眠的幻夢中。或許夢是現實世界的暫時崩塌。畢竟當你閉上眼睛,當你與現實斷開一切的聯絡,起初還能感知到的呼吸和身體與棉被、床罩和那一系列柔和的有些滑溜溜的東西接觸的感覺,之後又如同肉體抽離了一樣的時候,你已經不再以平常的形式存在了。你不存在,你的精神去了另一個世界,幻想的世界。 眼前已經看不到什麼了,只剩下空洞的靜穆;耳邊不再有音樂,而是無盡的虛空。但我的精神是自由的,可以來往於世界上任何一個角落。我與世界上任何一個人,都建立了一種現實中敢都不敢想,也絕不可能建立的牢固的連結,似乎宇宙大爆炸前就在那裡了。是的,就在那裡。自由穿行,我被賦予了超越人類的能力。我分不清哪個是現實了。 眼前是北京香山的楓葉,紐約時代廣場的雪花,倫敦的泰晤士河上的遊船,香港的高樓。我發現我身處冰塊之中;我發現我以我幻想的樣子存在,一個本屬於我又不屬於我的軀體,一個本屬於我又不屬於我的形態。我發現我回到了生命誕生之前的地方,光產生的地方,回到了萬物起源的神聖殿堂。意識是流動的液體,思緒是漂流的楓葉,蕩起漣漪。在光的照耀下,意識的水中波光粼粼,蕩起水花。每一刻,都有一個人被照亮。他的精神被點亮,他的肉體被點亮,他被點亮。但這不是蘭亭,沒有流觴曲水的娛樂,更不會有古代的隱者。這是我們內心深處的地方,是我們的靈魂所居的地方,是我們自己又不是我們自己。 冰塊閃耀,封印了幾個世紀為被人發覺的勝過世界上任何意見雕塑作品的雕花。冰屋裡,透過晶瑩又朦朧的冰幕,隔絕又無比接近地走進她的生命,他的生命,你的生命,我的生命。我發現我是所有人,我發現我誰也不是。一個人睡著了便不再有記憶了,因而我感覺我從未存在過。是的,眼前是不真實的謊言。《波西米亞狂想曲》中,未曾被賦予生命的願景又何嘗不是一種解脫?可惜生命被賦予那刻,就無法挽回地註定了,註定了你的一切。走出冰屋,來到一邊在永恆之光下流淌不息的意識的河川,每一滴水都是鮮活的生命。這時想起,生命的過程是否是另一種水迴圈?逆流而上,卻發現這條河流綿延。這是一條天河,我永遠也達不到終點。 “嘿!” 背後有人在呼喚我?轉過身去,卻只見到模糊的人影。那是我?是過去的我?未來的我?是存在的我?不存在的我?那是我?冰在融化,幻夢在崩塌,冰屋在崩塌,水流在乾涸……倏地感覺手心中有一股熱流,額上滿是熱汗,眼前景象仍是虛無和現實的交織。雙腿漸漸恢復力量,但是支撐著的手臂已然麻木。這才發覺又回到窗前。 倚在窗上,這次,離外面的世界更近了,離裡面的世界更近了,但這種距離永遠不能量化。看向尋常的,現在又不尋常了。曾經有一段時光,甚至希望在遙遠的 2011年,失手墜落的自己真的墜落了。消失在自己的過往,如同月下漂浮的煙靄,畢竟是毫不痛苦的。如今卻太遲了,已經開啟了一趟前往終結的旅途,路途中想都不敢想那一無所有的大限已至的寧靜。但那也會是可怕的,就像在迷失的 2021,痛苦地徘徊在現實和虛幻,拿一本書連線起精神和肉體,不真實的世界,敵對的世界,變得和善。如果我不曾活過,周圍的一切或許都一如現在一樣運作。非也!至少我也改變了身邊的人吧。活著,生命燦爛。 指著天空,什麼也沒有指向。把窗戶開到最大,把音響上的悠揚的爵士樂開到最大!疾風!暴風!狂風!暴雪!暴雨!吹走我的靈魂!洗滌我的靈魂!救贖我的靈魂!我渴望飛向遠方,飛向平流層,飛出大氣層,飛到外太陽系,飛出宇宙的邊界!我的精神沒有邊界!放下自己的肉體,放下沉重的一切。在夢裡我什麼都能放下,在醒時我什麼都能放下! 但是夢醒了便不記得夢中事了,但是入夢了就不記得真實了。不過夢也無需要一種理由吧。 所以睡一覺,回到那個冰屋,什麼都有了。 所以醒一日,把冰屋放在心裡,什麼都有了。

2022/12/21
articleCard.readMore

RECAP2022: 流星雨

獻給 2022,和那錯過的英仙座流星雨。 什麼時候才能在紛繁中尋回真正的自我?—— 可惜已經失去了光陰。險些忘記了今年是何年。 但是至少 2022 要在我的一生中留下痕跡。 久立窗前,空洞的夜空。清爽的山泉洗滌了我陰鬱中沉淪的靈魂。在父親上高中的地方,綠茵場上,苦苦等待流星雨。據說8月13號夜裡有英仙座流星雨。同一片夜空,同一個世界,不同的時空。那也是我將要作別故鄉的一天。 夜間,清涼的風,在這群山環抱的村落。幾天前,或在山泉水側垂釣,或在縣城夜市趕集。體態輕盈,行走如飛,來去匆匆。漸漸懂得《田園》中的樂趣,那跳動的音符正是我山行的步伐。只可惜那謝公已不再,也固然尋不到那登山的木鞋。村落的小巷邂逅多年前見過的人,停留在虛掩的院門,看那貓偷食吊掛的醃魚。在停電的日子裡,遠山的輪廓在星空中已經變得模糊,但記憶卻變得清晰。燒烤的薰香從千里之外,從數月之前遊蕩入我的鼻腔、口腔,刺激我的味蕾。 或許是在汽車上,看著長江三峽的萬噸貨輪,看碧雲藍天,看千里江山。而在火車上,望著遠去的平原,山谷,那幽靜的小徑和喧鬧的都市,水庫、太陽能板和乾涸的小河。空闊的世界!耳機中流淌出來的柴可夫斯基第一鋼琴協奏曲,遠行的這現代的人類奇蹟!自然奇蹟! 雨水擊打在窗上,夕陽斜照在車站的一角,層雲化身灰暗的三角,指向遙遠的天際。一切都如同離開上海的那天,從颱風逃離。突然告知朋友取消明日的騎車旅行。熟悉的路徑,就和今年二月一樣,也和去年二月一樣。走過蜿蜒的林間小道,沿著黃浦江,直上陸家嘴。世博源的火鍋?不小心用公筷嚐了一口滾燙的牛丸,卻惹你們一笑。冒泡的熱湯,溫暖,深情厚誼在其中昇華。下次當然會更小心!不知不覺竟與過去的你對話,卻不知道如今同學群卻陷入沉寂。畢竟大家都要踏上遠行的征途,何日是歸期? 幾日前,還在剪輯畢業影片。遠端指導錄製、演唱,擺好動作,調整姿態。卻不記得之前是自招、是中考。雷雨的那天,幾乎徹夜未眠,次日早要靠那濃茶。出考場的時候,頭暈目眩。 好在完全沒受影響,甚至超常發揮。收到錄取通知的那天,正從故鄉趕來。計劃未來,卻不知道初進校園,陌生的地方,甚至有些不習慣。從社恐,到如今向外邁出了幾步。新的一年要自信。 夜間逛校園,遠望天鵝湖對岸,確認是否有其他人。雨中漫步,把外套當成雨衣。或許只有我們會傻到這樣做。離譜。 在機房停課學習 OI,一週更五篇部落格。逐漸從疫情復甦的國家,重複的網課。今年三月,突然接到通知回家,卻是欣喜?自以為是認為可以很好利用時間。誰知開啟了摸魚的生活。也罷,不斷調整,欣賞古典音樂,在陽春三月的雨中用最大的音量播放那異國他鄉,來自紐約的爵士樂。又憶起時代廣場霓虹燈中的雪,可久遠的回憶已經變得朦朧。和同學拓展關係,絲毫不覺得臨近分別為時過晚。確實有些遺憾吧。但是返校之後,在清晨、黃昏的網約車上,每一種天空,每一個日子。也曾和司機交談,走近那些不一樣的人生。Life is tough. 沉溺在試卷中,卻清醒地看見明天——我離未來最近的一段時光。和你在網上打卡拍攝天空,可惜現在不知道你身處何方。 十二月的天空,有些許悽清。準備英才計劃,還是剛剛得知 NOIP 一等的訊息。臨近聖誕,或許在某個平行宇宙,在永恆的過去的 2021,還有一些人在夜間的校園長跑。或許望著校門外的燈火,會在寒風中拿出手機,播放那一曲 Merry Christmas,會大聲祝福:”馬上 2022 了,一切會更好的!“ 2021對我是痛苦的,但我確實有所得。 幸運的是,如今也應該會有這樣的聲音。Merry Christmas。 回到山間,記憶中的氣味逼近。是外婆。是外公夜間騎摩托回到山上的家,留下外婆給我們送行。是遠處已經遠到看不見的燈火亮起,親朋好友在農閒時刻歡聚。是院中的雛雞、豬羊。想起那個在農家小院裡,看著天空,亂按計算器的日子。 躺在鄉間某個高中的足球場上,嗅著草香,意識遊離。等待流星雨,痛恨沒把相機帶在身邊。一顆,兩顆,三顆……但只見得滿天星斗,似乎有什麼東西閃過,若隱若無。那天或許沒有流星雨,但眼前的映象模糊了,變成了一團,漸漸看不真切了。 醒來時在上海,2022年,12月,17日。 不知如何評價這一年,但是我將要改變了,但是我確實改變了。希望明年不要再把 2023 錯認成 2022。我存在過,每一年我都存在過。Carpe diem。 2023會更好。 Restart. 至少還會有流星雨。

2022/12/17
articleCard.readMore

清夜

月起晚風拂,螢光碎火流;不知明朝復今夕。 月升,微光照過灰色的十二月的天空。十二月了?可我的意識還停留在那個蟬鳴的六月。 門外有孩童在嬉戲;遠方的地鐵列車飛馳,發出空氣摩擦的空洞聲響;窗外昏黃的路燈仍然用橙黃的光將樹染成懷舊的顏色——這一切,像四月。池塘還是靜穆,那輪明月依舊懸在明朗的夜空。 九天之上或許有天宮?都說天上一日,地上十年。或許仙人也總是逍遙。也能想象,在遠方的故鄉,農夫或許剛剛回到土屋之中,嗅著空氣中帶著的泥土的香味,在冉冉升起的炊煙中休憩。雞犬相聞,微風吹過樹梢,遠山的巨大影子和繁星點點……蜿蜒的山路,還有車輛往來。引擎發出那種低沉的連續的聲響。山腳下,家家戶戶的燈亮起,家家戶戶的燈熄滅…… 但那都不是我。記憶跟不上遺忘的步伐,時間的鄉愁卻見漸變得濃烈。我甚至記不起2021年。多少的過去在時光中淡去,但是那些人和事卻在遺忘中永恆。留不住的。 月光皎潔,星海盪漾,旋轉、流動。高壓線塔的輪廓依稀可辨,卻和夢的幻境交織結合。久遠的夏日,我曾望著遠山,那山的輪廓模糊。或許那是 2013,或許是 2015,和早已消散在時光裡的故人在山間漫步。甚至認為這是前世的記憶。熟悉的面容在時間裡溶解。 一顆明星劃過星野,遠處市中心的燈光依舊燦爛。在回鄉的火車上,我看到粉紅色、橙紅色漸變到深邃的夜的藍。遠行的高鐵列車上,我欣賞著晝的關閉,夜的開啟。群山、平原、天空、城市、鄉村都以讓人眼花繚亂的速度劃過眼前,連成一線。我要離了我的故鄉了。 那是在深秋,操場跑道鋪了一層金黃色的樹葉的地毯。夜空清澈,一群學生在長跑。深紅色跑道、淺黃色的教學樓、深棕色的長椅和一排綠色的松樹,在黑夜中卻顯出一個顏色。校門外的燈火被綠樹的高牆阻隔。但透過升騰著的吸氣、呼氣的霧靄,少年的歌聲嘹亮。如今或許看不到這樣的學生了?當年,這樣的夜晚卻不啻那一天。或許很快,我也終說不去那些人的姓名。 錯亂的時空。 月升,水汽中迷濛的窗,映著我的面容。給我開一壺良藥,復活我逝去的時光。我的過去已死,如果再來,我想我仍會這樣選擇。 也罷,往事如煙。 後記:分了兩天寫,所以有銜接不連貫(

2022/12/12
articleCard.readMore

割點 Tarjan 演算法

最近學習圖論,寫篇題解記錄一下。 定義 對於一個無向圖,如果把一個點刪除後這個圖的極大連通分量數增加了,那麼這個點就是這個圖的割點(又稱割頂)。 這篇部落格主要介紹,Tarjan 演算法用於求 割點。 割點 Tarjan 演算法,記錄 節點訪問時間戳 dfn,節點能夠回溯到的最早的點的時間戳 low。 對於某一點: 如果這個點是根節點,且有兩個以上與它相連的連通分量,那這個點就是割點。 如果某個點後繼是一個連通分量,而這個點不是根節點,那這個點是割點。 第一種情況 我們需要記錄根節點 fa,在遍歷的時候,如果重複訪問到了 fa,說明找到了 fa 下面的一個連通分量。 第二種情況 設當前點 s,對於訪問的下一個節點 y: 如果 low[y] >= dfn[s],說明這個點下面有一個連通分量。 注:這裡類似於 Tarjan 求 SCC 時,我們所做的 min(low[s],dfn[y])。 程式碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 void tarjan(int s, int fa){ dfn[s] = low[s] = ++cc; int child = 0; for(int i = head[s]; i!=-1; i=NDS[i].next){ int y = NDS[i].to; if(!dfn[y]){ tarjan(y,fa); low[s] = min(low[s], low[y]); // 回溯後更新 if(low[y]>=dfn[s] && s != fa) // 情況 2 cut[s] = 1; if(s==fa) // 情況 1 計數 child++; } low[s] = min(low[s], dfn[y]); } if(child>=2&&s==fa){ cut[s] = 1; // 情況 1 } }

2022/12/4
articleCard.readMore

P3147 USACO16OPEN 262144 P 題解

DP 系列。 題面 看題,Luogu 合併相鄰的相同數字,變成數字加一。求獲得的最大值。 思考 最初想到的是基礎的區間 DP,不做解釋: 1 2 3 4 5 6 7 8 9 10 11 12 13 long long ans = 0; for(int len = 2; len<=N; len++){ for(int i = 1; i+len-1<=N; i++){ int y = i+len-1; for(int k = i; k<y; k++){ if(DP[i][k] == DP[k+1][y]){ DP[i][y] = max(DP[i][y], DP[i][k] + 1); } } ans = max(ans, DP[i][y]); } } cout << ans << endl; 但是 $ 2 \leq n \leq 262144 $ ,顯然會 MLE。 最佳化 借鑑思路,我們發現可以使用類似倍增的方法去做。 用狀態 f[i][j] 表示 合成之後結果為 i,右端點為 j 的區間的左端點位置,如果 值為 0 即 不可行。 因為題目要找兩個相鄰相等的區間,合成。有: 1 2 3 f[i][j] = f[i-1][f[i-1][j]]; 把 f[i][j] 拆分成兩個能合成為 i-1 的區間 即 1 2 3 4 5 f[i-1][j] |------<i-1>-----|----<i-1>-----| j f[i-1][f[i-1][j]] 如果 f[i-1][j] 或 f[i-1][f[i-1][j]] 不成立,f[i][j] 就不成立,即轉移為 0 那如何表示結果? 記錄 ans,如果 f[i][j] 可行,就更新 ans。因為 i 遞增,所以不需要 max 操作。 得到 1 2 3 4 5 6 7 8 9 10 for(int i =2; i<=58; i++){ for(int j = 1; j<=N; j++){ if(!DP[i][j]){ DP[i][j] = DP[i-1][DP[i-1][j]]; } if(DP[i][j]){ ans = i; } } } 注意我們倍增合併,所以 log2262144+40=58log_2{262144} + 40 = 58log2​262144+40=58 是可能獲得的最大值。 初始化 顯然不合並是可行的,所以在輸入的時候,初始化 1 2 3 4 5 for(int i = 1; i<=N; i++){ int in; cin >> in; DP[in][i] = i+1; } 關於 i+1:為了避免區間重複,我們 f[i][j] 表示的區間是左閉右開區間,所以右端點是 i+1 小結 這道題是區間 DP 狀態最佳化,DP 學習之路漫漫,還需要多加練習。

2022/11/24
articleCard.readMore

P3354 Riv 河流 題解

最近在練樹形 DP,正好看到 這一道 虛標的紫題,但本蒟蒻不會寫,想出來了便記錄一下 題面 & 思考 先看題面,一顆有根樹,選定 k 個節點作為 ”伐木場“,求運送木料最小費用。注意木料費用是 dis * wood。 最開始想到簡單的樹形揹包,狀態轉移方程: 1 f[i][k] = min(f[j][s] + cost, f[i][k]); 但是注意到,如果某個後代節點如果是伐木場,cost 不需要計算,所以狀態中還需要儲存後代的伐木場情況。 但是後代中鋸木廠不止一個。考慮到樹有唯一的父親,且同一深度祖先唯一,可以記錄最近的伐木場祖先,作為狀態的一部分。 有 f[i][j][k] 即 i 節點 最近的伐木場祖先為 j,後代(不算自己)有 k 個是伐木場。 但是由於 f 自己也可能是伐木場,轉移方程不同,需要分類討論,於是改成:f[i][j][k][0/1] 其中 0 代表自己不是伐木場, 1 表示自己是伐木場。 狀態轉移方程 因為我們需要列舉祖先,在 DFS 時需要記錄 Fa 陣列: 1 2 3 DFS 開始時:fa[++tot] = x; 結束時:tot--; 簡單記錄祖先 stack。 回溯後,對於當前節點 x 和 子節點 y 每個祖先 ff: 1 2 3 4 先對每個 k 賦初值,對於每種伐木場個數 l: f[x][ff][l][0] += f[y][ff][0][0]; --> 當前節點不是伐木場,子節點 y 最近伐木場為 l,對任意 f,賦最大值,即 子結點中沒有伐木場。 f[x][ff][l][1] += f[y][x][0][0]; --> 當前節點是伐木場,子節點 y 最近伐木場為 當前節點,對任意 f,賦最大值,即 子結點中沒有伐木場。 然後就是樹形揹包: 1 2 3 4 f[x][ff][l][0] = min(f[x][ff][l][0], f[x][ff][l-s][0] + f[y][ff][s][0]); --> 當前節點不是伐木場,對 y 節點分配 s 個伐木場個數 f[x][ff][l][1] = min(f[x][ff][l][0], f[x][ff][l-s][1] + f[y][x][s][0]); --> 當前節點是伐木場,對 y 節點分配 s 個伐木場個數 唯一的不同是 y 節點最近祖先為 x 注意在列舉 l 時,由於是01揹包,需要倒過來列舉,不然狀態計算會疊加。(很容易理解) 做完了?好像還差一個 cost! 考慮到 祖先節點 ff cost 的貢獻,因為是 當前節點 W 乘以 到 ff 的總距離! 需要計算總距離,維護 dep 陣列,即當前節點到根節點距離,即可!很容易理解。ff 到 x 的距離就是 dep[x] - dep[ff] 對於當前節點 x 每一個祖先 ff: 1 2 3 4 5 6 7 8 if(l>=1){ --> 因為 l 需要 -1 ,要分類討論。 f[x][ff][l][0] = min(f[x][ff][l][0] + W[x] * (dep[x]-dep[ff]), f[x][ff][l][1]); 合併 0 和 1,因為回溯之後,1 的狀態不再被使用,便於下一步計算。 -1 是因為上文 f[x][ff][l][1] = min(f[x][ff][l][0], f[x][ff][l-s][1] + f[y][x][s][0]); 時,s+l-s = l 但是當前節點也是伐木場,所以狀態更新是 l+1 的,要 -1 獲取正確結果 }else{ l = 0 時,當前節點不可能為伐木場,直接新增到 ff 增加的貢獻 f[x][ff][l][0] += W[x] * (dep[x]-dep[ff]); } 這裡再來聊一聊合併。為了便於討論,上文揹包轉移的時候,我們沒有考慮 y 的 1 的情況,而是在每次 y 回溯時 合併 0 和 1。這有點類似於滾動陣列?回溯後 0 不再表示之前的意義,而是我們最初設計的狀態:i 節點 最近的伐木場祖先為 j,後代(或自己)有 k 個是伐木場! 程式碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 #include <cmath> #include <cstdio> #include <iostream> #include <cstring> using namespace std; struct Node { int to, next, d; } NDS[201]; int head[201], vis[201]; int cnt = 0; long long W[201]; int tot = 0; void add(int a, int b, int d) { NDS[cnt].to = b; NDS[cnt].next = head[a]; NDS[cnt].d = d; head[a] = cnt++; } int fa[201]; long long f[201][201][52][2]; long long dep[201]; long long n, k; void dp(int x) { fa[++tot] = x; vis[x] = 1; for (int i = head[x]; i != -1; i = NDS[i].next) { int y = NDS[i].to; if (vis[y]) continue; dep[y] = dep[x] + NDS[i].d; dp(y); for (int j = tot; j >= 1; j--) { int ff = fa[j]; for (int l = k; l >= 0; l--) { f[x][ff][l][0] += f[y][ff][0][0]; f[x][ff][l][1] += f[y][x][0][0]; for(int s = l; s>=0; s--){ f[x][ff][l][0] = min(f[x][ff][l][0], f[x][ff][l-s][0] + f[y][ff][s][0]); f[x][ff][l][1] = min(f[x][ff][l][1], f[x][ff][l-s][1] + f[y][x][s][0]); } } } } for (int j = 1; j <= tot; j++) { int ff = fa[j]; for (int l = k; l >= 0; l--) { if(l>=1){ f[x][ff][l][0] = min(f[x][ff][l][0] + W[x] * (dep[x]-dep[ff]), f[x][ff][l-1][1]); }else{ f[x][ff][l][0] += W[x] * (dep[x]-dep[ff]); } // cout << x << " "<<ff << " "<< l << " " << f[x][ff][l][0] << " " << endl; } } // cout << dep[x] << endl; tot--; } int main() { // Type your code here memset(head, -1, sizeof(head)); cin >> n >> k; for (int i = 1; i <= n; i++) { int w, v, d; cin >> w >> v >> d; W[i] = w; add(i, v, d); add(v, i, d); // 鏈式前向星 } dp(0); cout << f[0][0][k][0] << endl; // 輸出結果,注意是 合併後,所以是 0 return 0; }

2022/11/23
articleCard.readMore

馬拉車演算法

最近在學習馬拉車演算法,簡單記錄一下心得。(如有疏漏,請指出 先看 模板題 ,要求最長迴文串的長度。 首先思考樸素演算法,顯然是 O(n3)O(n^3)O(n3) ,無法透過。而馬拉車演算法能將時間複雜度最佳化到 O(n)O(n)O(n)。 性質 對於一個迴文字串,必然有一個對稱中心,在對稱中心兩側的部分均全等。 一個迴文字串對稱之後得到的一定也是迴文字串 即 aba x aba 但是對於奇數、偶數長度的迴文字串,這個對稱中心可能是字元,也可能在兩個字元中間。 如:ab | ba, a b a 所以考慮,在兩個字元中間都插入隔板 #,即 abba 變成 #a#b#b#a#。原長度為 n 的字串,增加 n+1 隔板,長度變成 2n+1 必然是奇數,方便統計。 思路 馬拉車演算法,即記錄一直最長迴文子串區間,對樸素演算法進行最佳化。 因為是樸素演算法兩側向外拓展,隔板對判斷迴文無影響。p[i] 陣列儲存以 str[i] 為對稱中心的迴文字串半徑長度。半徑長度中計算了隔板個數,所以得到的就是迴文字串長度。 1 2 3 4 5 6 7 8 9 10 11 for(int i = 0, l = 0, r = -1;i<s2.size(); i++){ int k = (i>r)? 1 : min(p[l+r-i], r-i+1); while(0 <= i-k && i+k<s2.size() && s2[i-k]==s2[i+k]){ k++; } p[i] = --k; // 注意最後一次迴圈會對加上一個 1 if(i+k>r){ l = i-k; r = i+k; } } 先記錄 i,列舉每個對稱中心,記錄 l 和 r 即目前最長迴文字串的左右端點。k 則是目前半徑長度。 演算法主體就是樸素演算法,向左右兩端拓展。 考慮最佳化。如果i處在一個迴文子串中,因為對稱性,可以得到與i對稱的點j的最長迴文串長度。因為i不斷增加,對稱的點的座標一定小於i即已經更新過。 由於上文推斷的性質,迴文子串可以對稱得到。注意單個字元也可以考慮成一個迴文子串。所以 k 可以從 之前的 p[j] 開始計算。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 (設 x 表示 [l,r] 對稱中心,j 就是 i 關於 x 的對稱點) 1. 案例1 l r #a#b#b#a# ^ ^ ^ j x i 2. 案例2 l r #a#b#a#b#a#b#a# ^ ^ ^ j x i 手推方便理解 然後考慮如何計算 j,因為中點座標公式,得到 l+r2=i+j2=mid\frac{l+r}{2} = \frac{i+j}{2} = mid2l+r​=2i+j​=mid ,得到 j=l+r−ij = l+r-ij=l+r−i。 但是因為確定區間,且偶數情況存在。所以還應判斷 j 是否在區間內。 而如果不在區間內,很顯然應該從1開始列舉。 所以得到: 1 int k = (i>r)? 1 : min(p[l+r-i], r-i+1); 每次更新 l r 區間長度即可。 AC Code 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 #include <cstdio> #include <iostream> #include <string> using namespace std; int p[110000001]; int main() { // Type your code here string s1; cin >> s1; string s2; s2.resize(s1.size()*2+4); s2[0] = '~'; s2[1] = '#'; for(int i = 0, j=2; i<s1.size(); i++, j+=2){ s2[j] = s1[i]; s2[j+1] = '#'; } int ans = 0; for(int i = 0, l = 0, r = -1;i<s2.size(); i++){ int k = (i>r)? 1 : min(p[l+r-i], r-i+1); while(0 <= i-k && i+k<s2.size() && s2[i-k]==s2[i+k]){ k++; } k--; p[i] = k; if(i+k>r){ l = i-k; r = i+k; } ans = max(ans,p[i]); } cout << ans << endl; return 0; } 小結 馬拉車演算法實用且容易理解,但熟練掌握還需要練習。 作業 | 最長雙迴文串

2022/11/20
articleCard.readMore

夜雨

太久沒更部落格了,別來無恙 2020.11.16 日夜雨有感,18日記 夜雨一直下,下在這小城的一隅。 從廣場走出,漆黑一片,不遠處卻是車水馬龍,彷彿隔了一個世界。雨傘下,近乎看不清的霓虹漸明瞭。但此刻我只見到微光下如絲的雨和它有節奏地擊打在傘面上的聲音。 這是冬日的雨,路旁佇立,晶瑩的雨點散在大衣的絨毛上,形成了彩色的淚珠。我知道這和去年此刻的景一樣。佇立,長嘆,冉冉升起的白色霧靄是對這喧囂的叛逆。 風起,微寒,但雨下如同淚漣漣,不知何時終了。 上了車,向車後望去,移動的燈火與車流、人流,在夜的深邃中,形成紅色的流動的絲綢飄帶。車窗外的世界總是呈現懷舊的昏黃,而這生活的千篇一律著實讓人麻木。在自己的熱愛織成的繭中窒息,在對舊日的追憶中迷失。西西弗斯式地在單調中沉淪,無可救藥,我親手造成了自我的慢性自殺。 腦海中湧動的昨日,如同白紙,毫無意義。微寒,放鬆,我似乎又找回了去年此刻的心境。身體卻是輕盈,御風而行,泠然善也。我知道過去成就今日,而此刻我仍然陷入迷茫卻清醒的疊加態,是迴圈往復的輪迴的孤獨。 窗外的雨一如去年、前年、漸漸被忘卻的過往,落地就無法挽回的過往。那些,我已經找不到了,於是只能嘆息。這是人生最大的 BUG,過去的就不能再來,經歷就是一種失去。 人生。 改變? 一切盡在車窗外夜雨的緘默裡。 而我。 我還是我嗎? 這些話,何止今日說起過呢? 換一種思維方式,換一種生活,換一種人生,突破過去的繭,重獲新生。 人生不再千篇一律。真正的自我解放。 但夜雨不會停。因為沒有辦法讓它停下來。 但天亮了就會是新的一天。

2022/11/18
articleCard.readMore

層霧

花非花,霧非霧。夜半來,天明去。來如春夢幾多時,去似朝雲無覓處。 今晨,上海下著一場大雨,天色陰沉。窗上蒙了一層水汽,更遠處便是層層霧靄之下的昔日無比熟悉的河川景緻。 出門。雨敲擊在雨傘上發出有節奏的聲音,四周也有些許走著的行人。但這些都離我太遠了。雨帶給我的只有那本就屬於我的失意。 回想 2020 到現在,時光匆匆,幾乎不能感知時光的流逝。但它終究還是溜走了!而且似乎在一瞬間!舊日的一幕幕,如今還在疾速流去的時光之河。我想讓它停下來,是的一定要停下來。反覆思索,琢磨,這樣在低沉中的迴圈是否是生命的本質,詰問我們是否真的有能力改變我們的未來,改變命運的走向。或許掌握僅僅是自我麻醉。我們或許從未掌控過什麼。也許我們僅僅是回憶,也許我們從未存在,也許我們存在過,也許眼前的一切都是虛幻。沒有人知道答案。 層霧遮蓋住眼前的道路,看向四周的人。我總覺得陌生,而無所適從,總覺得不安。畢竟在層霧中的人,找不到方向。我好像從來都沒有得到內心深處的滿足,從未有過,在短暫的逝去的歲月裡。未來也不會有吧?畢竟我們沒有能力決定我們的來去,甚至連自己最基本的屬性都不能決定,沒有能力。我們的開場都是天命。 到了。站立在燈光微弱的走廊裡,四周是一片空虛。時間太早了,沒有什麼人。 人生是這樣嗎?逝去的歲月是這樣嗎? “花非花,霧非霧。夜半來,天明去。來如春夢幾多時,去似朝雲無覓處。” 也許是我沉溺在對無意義的追索中,或者僅僅是過去的痛苦太愉快了,又或者是我已經被單調麻醉?生活最初的樣子? 忽然來了兩個孩童,蹦起來按下電燈開關。 眼前一片明朗,就像昨日的陽光。 ”也許明天也是這樣。“ 誰知道呢?但是 C’est la vie. 層霧,……

2022/11/11
articleCard.readMore

從愚人節玩笑到真的玩笑(bushi): 淺談 lsnotes

離了個大譜 (不過這個想法挺好的 想法 愚人節本來是開玩笑的,結果發現,挺有用。笑死 前幾天和朋友討論,說要給每個目錄加個提示語,ls pwd cd 這些命令時顯示出來。 e.g.: 1 2 3 4 $ ls The place where I code! --- Demo OI Playground Projects 這樣就解決了,目錄那麼多,自己甚至都忘了這個幹啥了的情況。當然,還有很多其他的用法,比如可以在 .lsnotes 中顯示 build 方式。 概況 lsnotes 是個水專案,目前只支援純文字顯示(未來會加上 Markdown 解析,短時間內先鴿了。 就目前的程式碼量而言,小學生都會寫… 後續 MrWillCom 又做了一個更強的 lsnotes 即 lnn。 這個專案功能簡單,但實用,未來不怎麼會維護了。

2022/4/15
articleCard.readMore

題解 紀念品分組

快要 CSP-S2 了,複習一下一些演算法(弄文化課弄了好久了,很多東西都要忘了。。。 讀題 題目連結: Luogu P1094 看題,把購來的紀念品根據價格進行分組,但每組最多隻能包括兩件紀念品。看樣子,應該是貪心。每次取一個大的,一個小的,就可以保證了。如果大的小的組合起來,超過了最大值,就只取大的。 解題 很容易就可以寫出程式。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <algorithm> #include <cstdio> #include <iostream> using namespace std; int ar[30001]; int main() { int n, w; cin >> w >> n; for (int i = 0; i < n; i++) cin >> ar[i]; sort(ar, ar + n); int ans = 0; int l = 0; int r = n - 1; while (r >= l) { if (ar[l] + ar[r] <= w) { l++; r--; ans++; } else { r--; ans++; } } cout << ans; return 0; } 證明 可以參考這個 結語 能力有限,如有疏漏,請諒解並補充,謝謝。

2021/10/1
articleCard.readMore

題解 導彈攔截

快要 CSP-S2 了,複習一下一些演算法(弄文化課弄了好久了,很多東西都要忘了。。。 讀題 題目連結: Luogu P1020 資料是這樣的:389 207 155 300 299 170 158 65 推一推,我們發現,389 300 299 170 158 65是第一問的答案;在 155->300的時候,要使用另外一顆炮彈了,故最少應該配備兩套。 在草稿紙上面推一推,不難發現,這道題的兩小問,就是讓你求一個不上升序列長度和一個上升序列長度。 預備知識 在求這兩個東西之前,需要先學習STL中的兩個函式:lower_bound upper_bound。 其中, lower_bound 是求序列中第一個大於等於某個數的數;upper_bound 是求序列中第一個大於某個數的數。這兩個函式返回的是指標。 它們使用的前提是 序列是有序的。 以lower_bound為例,具體用法類似: 1 2 3 int a[100]; // ... lower_bound(a, a+len, x); // 前兩個引數傳入的都是指標 這一函式預設是求升序序列中符合條件的數,如果要改為降序序列,則需要一個 cmp 函式,這一點類似於 sort。比如: 1 2 3 int a[100]; // ... lower_bound(a, a+len, x, std::greater<int>()); // 前兩個引數傳入的都是指標 解題 我的 AC 程式碼。 本題有兩種演算法,O(n)O(n)O(n),O(nlogn)O(nlogn)O(nlogn)。這裡只講後者。 不上升序列 我們用陣列d1當作棧來儲存它。 遍歷導彈高度,把棧頂元素和高度比較: 若 ai<=dlena_i <= d_lenai​<=dl​en,此時aia_iai​是符合要求的,直接入棧。 若 ai>dlena_i > d_lenai​>dl​en,此時aia_iai​把棧內第一個小於它的覆蓋掉。這裡說一下,能夠覆蓋它是因為我們不需要再訪問它的值了。在測試資料中,此後幾次都會進行這一操作,如果僅僅取這幾個資料,最長不上升子序列長度仍然正確; 取所有資料,幾次操作之後,就會執行操作1,結果仍然正確。 上升序列 和不上升序列一樣,只不過把 upper_bound 改為了 lower_bound,因為會出現兩個相同高度的導彈的情況,這兩個導彈僅僅需要同一的炮彈去攔截。 程式碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <algorithm> #include <iostream> using namespace std; int ARR[100001]; int n = 0; int d1[100001]; int c1 = 0; int d2[100001]; int c2 = 0; int main() { while (cin >> ARR[n]) n++; d1[0] = ARR[0]; d2[0] = ARR[0]; for (int i = 1; i < n; i++) { if (ARR[i] <= d1[c1]) d1[++c1] = ARR[i]; else { int j = upper_bound(d1, d1 + c1, ARR[i], greater<int>()) - d1; d1[j] = ARR[i]; } if (ARR[i] > d2[c2]) d2[++c2] = ARR[i]; else { int j = lower_bound(d2, d2 + c2, ARR[i]) - d2; d2[j] = ARR[i]; } } cout << c1 + 1 << endl << c2 + 1 << endl; return 0; } 結語 能力有限,如有疏漏,請諒解並補充,謝謝。

2021/10/1
articleCard.readMore

如何高效使用搜尋引擎

這篇文章已經在 GitHub 開源了。 最近遇到一些人不太會用搜尋引擎,就寫了這篇文章。。。有不對的地方,或者要補充的地方希望大家踴躍評論、PR。 目錄 簡介 認識搜尋引擎 搜尋引擎的代表 如何高效檢索 言簡 意駭 高階技巧 一舉多得 讓搜尋引擎為你做減法 我只想看這一個網站怎麼辦 限定檔案格式 限定網址 限定標題 告訴搜尋引擎搜尋的是作品 告訴搜尋引擎不要拆開關鍵詞 推薦閱讀 特別鳴謝 參考資料 簡介 最近我經常遇到一些同學,經常跑來問我一些很明顯在搜尋引擎上面就可以搜到的問題。我問他們為什麼不去網上搜,卻發現其實他們搜過,只是效果不好:有些人直接把一整個問句打上去了,還有人漏關鍵字。這些都是他們沒有掌握搜尋引擎使用的精髓。所以,我寫了這個教程,幫助解決搜尋引擎的使用問題。 認識搜尋引擎 我這裡說的搜尋引擎是指網路搜尋引擎。下面給出維基百科中的定義: 網路搜尋引擎(英語:web search engine)是設計在全球資訊網上進行搜尋,意思是指自動從全球資訊網蒐集特定的資訊,提供給使用者進行查詢的系統。 搜尋引擎的工作原理大致可以分為蒐集資訊、整理資訊和接受查詢。為了使文章簡單,更深入的知識我在這裡就不展開了,本文以如何使用為主。 搜尋引擎的代表 百度 必應 搜狗 360搜尋 谷歌 Yandex 還有學術型搜尋引擎: 百度學術 谷歌學術 如何高效檢索 我認為,高效檢索只需要注意:言簡、意駭 言簡 言簡,顧名思義,就是讓你的搜尋儘可能更簡單。在搜尋的時候,應該對問題提取關鍵詞。比如,我想要知道高錳酸鉀製取氧氣的文字表示式是什麼時,我僅僅需要寫高錳酸鉀製取氧氣 文字表示式 即可;同樣的,在查詢怎麼寫檢討書時,甚至只需搜尋檢討書;在想知道《將進酒》是誰寫的,只用查詢將進酒 作者。如果不確定要搜尋的內容名稱,應該做更少的準確限定,換為更零散的關鍵詞。如果是英文,則應去除複數和第三人稱複數。雖然某些搜尋引擎在進行查詢的時候會自動去除掉 「xxx的xxx」這種詞彙和標點符號,但在搜尋時還是應該儘量減少他們的使用。也儘可能不要出現 「如何」「怎樣」這些疑問詞,更不需要和搜尋引擎講禮貌——不要出現類似「請問寒假什麼時候放假?」而是寫成「寒假放假時間」。 意駭 意駭,就是把你的搜尋描述的儘可能完備。不要一昧地追求簡單,在有些情況下還是需要給出限定的:比如,在搜尋蘋果時,如果你指的是水果的蘋果,那為了提升效率,最好寫蘋果 水果;同理,想要搜尋蘋果公司,則應該寫蘋果 公司,這可以簡化為蘋果公司。如果不進行限定,有可能搜尋結果第一頁都會被此關鍵詞的其他意思所充滿,在搜尋時,就會降低效率。 更加高階的限定方法,請見高階技巧。 高階技巧 一舉多得 如果我在搜尋的時候,想同時獲得多個關鍵詞的結果,我可以使用 |,即“或”。注意:用|分隔關鍵詞的時候,一定要加上空格,|必須要是半形的,舉個例子:東京 | 塔。 讓搜尋引擎為你做減法 如果我在搜尋的時候,不想獲得某個關鍵詞的結果,我可以使用 -,即“減”。注意:用-,一定要在之前加上空格,舉個例子:蘋果 -水果。 我只想看這一個網站怎麼辦 如果我只想獲得某個網站的結果,可以在整個搜尋之前或之後加上 site:xxx網站,比如:site:blog.aeilot.top 數碼。 限定檔案格式 如果我只想搜尋一種格式的檔案,可以在整個搜尋之前或之後加上 filetype:xxx網站,比如:filetype:pdf 物理課本。不指定檔案型別搜尋檔案時,可以使用filetype:all。 限定網址 如果我想讓我的搜尋結果的網址都包含某一字斷,可以加上 inurl:xxx,比如:inurl:pan PPT模版,就可以找到網址帶pan的所有相關結果。 限定標題 如果我想讓我的搜尋結果的標題都包含某一字斷,可以加上 intitle:xxx,比如:intitle:免費 PPT模版,就可以找到標題帶免費的所有相關結果。 告訴搜尋引擎搜尋的是作品 如果我搜尋的是作品,如文學作品、藝術作品,可以加上書名號,比如:《星際穿越》。 告訴搜尋引擎不要拆開關鍵詞 如果我不想要拆開搜尋的關鍵詞,可以加上引號,比如:“九年級化學“ 推薦閱讀 知乎 - 如何高效地使用搜尋引擎 特別鳴謝 MrWillCom 他對我的創作進行了指導。 Cathy Aeilot 她啟發了我寫這份教程。 參考資料 網路搜尋引擎 – 維基百科

2021/8/27
articleCard.readMore

用 GitHub Actions 格式化 C/C++ 程式碼

我的 ProblemSet 專案,每次都 Format 太麻煩了,需要一個自動化 Format 的功能。利用 GitHub Actions,即可實現。 GitHub Actions GitHub Actions 是 GitHub 推出的持續整合服務,最近不要錢了,用(白嫖)的人就多起來了。 程式碼 直接上程式碼: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 name: "Check Clang Format" on: [push, pull_request] jobs: format: name: "Run Clang Format" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: "Install clang-format" run: | sudo apt-get update sudo apt-get install clang-format-10 - name: "Format Codes" run: clang-format-10 -style=file -i */*/*.cpp - name: Push changes uses: actions-go/push@v1 with: author-name: Clang Format Bot commit-message: Run clang-format 後記 如果大家有什麼改進的好方法,可以在下方評論!

2021/8/20
articleCard.readMore

四季的天空

使用單反拍攝,Louis Aeilot 版權所有,所有圖片都未經過後期處理,僅經過壓縮。 拍攝用時1年,就是每次看到了都會去拍一張。

2021/7/19
articleCard.readMore

洛谷 7 月月賽 Div.2 總結

昨天(7/14)參加了洛谷的 7 月月賽,感覺題目挺新穎的,就是資料有點。。。 比賽連結:這裡 我的程式碼在 這裡 A 乍一看好像沒思路,但是想一想,其實很簡單,就是會有幾種情況: 0個空格 全是數字的話,需要判斷一下 A. 兩者完全相同 B. 非A,情況 A 是可以輸出 Yes 的。 1個空格 這一種情況,從原理上分析,“選擇一個有正整數的格子和一個與之相鄰的空格子,將正整數移到那個空格子中”,可以得出,其實只要順時針轉一圈,初始和最終狀態順序相同,就符合條件。所以,只需要做三次判斷即可,簡單粗暴(在座的各位大佬要是有高階方法可以通知一下🙇)。 2、3、4個空格 這幾個情況,經過分析會發現,不管如何,都是滿足條件的。 所以,這樣分析下來,很簡單就 AC 了,拿到 100 分! B 這道題。。。無語了。。。測試資料透過了,結果測評資料每次都卡一個,萬惡的捆綁測試。。。最後得不了 100 分了。。。 這道題最開始想的是貪心演算法,排序,但是會超時,就改了一下:開兩個陣列,不管順序,只管符號,放進去。這樣做基本上不超過 10ms。 原理就是,計算 +正數 +負數 *正數 *負數 出現的個數,進行排列,最後計算結果。 C 因為學業原因,離開了幾個小時,回來了之後都沒時間做了。。。有點思路,但沒寫完。。。 其實,可以根據數第一次出現的位置判斷從左、右數的位置,最後進行排列即可。 D 看了一眼就懶得寫。 小結 等題解吧!感覺題目很有意思,都是些需要仔細思考的題目。(奈何我太弱了。。。太弱小了!!!

2021/7/15
articleCard.readMore

題解 最近公共祖先 (LCA)

好久沒刷題了,複習一下:LCA。 題目詳情 題目很簡單,就是求多叉樹兩個點的最近公共祖先。 連結: 洛谷 P3379 LCA(Least Common Ancestors),即最近公共祖先,是指在有根樹中,找出某兩個結點u和v最近的公共祖先。 ———來自百度百科 圖中,4 和 3 的 LCA 就是 1。 解題 最簡單的方法 (暴力) 這種方法資料一大就會TLE。 原理很簡單,讓兩個數一個一個向上走,直到兩個數相遇。第一次相遇就是他們的 LCA。 很簡單,就不贅述了,直接上程式碼 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 #include <cmath> #include <cstdio> #include <iostream> #include <vector> using namespace std; #define MAX 500000 vector<int> tree[MAX];// 以鄰接表形式儲存 int dep[MAX]; int fas[MAX]; namespace M1 { void dfs(int x, int fa) { if (fa != -1) { dep[x] = dep[fa] + 1; } fas[x] = fa; for (int i = 0; i < tree[x].size(); i++) { if (fas[tree[x].at(i)] == -2) M1::dfs(tree[x].at(i), x); } } int solve(int a, int b) { while (1) { if (a == b) { return a; } else if (fas[a] == fas[b]) { return fas[a]; } else if (fas[b] == a) { return a; } else if (fas[a] == b) { return b; } int da = dep[a], db = dep[b]; int delta = abs(da - db); if (da > db) { for (int i = 0; i < delta; i++) { a = fas[a]; da = dep[a]; } } else if (da < db) { for (int i = 0; i < delta; i++) { b = fas[b]; db = dep[b]; } } else { a = fas[a]; da = dep[a]; } } return -1; } }// namespace M1 bool first = true; int LCA(int a, int b, int r) { if (first) { M1::dfs(r, -1); first = false; } int res; res = M1::solve(a, b); return res; } int main(int argc, char *argv[]) { int m, n, s; cin >> m >> n >> s; for (int i = 0; i < MAX; i++) { dep[i] = 0; fas[i] = -2; } for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; tree[x].push_back(y); tree[y].push_back(x); } dep[s] = 0; fas[s] = -1; for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; int res = LCA(a, b, s); cout << res << endl; } return 0; } 注意這道題目的資料輸入,x y 表示x 結點和 y 結點之間有一條直接連線的邊(資料保證可以構成樹)。 所以需要用鄰接表的形式,表示多叉樹。 倍增法 這個演算法是對上面暴力演算法的最佳化。這個演算法的時間複雜度為O(nlogn)O(nlogn)O(nlogn),已經可以滿足大部分的需求。 上述演算法中,一步一步跳太慢了,這裡我們事先做好標記,就可以每次 2i2^i2i 步向上跳,一直到相遇。 程式碼中有較為詳細的註釋: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 #include <cmath> #include <cstdio> #include <iostream> #include <vector> using namespace std; #define MAX 500001 // 本題最大資料規模 #define MUL_MAX 22 vector<int> tree[MAX]; // 以鄰接表形式儲存 int dep[MAX]; // 預處理儲存節點深度 int fas[MAX][MUL_MAX]; // 儲存 x 節點上面第 2^i 次方個祖先 bool first = true; // 記錄預處理是否結束 int lg2(int x) { return log(x) / log(2) + 1; // 無理數計算記得加上 1 來避免誤差 } // 手動寫了一個函式,來求 log2(x) void dfs(int x, int fa) { // x 是當前節點,fa 是它的父節點 if (fa != -1) { dep[x] = dep[fa] + 1; // x 的深度就是它的父節點加一,這很好理解 } fas[x][0] = fa; // x 節點的第一個父節點是 fa for (int i = 1; (1 << i) <= dep[x]; i++) { // 迴圈直至 2^i 大於當前節點深度,即完成當前節點到樹根的所有可跳轉到的節點的預處理工作 fas[x][i] = fas[fas[x][i - 1]][i - 1]; // 這一步是演算法的精髓 // 得到狀態轉移方程,動態規劃計算 // 意思是x的2^i祖先等於x的2^(i-1)祖先的2^(i-1)祖先 } for (int i = 0; i < tree[x].size(); i++) { if (tree[x].at(i) != fa) dfs(tree[x].at(i), x); // 鄰接表儲存當前節點所有相連的節點,只要節點不是它的父節點,即節點是它的子節點,就進行下一步遞迴 } } // 深度優先搜尋來預處理一下 int solve(int a, int b) { if (dep[b] > dep[a]) swap(a, b); // 確保 a 的深度更深,避免冗餘的判斷 while (dep[a] > dep[b]) { a = fas[a][lg2(dep[a] - dep[b]) - 1]; // a 向上跳,跳至兩節點同級 } if (a == b) return a; // 若此時兩節點相遇,就可以直接返回。否則兩節點還需再次向上跳。 for (int i = lg2(dep[a]); i >= 0; i--) { if (fas[a][i] != fas[b][i]) { a = fas[a][i]; b = fas[b][i]; } } // 從可跳到的最高處向下列舉,得到 LCA return fas[a][0]; // 返回答案 } int LCA(int a, int b, int r) { if (first) { dep[r] = 0; fas[r][0] = -1; dfs(r, -1); first = false; } int res; res = solve(a, b); return res; } int main(int argc, char *argv[]) { int m, n, s; cin >> m >> n >> s; for (int i = 0; i < MAX; i++) { dep[i] = 0; for (int j = 0; j < MUL_MAX; j++) fas[i][j] = -2; } // 陣列初始化 for (int i = 1; i < n; i++) { int x, y; cin >> x >> y; tree[x].push_back(y); tree[y].push_back(x); // 鄰接表存入資料 } for (int i = 0; i < m; i++) { int a, b; cin >> a >> b; int res = LCA(a, b, s); cout << res << endl; } return 0; } 其實還可以預處理出一個 lg 陣列,避免對數計算,大家可以自己去嘗試,會有一定時間最佳化效果。 無法理解倍增?這裡有個 經典資料 其他方法 實際上還有更快的方法求這道題的答案。倍增演算法已經可以滿足需求,就不再往下寫了。 Tarjan ST 演算法 大家有興趣可以去嘗試一下。 後記 這裡放上兩種列出演算法的評分。 暴力 倍增

2021/4/24
articleCard.readMore

用簡單的物理方法證明牛頓萊布尼茨公式

前幾天經過苦思冥想,想出來了一種簡單的證明方法,現在來簡單分享一下。 關於牛頓萊布尼茨公式 即微積分基本定理。 ∫abf(x)dx=F(b)−F(a)=F(x)∣ab\int_a^b f(x)dx = F(b) - F(a) = F(x)\bigg|_a^b∫ab​f(x)dx=F(b)−F(a)=F(x)∣∣∣∣∣​ab​ 其中,F(x)F(x)F(x) 為 f(x)f(x)f(x) 原函式。 構思 有初中物理知識可以知道,在 v-t 影象中,面積就是位移,兩點間面積之差就是時間差的積分。 所以很容易證明出這一定理。 證明 所以 v-t 函式的原函式就是 s(t) 函式,所以 [a,b] 區間內的位移就是 s(t)∣abs(t)\bigg|_a^bs(t)∣∣∣∣∣​ab​ 而上文已經得出 v(t) 函式在 [a,b] 區間的積分也指的是位移。 所以很容易得出要證明的結論。 總結 本人僅是業餘愛好進行學習,若有不足,請指正。

2020/12/25
articleCard.readMore

簡評榮耀手環6

前幾天買了個 榮耀手環6,簡單點評點評。 優點 螢幕在一般的手環裡算是比較大的,比較全面屏,顏值也挺高的。 續航把類似自動運動監測、心率檢測、壓力檢測之類的開啟了之後都有一個星期的續航。 離線支付寶、NFC真的很方便。 etc 不足 錶盤,說實在的,真得沒有小米的好看,還不支援自定義,希望以後能適配。話說。。。這設計者怕不是喝了假酒(bushi)。 非華為系手機用不了一些特別功能,比如心臟健康什麼的,不過可以破解,自行搜尋。 24小時血氧檢測這個功能是不支援的。 期望 第一次買華為系的產品(不過現在榮耀已經分離了),感覺還挺好。 就是希望官方能夠加強自定義功能和對於非華為手機,特別是蘋果手機的適配。 總結 這一款產品還是非常值得買的,當然,這裡不建議蘋果使用者購買NFC版本,不然很多功能無法使用。 價格:標準版 249,NFC 289

2020/12/25
articleCard.readMore

海上生明月,天涯共此時。

使用單反拍攝,Louis Aeilot 版權所有。鏡頭不夠。。。。。。

2020/10/1
articleCard.readMore

我為什麼重新拿出了 iPod

我有一個 iPod Shuffle,已經吃灰多年了。。。最近重新拿出來用了。 事情的起因 我訂閱了 Apple Music,挺不錯的,只是在 Phone,Mac 這些裝置上,不管多麼優美悅耳的音樂都成了使用裝置的背景音樂。。。無法讓人專心聽完一首歌曲。我們似乎已經忘記之前沒有這些東西時,沉靜在一首歌曲中的感受了。 我拿出了這款 iPod Shuffle,有點掉色,卻不影響使用。感謝,它讓我找回了之前發現一首好歌、聽完一首好歌的激動、欣喜與感動!閉上眼睛,靜下心來,享受著屬於你自己的餘音繞樑的樂曲。 音樂來源 有人要問了,你的樂曲從哪裡來的?眾所周知,現在所有的平臺都有 DRM… 我購買了網易雲音樂包,8元一個月,然後使用這個 Doge。 結語 實際上 iPod 還有一個好處,就是小巧 方便帶到學校去,好藏。

2020/9/11
articleCard.readMore

Swift 中的 SharedPreferance —— UserDefaults

從 Android 開發又最終回到 iOS 了,好多東西都不知道。最近一直有需求要用一個類似 Android 的 SharedPreferance 的東西。找了一下資料,來總結一下。 它是什麼 不會吧?不會還有人不知道 SharedPreferance 吧??? SharedPreferance 是一種輕量級的 Android 儲存API, 用於儲存簡單的資料,資料多了就不如其他方式高效了。 iOS 中,起同樣作用的東西,叫 UserDefaults。 這兩者都以 key-value 的形式儲存。 使用場景 簡單資料 簡單資料 簡單資料 複雜資料建議使用 SQLite 或者 Core Data,不建議作死。。。 Quick Start 直接上程式碼,裡面註釋我都寫好了。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 let defaults = UserDefaults.standard // 獲取全域性的 UserDefaults defaults.set(Int.max, forKey: "int") // 儲存 defaults.integer(forKey: "int") // 讀取 defaults.set(true, forKey: "bool") defaults.bool(forKey: "bool") defaults.set(Double.infinity, forKey: "double") defaults.double(forKey: "double") defaults.set(Float.infinity, forKey: "float") defaults.float(forKey: "float") 預設值 Float, Int, Double 的預設值都是 0;Bool 的預設值是 false。 更多 大家可以多在 Playgrounds 裡面試試,還可以看看 Apple Developer Documentation。

2020/8/24
articleCard.readMore

凝視那一輪明月

隨便亂寫的,,,要拿去參加一個小比賽。。。 初夏,深夜,我卻還在熬夜趕作業。 夜裡,恍惚著,偶然向窗外望了望。多完美的一輪明月啊! 凝視這一輪明月,它明亮,照白了月邊稀疏的雲;它潔淨,彷彿世間沒有什麼比它跟潔白如雪;它深邃,好像有不盡的秘密。周圍繁星點點,如同月亮的夥伴,與她共歡笑。明朗的夜空,月亮和繁星點綴,讓這個夜晚,顯得十分安靜祥和。 我不禁陷入無盡的想象。月兒啊!這孤獨的夜,你有著星星的陪伴,與我不同!月兒啊!夜深人靜時,你怎麼還沒入睡?是否和我一樣,還在努力奮鬥?月兒啊!一個個夜晚,你是否感覺有點煩悶?…… 想著想著,我突然想到:身處鬧市區,是如何見到這樣的明月的?這夜空明朗的,讓人驚奇,我從未見過。這夜空,不應該滿是霧霾、塵埃嗎? 我回想起,好多年前,那也是一個這樣的的夜晚,我和朋友們在路口乘涼,可能才剛上小學。 抬眼望去,一片灰濛濛的天,幾乎看不清什麼月亮,更別提星星了。那時候,上海的明朗的夜晚並不是天天有。 “這月兒,今天晚上又躲起來了,看不清了。”一旁的嘉豪一邊向天上凝視,一邊說。但是,最終他還是未能知曉月兒的真面目。 “環境汙染太嚴重了,那些工廠、車輛,人類生活影響到了環境。”我說著,彷彿看到了遠處郊區的幾座大工廠,煙還在不斷上升到大氣中。 嘉豪打了個哈欠。現在已經是夜深人靜時,遠處的卡車的聲音,很清楚,又很微弱。老師講過,有些卡車用的油不符合標準,排放出來很是汙染環境。我們站在這路口,似乎還能聞到汽油發出的一種難聞的味道。 這路口,昏黃的路燈還在亮著,高樓大廈還在發著光。“書上說,大部分的電力還是來自不清潔能源。”喜愛讀書的嘉豪說。一個晚上,這燈火通明的鬧市區,不知要浪費多少電呢。 夜深了,我記不起後面說了什麼,想了什麼。 恍惚間,我又陷入了沉思。啊,我的月兒!你看起來有些孤獨!沒有星星的陪伴,你還好嗎!啊,我的月兒!你聽見這卡車的聲音,問道汽油的味道了嗎?啊,我的月兒!這樣一個夜晚,你卻躲了起來,是有什麼秘密嗎? …… 夜晚,有些涼,我被凍醒,已經凌晨二三點了吧。 我再次舉頭凝視天邊的月亮。誒?月亮怎麼又躲起來了?我望向桌上雜亂擺放的作業。誒?我的作業怎麼已經寫完了!原來,一切,是夢。作業寫完後,我太累了,就在書桌上睡著了。 我的月亮,答應我,以後不要再躲起來了?我都能見到你和星星,在無數個靜寂而孤獨的夜!答應我,讓我夢想成真,好嗎? 將來,我再次凝視那一輪明月……

2020/8/23
articleCard.readMore

用 GitHub Actions 部署 Hexo 部落格

最近有一個朋友找我,說她弄了一個 Hexo 部落格,想做 GitHub Actions 自動部署,奈何不會弄,只好讓我幫忙。 GitHub Actions GitHub Actions 是 GitHub 推出的持續整合服務,最近不要錢了,用(白嫖)的人就多起來了。 程式碼 話不多說,直接上程式碼: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 name: "Hexo Blog Builder" on: [push, pull_request] jobs: build: name: "Hexo Blog Build" runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 with: fetch-depth: 1 - uses: actions/setup-node@v1 with: node-version: '12' - name: Install Hexo run: npm install -g hexo - name: Install Dependencies run: npm install - name: Clean Previous Build run: rm -rf docs - name: Hexo Clean run: hexo clean - name: Generate New Build run: hexo generate - name: Move docs/ run: mv public docs - name: Publish run: | mkdir -p ~/.ssh/ echo "${{ secrets.KEY }}" > ~/.ssh/id_rsa.pub chmod 600 ~/.ssh/id_rsa.pub ssh-keyscan github.com >> ~/.ssh/known_hosts git config --global user.name "Hexo Deploy Bot" git config --global user.email "nobody@nobody.nobody" git config --global core.quotepath false git add --all git commit -m "Hexo Blog Build" git push origin master 本 Action 需要你的 ssh 公鑰,也就是 id_rsa.pub 存在專案設定中的 Secrets 內。 TL;DR 具體實現很簡單,用 Hexo 自帶生成器生成到 public 目錄,把 public 目錄改名為 docs。最後 push 到專案裡。 push 使用 SSH Key,實現免密碼。 在 GitHub Pages 設定中,需要把 Source 改為 docs 目錄。 後記 如果大家有什麼改進的好方法,可以在下方評論!

2020/8/22
articleCard.readMore

遲來的日誌 - WWDC 2020 獎學金

今年 6 月份,因為疫情的影響,WWDC改為線上,之前的獎學金重新命名為 Swift Student Challenge。 我老早就有參加這一活動的打算,只是一直沒能實現,也沒有達到要求。今年大膽嘗試了一會,雖然成績不理想,但還是收穫了許多。 我今年的專案在 aeilot/WWDC2020。 Overview 今年我的專案使用了 SwiftUI 和 SpriteKit。我做了一個 2D 的跑酷類遊戲,內容是關於 COVID19 的。 整體開發下來,多虧了 Apple Document,裡面的文件很詳細,還有示例,👍。 當然開發中遇到的幾個問題,我也是在 Apple Developer Forums 和 Stack Overflow 解決的。具體連結就不寫了,社群很活躍,10分鐘就有人回答了。 只是開發太趕,只有不到一個星期,決定參加都花了幾天,🤦‍♀️。。。所以最後成果像一個半成品。。。 Future 關於明年的 WWDC,我想我是一定會去參加的!最好早點準備,加油!等著我的好訊息!

2020/8/19
articleCard.readMore

vcpkg - 方便的 C/C++ 庫管理器

最近在做一個大型的 C++ 專案 ── Albumist。它使用 Qt,需要用到很多類似 sqlite3、exiv2、libcurl 之類的庫。第三方庫管理似乎成了問題。 支援 git 的都可以使用 git submodule 來管理,但是還有少部分是不支援的,或者是二進位制的,比如 sqlite3。這就要我們手動去下載,手動去更新,好生麻煩。 vcpkg 我一直想要一個類似 npm pip 一樣的東西,把依賴寫在檔案裡面,然後直接 install 即可。 經過搜尋,我發現了微軟的一個專案 vcpkg。官方定義它為 “C++ Library Manager for Windows, Linux, and MacOS”,簡單翻譯一下就是 “支援 Windows, Linux 和 macOS 的 C++ 庫管理器”。 在 Arch Linux 上面,安裝它並不複雜,只需要:sudo pacman -S vcpkg-git 即可。其他系統沒有嘗試過,跟著它的 README 也很方便安裝。 “清單” 功能 在專案根目錄建立一個 vcpkg.json, 然後 vcpkg install。這一功能就是“清單”功能。它正好是我想要的,可是卻暫時處在 Beta 階段,不過也無關緊要。 要啟用“清單”功能,在執行命令時,需要接引數 --feature-flags = manifests。 清單大概長這樣: 1 2 3 4 5 6 7 8 { "name": "<name of your project>", "version-string": "<version of your project>", "dependencies": [ "abseil", "boost" ] } 很方便,也很快捷。 不足 比起這樣一個純粹的管理器,我更希望獲得類似 Java 的 Gradle 的體驗。如果 CMake 能加入類似功能,體驗一定能夠翻倍,快捷方便且無需第三方軟體。 後記 — 2020 08 15 更新 又停止使用這個東西了,Windows 上面需要 MSVC。。。

2020/8/13
articleCard.readMore

vimrc 配置指南

Vim 即 Vi Improved,它的前身是 Vi。它是一個十分強大的編輯器,優點如下: 不需要滑鼠,純鍵盤 Linux 系統預裝 鍵盤命令肌肉記憶之後,效率非常高 但是,它的配置許許多多,讓人難以記住。我正好要在新電腦上配置它,故寫此文。 為什麼不用別人現成的? 自己的配置更順手,自己更熟悉。 瞭解 Vim Script,可以自己造外掛 etc. 不會 Vim 怎麼辦 啊這…… 🤣 你可以試著在命令列輸入 vimtutor。 vimrc 的位置 本篇文章以新安裝的 vim 為例。Vim 的配置檔案叫做 vimrc。在 Mac、Linux 等系統上,位於 ~/.vimrc。在 Windows 系統中,它叫做 _vimrc,但是同樣位於 Home 目錄中。 全域性的配置,在 Mac、Linux 等系統上,位於 /etc/vimrc。在 Windows 系統上,它儲存在 Vim 安裝目錄,同樣叫做 vimrc。 配置 一個什麼也沒有配置的 Vim,開啟可能是這樣的: 下面各配置,你可以新增自己想要的。以下所有配置都可以在命令模式輸入,臨時啟用或關閉。 基本 1 2 3 4 5 6 7 8 9 set nocompatible " 不使用 Vi 相容模式 filetype plugin on " 檢測檔案型別,載入外掛 syntax on " 開啟語法高亮 set showmode " 顯示當前模式 set showcmd " 在底部顯示命令 set mouse=a " 啟用滑鼠,不建議開啟 set encoding=utf-8 set t_Co=256 filetype indent on " 根據檔案型別,不同縮排 縮排 1 2 3 4 5 set autoindent " 自動縮排 set tabstop=4 " tab佔4個空格 set shiftwidth=4 " 在文字上按下>>(增加一級縮排)、<<(取消一級縮排)或者==(取消全部縮排)時,每一級的空格數。 set expandtab " tab自動轉為空格 set softtabstop=2 " tab轉為多少空格 介面 1 2 3 4 5 6 7 8 9 set number " 顯示行號 set relativenumber " 顯示游標所在行當前行號,其他都顯示為相對於當前行的行號 set cursorline " 當前行高亮 colorscheme default " 設定顏色主題為 default,顏色主題儲存在 Home 目錄的 .vim/colors 資料夾,Windows 下叫做 vimfiles/colors set wrap " 設定多於行寬的文字自動分拆為多行顯示,反之: set nowrap set linebreak " 遇到特殊的符號才折行 set laststatus=2 " 是否顯示狀態列。0 不顯示,1 只在多視窗時顯示,2 顯示。 set ruler " 狀態列顯示游標位置 set showmatch " 高亮括號 搜尋 1 2 3 set hlsearch " 高亮搜尋結果 set incsearch " 搜尋輸入時,即時跳轉 set ignorecase " 忽略大小寫 更多 Vim 配置不止這些,我只介紹了一些常用的。Vim Script 值得學習,學完了還可以自己開發外掛。 題外話 - 外掛 外掛管理,Vim 雖然新增了自帶的,但我還是很推薦 Vim-Plug

2020/7/26
articleCard.readMore

NextCloud - DIY NAS 解決方案

前言 我之前給家中的伺服器做了一個 WebDAV 功能,帶有不堪入目的 UI 和難以使用的功能。而且配置起來並不簡單。一直想要改進,但是卻一直沒時間。最近有空了,便開始尋找替代品。 查了幾下 GitHub,找到了一個標星 10k+ 的專案 —— NextCloud。瞭解了一下,NextCloud 是一個擁有 全平臺客戶端,支援 WebDAV,而且 外掛化,可以 多使用者 使用的私有云儲存網盤專案。不僅如此,它還支援共享、版本控制、團隊協作等功能。外掛化讓它擁有了類似 Markdown 線上編輯,Draw-io 線上編輯,顯示 RAW 檔案的功能。 而且,我發現它支援 Docker,這無疑簡化了我們配置的步驟。 那麼,我們開始吧! Docker 配置 Docker 安裝很簡單,為了安裝快速,你可以參考清華大學開源映象站給出的 文件。如果你已經安裝了 Docker, 那麼可以忽略這一步。 更換映象也是讓你更快體驗的必不可少的一步,修改 /etc/docker/daemon.json 檔案 1 2 3 4 5 6 7 8 9 10 11 { "registry-mirrors" : [ "http://registry.docker-cn.com", "http://docker.mirrors.ustc.edu.cn", "http://hub-mirror.c.163.com" ], "insecure-registries" : [ "registry.docker-cn.com", "docker.mirrors.ustc.edu.cn" ] } 安裝 NextCloud 執行如下命令即可: 1 2 3 4 5 sudo docker run -d \ --name nextcloud \ -p 8000:80 \ -v <資料儲存位置>:/var/www/html \ nextcloud 如果遇到如下問題: 1 Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 可以執行: 1 2 systemctl daemon-reload systemctl restart docker.service 配置 配置這一部分很簡單,不用解釋了。 選擇資料庫時,使用量小可以選擇 SQLite,但是並不推薦。 結語 NextCloud 還是非常穩定的,基本配置完之後不會遇到什麼問題。而且原生支援中文,只需要在設定中設定一下就可以了。 我主要用它儲存我的照片,攝影還有一些不需要經常檢視的檔案。由於自己伺服器效能不錯,用起來很流暢,完全沒必要買現成的,硬碟不夠再買一個即可。

2020/7/23
articleCard.readMore

sudo shutdown -r now

你可能會驚奇地發現部落格上面以前的文章全不見了。最近我對我部落格進行了一次大清掃。 tl;dr 前幾天,我突然發現自己的部落格上面全是水文(以前就知道,懶得弄),心裡總覺得過不去。所以,我下定決心,準備重新來過。這種感覺很好,當_posts目錄空無一物時,我的心也釋然了。 之前我所有的文章,要麼是抄襲的,要麼是很水的那種。在這裡,我承諾,以後不再會有這種不可饒恕的情況發生了。這樣做我自己心裡也總是想著缺點什麼。倖幸苦苦修改的自定義hexo主題(沒錯,我把 hexo-theme-cactus 修改了很多,支援了 RSS,還弄了些 UI 改進,配置了 Valine.js,etc.),結果呢?上面全是水文!?這…… 那麼之前的文章都去哪裡了呢?我並沒有刪除,而是把它們放置在了 Draft 目錄下面,當然,你們看不到。手動狗頭 讓我們重新認識對方,執行一次sudo shutdown -r now!

2020/7/22
articleCard.readMore