盤線段樹、DP與圖論復(fù)習(xí)法)
“0x3f第29天復(fù)習(xí)”這串字符如果你不懂算法競賽圈的梗猛一看會以為是什么暗號。其實(shí)0x3f是十六進(jìn)制數(shù)63在代碼世界里它幾乎快成了“無窮大”的代名詞——0x3f3f3f3f是無數(shù)OI和ACM選手初始化距離、DP數(shù)組時最順手的值。而我一個堅(jiān)持算法打卡第29天的普通學(xué)習(xí)者今天想認(rèn)真聊聊這次“8.34-9.41”的復(fù)習(xí)記錄背后到底復(fù)盤了什么以及為什么第29天才是真正容易翻車、也真正能拉開差距的節(jié)點(diǎn)。這篇內(nèi)容適合正在按計(jì)劃刷題、準(zhǔn)備面試算法、或者單純想建立長期學(xué)習(xí)節(jié)奏的人我會把復(fù)習(xí)思路、時間分配和踩過的坑一次性講透。1. “0x3f”與我的算法學(xué)習(xí)第29天——先把這個標(biāo)題拆開看1.1 0x3f到底是什么先解決一個可能困惑新手的問題為什么大家都愛用0x3f3f3f3f而不是寫INT_MAX早年間我在Windows上寫CINT_MAX是2147483647看著很安全但有兩個隱患第一如果用INT_MAX去初始化距離數(shù)組一旦代碼里出現(xiàn)dist[u] w很可能直接整數(shù)溢出變成負(fù)數(shù)整個最短路邏輯直接崩掉第二INT_MAX參與加法會產(chǎn)生未定義行為靠編譯器心情做事。而0x3f3f3f3f等于1061109567大約是10^9級別比一般數(shù)據(jù)范圍上限大得多兩個0x3f3f3f3f相加是2122219134仍然小于INT_MAX不會溢出。memset按字節(jié)填充時0x3f3f3f3f每個字節(jié)都是0x3f一條memset(dist, 0x3f, sizeof dist)就能把整個數(shù)組干凈地初始化為同一個“大數(shù)”這就是這個魔術(shù)常量的核心優(yōu)勢。所以“0x3f”不只是個數(shù)字它代表了一種減少心智負(fù)擔(dān)的編碼習(xí)慣也代表算法競賽圈里那些“約定俗成的智慧”。用這個詞作為復(fù)習(xí)日志的編號對我來說還有另一層意思它提醒我很多看似玄乎的技巧拆開看底層邏輯都特別樸素就像0x3f的選用本質(zhì)就是“大數(shù)不溢出方便memset”三條原則。1.2 為什么是第29天——復(fù)習(xí)節(jié)奏的臨界點(diǎn)21天養(yǎng)成習(xí)慣的說法深入人心但真實(shí)的算法學(xué)習(xí)里第21天往往還處在“對模板半生不熟”的階段。到了第29天你大概率已經(jīng)過了最初的新鮮感開始面對最現(xiàn)實(shí)的問題前面學(xué)的開始忘了。我自己的學(xué)習(xí)計(jì)劃是每天投入一到兩小時前兩周主攻基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)和貪心中間一周切入圖論和搜索最近一周開始碰動態(tài)規(guī)劃。但在第26天左右我回頭做第一周做過的題目居然發(fā)現(xiàn)鄰接表的寫法都要想半天。那一刻我意識到不能再無腦往前刷了必須系統(tǒng)復(fù)習(xí)。第29天就是這個復(fù)習(xí)周期的第一棒——筆記上寫著的8.34-9.41正是我不看手機(jī)、不刷視頻、完整留出來的67分鐘復(fù)盤時間。為什么偏偏是67分鐘因?yàn)檫@個時長剛好能裝下“三個主題復(fù)習(xí)一道重做題筆記補(bǔ)漏”再長會明顯疲勞再短則連狀態(tài)都還沒熱起來。第29天的意義本質(zhì)上就是在習(xí)慣真正固化之前主動剎車一次把松動的根基重新砸實(shí)。1.3 8.34-9.4167分鐘的黃金復(fù)盤時段8.34開始不是巧合。早上大腦前額葉功能最清醒適合做需要高度專注的“回憶型復(fù)習(xí)”而不是被動地看筆記。我試過晚上復(fù)習(xí)結(jié)果往往是從“看一遍模板”變成“刷手機(jī)十分鐘”因?yàn)橥砩系淖晕壹s束力已經(jīng)消耗得差不多了。這67分鐘我的策略是前20分鐘閉卷回憶中間30分鐘重做經(jīng)典題最后17分鐘對照總結(jié)。為什么把重做題放在中間而不是開頭因?yàn)槿绻簧蟻砭妥鲱}大腦會直接進(jìn)入“搜索解法”模式這會跳過“提取記憶”的過程。記憶的核心不是多看而是成功提取。先合上書回憶一遍今天要復(fù)習(xí)的算法框架再做題提取效率完全不同。2. 復(fù)習(xí)到底復(fù)盤什么——第29天的核心內(nèi)容拆解2.1 數(shù)據(jù)結(jié)構(gòu)模塊把“0x3f”這類無窮大用活第29天我給自己定的復(fù)習(xí)范圍是前半個月的內(nèi)容數(shù)據(jù)結(jié)構(gòu)這里重點(diǎn)關(guān)注線段樹和并查集。說實(shí)話線段樹這種東西寫的時候覺得自己會了三天不碰再看代碼就像看天書。復(fù)習(xí)線段樹關(guān)鍵是在一張白紙上從零推導(dǎo)四個函數(shù)build、pushup、update、query。有個細(xì)節(jié)特別值得拿出來說——pushup是線段樹的靈魂。很多新手寫線段樹喜歡把維護(hù)邏輯塞進(jìn)update里結(jié)果一旦涉及區(qū)間覆蓋、區(qū)間加的復(fù)合操作代碼立刻失控。正確的抽象方式是讓所有子節(jié)點(diǎn)更新完之后通過pushup統(tǒng)一匯總父節(jié)點(diǎn)信息。這樣無論是單點(diǎn)修改還是區(qū)間修改父節(jié)點(diǎn)更新邏輯只有一處調(diào)試時只需要盯著這一個函數(shù)。我用一個自己踩過的坑總結(jié)一下線段樹區(qū)間加懶標(biāo)記時node[p].tag k和node[p].max k的順序不能反。先更新存儲值再更新懶標(biāo)記這兩步看似無所謂但如果你先更新tag再更新max而max的更新公式里恰好用到孩子節(jié)點(diǎn)、系統(tǒng)會按舊tag執(zhí)行后續(xù)pushdown就出錯了。這種順序細(xì)節(jié)只有重新靜下心寫一遍才能發(fā)現(xiàn)。2.2 圖論與搜索從模板到變形題第三周圖論部分我復(fù)習(xí)了Dijkstra最短路、并查集判環(huán)、拓?fù)渑判蚝虳FS回溯。圖論是很容易“背模板”的領(lǐng)域你上周能把Dijkstra的優(yōu)先隊(duì)列寫法默寫出來不代表今天還能。我的復(fù)習(xí)方法是不看模板只憑記憶寫出堆優(yōu)化Dijkstra然后拿去跑一道當(dāng)初做過的裸題。如果你也正好在圖論復(fù)習(xí)階段我會特別提醒一個被輕視的細(xì)節(jié)優(yōu)先隊(duì)列排序要用pair第一維距離、第二維節(jié)點(diǎn)編號。因?yàn)閜riority_queue默認(rèn)是大根堆pair排序先比第一維再比第二維剛好符合“先取出距離最小”的需求。如果只存節(jié)點(diǎn)編號卻想自定義比較淘汰器寫起來容易繞暈。另外求最短路徑條數(shù)時dist相等也要執(zhí)行cnt[v] (cnt[v] cnt[u]) % MOD而不是只在找到更短路時更新這是很多變形題的埋雷點(diǎn)。搜索部分DFS回溯我總是強(qiáng)調(diào)“撤銷操作”和“剪枝”的關(guān)系。很多人把剪枝當(dāng)成一種優(yōu)化手段其實(shí)在超大數(shù)據(jù)范圍下沒有剪枝的DFS就是暴力枚舉時間復(fù)雜度是災(zāi)難級的。復(fù)習(xí)時我建議把決策樹的形態(tài)畫出來標(biāo)注哪些分支可以被可行性剪枝、哪些需要最優(yōu)性剪枝這個功夫能幫你節(jié)省大量實(shí)戰(zhàn)時間。2.3 動態(tài)規(guī)劃常見狀態(tài)設(shè)計(jì)的復(fù)盤動態(tài)規(guī)劃是第29天復(fù)習(xí)的重頭戲。DP的難點(diǎn)從來不是轉(zhuǎn)移方程本身而是為什么狀態(tài)要這么定義以及初始化邊界為什么要是這個值。比如背包問題為什么dp[0] 0、其他位置初始化為負(fù)無窮因?yàn)椤扒『醚b滿”和“不超過容量”兩種問法的初始化策略完全不同——前者要求必須從容量0開始拼湊后者天然允許任何容量下都繼承0。復(fù)習(xí)時我會挑三個經(jīng)典模型各寫一遍0-1背包、最長上升子序列、區(qū)間DP。區(qū)間DP里最經(jīng)典的合并石子/矩陣鏈乘問題它的三層循環(huán)結(jié)構(gòu)值得反復(fù)默寫先枚舉區(qū)間長度再枚舉左端點(diǎn)最后枚舉分割點(diǎn)。如果我發(fā)現(xiàn)自己連“為什么先枚舉長度而不是先枚舉左端點(diǎn)”都要想半天那說明上一輪學(xué)得其實(shí)不透徹復(fù)習(xí)的價值就在這里體現(xiàn)——逼你誠實(shí)地面對知識漏洞。3. 實(shí)操過程一個67分鐘復(fù)習(xí)時段怎么安排3.1 時間分配表我把8.34到9.41這67分鐘拆成了五段每段都有明確的輸出物。沒有輸出的復(fù)習(xí)都是自我安慰。時間段時長復(fù)習(xí)動作預(yù)期輸出8.34-8.4511分鐘合上筆記在空白紙上列出本周學(xué)過的全部算法名稱一張“算法地圖”檢驗(yàn)?zāi)芑貞浂嗌?.45-8.5510分鐘憑記憶實(shí)現(xiàn)線段樹區(qū)間最大值的四個核心函數(shù)能跑通的代碼片段8.55-9.1015分鐘重做一道最短路徑計(jì)數(shù)題裸題AC或明確知道自己卡在哪一步9.10-9.3020分鐘閉卷補(bǔ)寫0-1背包、LIS、區(qū)間DP的模板三份手寫模板9.30-9.4111分鐘對照原筆記劃出遺忘點(diǎn)寫下三條改進(jìn)提示三條可執(zhí)行的明日要點(diǎn)這張表最核心的設(shè)計(jì)思路是“先回憶、后對照”。如果你一開始就翻開筆記看模板那你永遠(yuǎn)不知道自己到底忘了多少。復(fù)習(xí)最忌諱的就是把“看到認(rèn)識”當(dāng)成“會了”閉卷輸出才能照出原型。3.2 具體操作步驟與細(xì)節(jié)第一步的“算法地圖”并不是把所有算法名寫出來就完了而是要給每個算法標(biāo)星號——三顆星表示“閉卷能寫”兩顆表示“需要提示”一顆表示“完全沒印象”。經(jīng)過三周學(xué)習(xí)我最后標(biāo)出的兩星和三星大概各占一半沒有一顆星的但這個結(jié)果比我想象中樂觀說明前期刷題確實(shí)在起作用。第二步線段樹是個硬骨頭。我先在紙上畫出一棵區(qū)間[1,8]的線段樹然后從build函數(shù)開始寫。寫update的時候手會自然停下來因?yàn)橐貞沴azy標(biāo)記到底在什么時機(jī)下放。我的經(jīng)驗(yàn)是能不用懶標(biāo)記就盡量不要用。很多看似需要區(qū)間更新的題其實(shí)可以用差分思想或者樹狀數(shù)組繞開這樣代碼短調(diào)試更簡單。但一旦真需要區(qū)間更新區(qū)間查詢懶標(biāo)記必須掌握我的記憶錨點(diǎn)是一句話“pushdown先傳給兩個孩子再做區(qū)間操作”。第三步重做最短路計(jì)數(shù)題。這題當(dāng)初第一次做的時候我卡了將近一個小時現(xiàn)在做應(yīng)該流暢很多。實(shí)際動手之后我發(fā)現(xiàn)自己還是在邊界處理上猶豫了一下——cnt[s]要不要初始化為1答案是要因?yàn)槠瘘c(diǎn)到自己的路徑就是一條路徑。這類細(xì)節(jié)重做舊題比刷新題更容易暴露。第四步是DP模板的三件套默寫。0-1背包注意一維逆序LIS注意維護(hù)的是“最小末尾值數(shù)組”區(qū)間DP注意枚舉長度的循環(huán)順序。這三個模板寫下來我估算至少要十五分鐘所以時間表里給它排了20分鐘留一點(diǎn)緩沖。第五步是收尾。我對照原筆記把遺忘點(diǎn)分成兩類一類是“概念懂了但代碼忘了”的比如線段樹的pushdown寫法另一類是“當(dāng)初就沒理解透”的比如區(qū)間DP為什么枚舉長度要放外層。前者明天重寫一遍就能解決后者需要找一道新題再驗(yàn)證。這十一條改進(jìn)提示是我今天復(fù)習(xí)真正的產(chǎn)出物。3.3 我的記錄方法別在復(fù)習(xí)時“自我感動”有一個很要命的問題就是復(fù)習(xí)著復(fù)習(xí)著變成了重新學(xué)習(xí)。你原計(jì)劃只是過一遍線段樹結(jié)果看到某個細(xì)節(jié)不明白直接打開題解刷了三道新題。等回過神來67分鐘結(jié)束了當(dāng)初計(jì)劃中要復(fù)習(xí)的DP模板一個沒寫。我的解決辦法是在手機(jī)備忘錄里建一個“復(fù)習(xí)日志”的表格每一行只有三個字段復(fù)習(xí)主題、是否閉卷完成、發(fā)現(xiàn)的問題。嚴(yán)格規(guī)定復(fù)習(xí)過程中不打開新題。每當(dāng)腦子里冒出“不如順便做一道新題鞏固一下”的念頭我都會強(qiáng)迫自己先記在“發(fā)現(xiàn)的問題”里這相當(dāng)于給沖動一個“等一下再說”的緩沖。實(shí)際體會是大部分沖動過十分鐘就消失了而那些真值得去查漏的知識點(diǎn)在第二天的固定學(xué)習(xí)時間段里處理效果遠(yuǎn)好于復(fù)習(xí)中途臨時加塞。4. 常見問題與避坑指南4.1 忘得比學(xué)得快怎么辦這是第29天遇到的最大情緒沖擊。明明上周還能熟練寫出來的Dijkstra為什么今天默寫就漏了優(yōu)先隊(duì)列的greater比較器后來我想明白了這不是你“不行”而是人類的記憶曲線就是這樣的。遺忘是正?,F(xiàn)象不是能力缺陷。你的目標(biāo)不是讓記憶永不消逝而是在它消逝前建立足夠的提取路徑。我的方法有兩個第一每個算法至少建立兩條“提取線索”比如Dijkstra不僅記“每次找最小距離點(diǎn)”還記“看起來像BFS貪心本質(zhì)是優(yōu)先隊(duì)列反復(fù)出隊(duì)”第二把模板拆成“為什么”和“怎么寫”兩層來記很多代碼細(xì)節(jié)只要理解了邏輯就忘不掉比如堆優(yōu)化Dijkstra的核心本質(zhì)就是用小根堆替代O(n^2)掃描找最小值想到這個層面代碼自然就還原出來了。4.2 從“看得懂”到“寫得出來”之間的鴻溝怎么填很多人學(xué)算法時有這種體驗(yàn)看題解一看就懂合上題解一寫就廢。我在第29天重做舊題時也遇到了這個現(xiàn)象——題目很眼熟官方解法爛熟于心但手放鍵盤上第一行代碼就猶豫了因?yàn)闆]想清楚到底要從輸入建圖開始還是先定義鄰接表。這個問題的根源在于“理解”和“執(zhí)行”用的是大腦的不同區(qū)域。解決方法只有一條刻意練習(xí)“從零寫完整代碼”而不是“補(bǔ)全代碼”。復(fù)習(xí)時不要打開已經(jīng)寫好的模板去填空而要把模板收起來真的從#include開始寫。哪怕寫得又慢又丑這個過程能把那些只停留在視覺層面的知識轉(zhuǎn)變成肌肉記憶層面的能力。我給自己定了一個硬指標(biāo)每個算法模板至少獨(dú)立默寫三次間隔分別是一天后、三天后、一周后。第一次默寫會非常痛苦第二次會好很多第三次基本能一分鐘內(nèi)寫對核心結(jié)構(gòu)。第29天這次復(fù)習(xí)相當(dāng)于第二輪默寫的一部分。4.3 時間不夠用怎么辦——給復(fù)習(xí)做減法如果你是上班族或者課業(yè)繁重每天未必能抽出一整個小時復(fù)習(xí)。我的建議是把復(fù)習(xí)內(nèi)容壓縮到“一個最小集”只復(fù)習(xí)當(dāng)天必須鞏固的一個知識點(diǎn)一道重做舊題總時長控制在25分鐘以內(nèi)。這也是為什么我特意把每次復(fù)習(xí)主題“單元化”每次只安排兩三個主題而不是試圖在一天內(nèi)翻完整本筆記。具體的減法優(yōu)先級是第一優(yōu)先重做做錯的經(jīng)典題第二優(yōu)先閉卷默寫一個模板第三優(yōu)先瀏覽概念性筆記。做錯題之所以放最高優(yōu)先級因?yàn)殄e題代表你真實(shí)的薄弱點(diǎn)而概念性筆記瀏覽往往只是在增加“熟悉感”的錯覺。第29天這67分鐘里我做的最有價值的一件事就是重新手寫了一遍當(dāng)初錯了好幾輪的線段樹區(qū)間更新而且這次一次通過。4.4 心態(tài)崩了怎么辦——聊聊第29天的情緒管理說實(shí)話復(fù)習(xí)到中間我有過短暫的心態(tài)波動??吹阶约郝憫袠?biāo)記下放的時機(jī)第一反應(yīng)是“我是不是白學(xué)了”。這種情緒特別可怕它會導(dǎo)致一個惡性循環(huán)越覺得自己不行越不敢檢測自己越不檢測知識漏洞就越隱蔽。我的調(diào)整策略是給每次考試式復(fù)習(xí)加一個“容錯提示”。默寫模板出現(xiàn)錯誤時我不會批評自己而是在旁邊標(biāo)一個紅色的“補(bǔ)丁”符號然后寫出正確的版本。這樣一來正確的代碼和錯誤的記憶同時被強(qiáng)化了一次相當(dāng)于多了一次針對性訓(xùn)練。心態(tài)上我還會暗示自己第29天發(fā)現(xiàn)漏洞比第90天才發(fā)現(xiàn)劃算太多了。復(fù)習(xí)不是尋找自己的失敗而是在建立一份“還需要補(bǔ)什么”的清單這份清單本身就是學(xué)習(xí)進(jìn)度的一部分。5. 我在第29天踩過的坑和對后續(xù)30天的調(diào)整5.1 復(fù)習(xí)時最大的坑誤把“重看”當(dāng)“復(fù)習(xí)”我在第29天最深刻的體會就是復(fù)習(xí)的第一原則是提取不是重看。如果你打開上次的筆記從頭到尾讀一遍感覺自己都懂了那其實(shí)是一種幻覺。真正有效的復(fù)習(xí)應(yīng)該先蓋住筆記逼自己回憶關(guān)鍵詞、算法框架、代碼結(jié)構(gòu)再打開筆記核對。哪怕回憶出來的是錯的也比看一遍有用得多——因?yàn)殄e誤的回憶給了你一個明確修補(bǔ)目標(biāo)而流暢的重看只是給大腦一種“我很熟”的虛假安全。5.2 給后續(xù)30天設(shè)置的三個新規(guī)則踩了坑之后我馬上給自己后面的計(jì)劃調(diào)了三個新規(guī)則。第一每周末固定做一次“閉卷知識地圖”把本周所有涉及的算法全部寫在A4紙上不允許翻筆記這個動作強(qiáng)制我每周校準(zhǔn)一次記憶第二每次復(fù)習(xí)最多覆蓋三個主題寧可精嚼一個不要走馬觀花過十個第三建立一個錯題復(fù)發(fā)臺賬每道錯題記錄兩個字段第一次做錯的時間、復(fù)習(xí)時是否仍然卡殼超過五分鐘。如果一道題卡殼超過五分鐘說明它根本不是“粗心”而是底層思維鏈條還斷著必須拆碎重寫。這第三點(diǎn)其實(shí)是這個項(xiàng)目標(biāo)題背后最重要的經(jīng)驗(yàn)第29天不是終點(diǎn)而是曲線開始變陡的起點(diǎn)。所有堅(jiān)持到第29天的人最危險的一件事就是自滿最幸運(yùn)的一件事則是你還有第30天、第60天、第90天可以去修正方向。用0x3f這個編號里的探索精神去面對學(xué)習(xí)中的不確定性把每一次卡頓都當(dāng)成更新自己的機(jī)會這才是持續(xù)進(jìn)步的真實(shí)邏輯。我個人在實(shí)際操作中還有一個體會復(fù)習(xí)日志不必寫成大段感想越簡短越好。當(dāng)時做過的題、卡過的點(diǎn)、補(bǔ)上的代碼才是復(fù)習(xí)真正的痕跡。等哪天想偷懶時翻一翻記滿問題的日志比喊十遍“加油”都管用。希望這份第29天的復(fù)盤記錄能給同樣在長跑算法學(xué)習(xí)路上的你一個可以參照的節(jié)奏感。