檢測算法詳解:從拓?fù)渑判虻紻FS,掌握競賽與工程核心)
1. 項目概述從一道經(jīng)典算法題看環(huán)檢測的實戰(zhàn)價值“發(fā)現(xiàn)環(huán)”這個聽起來有點偵探小說味道的短語其實是算法競賽和軟件工程中一個極其經(jīng)典且基礎(chǔ)的問題。我第一次在2017年藍(lán)橋杯國賽的C/C B組題目中看到它時就覺得這題出得相當(dāng)精妙——它沒有復(fù)雜的背景故事直指“無向圖中找環(huán)”這個核心。但恰恰是這種看似簡單的問題最能考驗一個程序員對數(shù)據(jù)結(jié)構(gòu)的理解深度和代碼實現(xiàn)的扎實程度。在實際開發(fā)中環(huán)檢測無處不在從編譯器檢查代碼中的循環(huán)依賴到分布式系統(tǒng)檢測死鎖再到社交網(wǎng)絡(luò)分析中的關(guān)系圈查找其底層邏輯都與這道題異曲同工。今天我就以一個老碼農(nóng)的視角帶大家徹底拆解這道題不僅講清楚如何在競賽中快速AC更會深入探討其背后的多種解法、性能權(quán)衡以及在實際工程中的變形與應(yīng)用。無論你是正在備賽的算法新手還是想鞏固圖論基礎(chǔ)的開發(fā)者相信這篇從實戰(zhàn)中淬煉出的心得都能讓你有所收獲。2. 問題核心與場景映射為什么“發(fā)現(xiàn)環(huán)”如此重要2.1 題目精析與抽象建模我們先回到題目本身。題目描述通常是這樣給定一個包含N個頂點、N條邊的無向連通圖這意味著頂點和邊數(shù)相等并且每個頂點的度連接的邊數(shù)可能大于1。保證圖中存在且僅存在一個環(huán)。要求找出這個環(huán)中的所有頂點并按升序輸出。這里有幾個關(guān)鍵約束直接決定了我們的解題策略無向圖邊沒有方向A-B和B-A是同一條邊。N個點N條邊這是一個非常重要的條件。對于一棵樹無環(huán)連通圖其邊數(shù)是頂點數(shù)減一N-1?,F(xiàn)在多了一條邊正好構(gòu)成了一個“基環(huán)樹”結(jié)構(gòu)——即一棵樹上加了一條邊形成一個唯一的環(huán)。僅存在一個環(huán)這大大簡化了問題。我們不需要處理多個環(huán)相交的復(fù)雜情況目標(biāo)明確。抽象成模型就是給定一個基環(huán)樹找出其唯一的環(huán)。這比在任意圖中找環(huán)要簡單但也涵蓋了環(huán)檢測的核心思想。2.2 現(xiàn)實場景中的“環(huán)”理解了這個模型我們就能看到它的普遍性軟件包依賴管理npm,pip,Maven等工具在安裝依賴時必須檢測是否存在循環(huán)依賴A依賴BB又依賴A否則無法確定安裝順序。這本質(zhì)上就是在有向圖中檢測環(huán)。任務(wù)調(diào)度與死鎖檢測操作系統(tǒng)中多個進(jìn)程請求資源可能形成“循環(huán)等待”進(jìn)程P1持有資源R1并請求R2進(jìn)程P2持有R2并請求R1這就是一個環(huán)。檢測到這個環(huán)是預(yù)防和解除死鎖的關(guān)鍵。社交網(wǎng)絡(luò)分析在一個社群中如果A關(guān)注BB關(guān)注CC又關(guān)注A這就形成了一個“關(guān)注環(huán)”或“閉環(huán)社群”對于推薦算法和社群發(fā)現(xiàn)很有意義。編譯器與靜態(tài)代碼分析在編譯C時頭文件的包含關(guān)系不能成環(huán)在分析代碼調(diào)用鏈時遞歸函數(shù)調(diào)用需要被合理識別和處理環(huán)檢測是基礎(chǔ)。因此解決這道題絕不僅僅是為了競賽更是掌握一種基礎(chǔ)且強大的問題解決工具。3. 核心算法思路拆解多條道路通羅馬對于基環(huán)樹找唯一環(huán)主流解法有好幾種每種都有其獨特的思維角度和適用場景。3.1 拓?fù)渑判騅ahn算法的巧妙應(yīng)用這是我最推薦給新手首先掌握的解法思路清晰代碼不易寫錯。核心思想不斷刪除圖中度為1的葉子節(jié)點及其相連的邊因為這些節(jié)點不可能在環(huán)上。重復(fù)這個過程最后剩下的節(jié)點就是環(huán)上的節(jié)點。為什么可行在基環(huán)樹中環(huán)是唯一的核心。所有不在環(huán)上的節(jié)點都可以被視為從環(huán)這個“核心”上生長出來的樹枝樹鏈。這些樹枝末端的節(jié)點度數(shù)為1。當(dāng)我們不斷剝離度為1的節(jié)點時這些樹枝會被從末端向根部層層刪除直到抵達(dá)環(huán)上的節(jié)點。環(huán)上的每個節(jié)點至少有兩個連接在環(huán)內(nèi)因此其度數(shù)在剝離過程中始終大于等于2不會被刪除。最終所有“樹枝”被剝光剩下的就是純凈的環(huán)。操作步驟統(tǒng)計每個頂點的度入度。初始化一個隊列將所有度為1的頂點入隊。當(dāng)隊列不為空時 a. 彈出隊首頂點u。 b. 遍歷u的所有鄰居v 將v的度減1相當(dāng)于刪除邊u-v。 如果v的度減為1則將v入隊。過程結(jié)束后所有度仍然大于等于2的頂點即為環(huán)上的頂點。注意這個方法非常依賴于“僅有一個環(huán)”的條件。如果圖中有多個環(huán)或者是一個復(fù)雜的圖拓?fù)渑判蚝罂赡苁O露鄠€強連通分量而不僅僅是環(huán)上的點。3.2 深度優(yōu)先搜索DFS與父節(jié)點記錄這是更通用的找環(huán)方法稍不留意就容易寫錯但理解后非常強大。核心思想從任意節(jié)點開始DFS同時記錄每個節(jié)點的“父節(jié)點”是從哪個節(jié)點遍歷過來的。如果在遍歷過程中發(fā)現(xiàn)下一個要訪問的節(jié)點不是當(dāng)前節(jié)點的父節(jié)點并且已經(jīng)被訪問過那么就說明找到了一個環(huán)?;厮莞腹?jié)點鏈就可以得到整個環(huán)。為什么可行DFS會系統(tǒng)地探索圖的每一條路徑。在無向圖中正常的樹邊會形成一棵DFS樹。環(huán)的存在意味著圖中存在一條“回邊”——這條邊連接了DFS樹中的一個節(jié)點和它的一個非父祖先節(jié)點。當(dāng)我們沿著DFS探索時一旦遇到這樣的回邊環(huán)就被發(fā)現(xiàn)了。操作步驟從任一節(jié)點開始DFS。維護(hù)三個狀態(tài)數(shù)組visited是否訪問過、parent父節(jié)點、in_stack是否在當(dāng)前搜索路徑上用于快速判斷回邊。對于當(dāng)前節(jié)點u遍歷其鄰居v a. 如果v未被訪問則設(shè)置parent[v] u然后遞歸DFS(v)。 b. 如果v已被訪問且v不是u的父節(jié)點v ! parent[u]說明找到了環(huán)。此時可以從u開始沿著parent數(shù)組回溯到v再結(jié)合v到u的這條邊就構(gòu)成了環(huán)。需要仔細(xì)處理環(huán)的存儲和去重特別是按升序輸出時。實操心得DFS找環(huán)時最容易犯的錯誤是忽略了無向圖每條邊被算作兩條有向邊從而把“剛過來的那條邊”誤判成環(huán)。一定要用parent來排除這條邊。另一種實現(xiàn)是使用一個棧來顯式記錄當(dāng)前路徑當(dāng)遇到已訪問且在棧中的節(jié)點時從棧中彈出節(jié)點直到遇到該節(jié)點這些節(jié)點就構(gòu)成了環(huán)。3.3 并查集Union-Find的判環(huán)連接并查集通常用于判斷圖是否成環(huán)在這里可以稍作變通來“發(fā)現(xiàn)”環(huán)。核心思想逐條邊加入圖中并用并查集維護(hù)節(jié)點的連通性。在加入一條邊之前如果這條邊的兩個端點已經(jīng)屬于同一個集合連通那么加入這條邊必定會形成一個環(huán)。這條邊就是“環(huán)邊”之一。如何找到整個環(huán)知道一條環(huán)邊u-v后我們可以從u和v分別出發(fā)進(jìn)行BFS或DFS尋找兩條不相交除起點外的路徑這兩條路徑加上u-v這條邊就構(gòu)成了環(huán)。具體可以從u出發(fā)BFS記錄到達(dá)每個節(jié)點的前驅(qū)節(jié)點目標(biāo)是找到v。但注意不能走u-v這條邊。從v出發(fā)BFS同樣記錄前驅(qū)目標(biāo)是找到u。將兩條路徑合并就得到了環(huán)。這種方法實現(xiàn)起來比純并查集要復(fù)雜一些。并查集方法的優(yōu)劣優(yōu)勢思路直觀判斷環(huán)的存在非常高效近乎O(1)。劣勢僅能判斷環(huán)的存在并找到一條環(huán)邊要輸出整個環(huán)需要額外進(jìn)行路徑搜索整體實現(xiàn)不如拓?fù)渑判蚝啙崱?. 基于拓?fù)渑判虻脑敿?xì)實現(xiàn)與代碼解析鑒于拓?fù)渑判蚍ㄔ诮鉀Q本題時的簡潔性和高效性這里給出一個用C實現(xiàn)的詳細(xì)版本并逐行解析。4.1 數(shù)據(jù)結(jié)構(gòu)與輸入處理#include iostream #include vector #include queue #include algorithm using namespace std; int main() { int N; cin N; vectorvectorint graph(N 1); // 鄰接表下標(biāo)從1開始 vectorint degree(N 1, 0); // 每個頂點的度 for (int i 0; i N; i) { int a, b; cin a b; graph[a].push_back(b); graph[b].push_back(a); degree[a]; degree[b]; } // ... 后續(xù)代碼 }關(guān)鍵點使用vectorvectorint存儲鄰接表空間復(fù)雜度 O(NE)對于稀疏圖本題就是很高效。degree數(shù)組至關(guān)重要是拓?fù)渑判虻尿?qū)動力。輸入邊時無向圖需要在鄰接表中添加雙向邊并同時更新兩個頂點的度。4.2 拓?fù)渑判騽內(nèi)~子核心過程queueint q; // 初始化隊列將所有葉子節(jié)點度為1入隊 for (int i 1; i N; i) { if (degree[i] 1) { q.push(i); } } while (!q.empty()) { int u q.front(); q.pop(); // 刪除節(jié)點u相當(dāng)于將其從圖中剝離 for (int v : graph[u]) { // 對于每個鄰居v斷開連接度減1 degree[v]--; // 如果v變成了新的葉子節(jié)點入隊 if (degree[v] 1) { q.push(v); } } }過程模擬 假設(shè)一個環(huán)1-2-3-4-1外面有些樹枝。初始時所有樹枝末端的節(jié)點度為1入隊。隊列彈出這些節(jié)點時會將其鄰居的度減1。這可能導(dǎo)致一些“樹枝中間節(jié)點”的度從2變?yōu)?從而成為新的葉子被入隊。這個過程像波浪一樣從外向內(nèi)傳遞直到所有不在環(huán)上的節(jié)點都被“剝掉”。環(huán)上的節(jié)點因為彼此連接度始終至少為2永遠(yuǎn)不會被加入隊列。4.3 結(jié)果收集與輸出vectorint ringNodes; for (int i 1; i N; i) { if (degree[i] 1) { // 所有度大于1的節(jié)點就是環(huán)上的節(jié)點 ringNodes.push_back(i); } } sort(ringNodes.begin(), ringNodes.end()); // 題目要求升序輸出 for (size_t i 0; i ringNodes.size(); i) { if (i ! 0) cout ; cout ringNodes[i]; } cout endl;為什么是degree[i] 1在剝離過程結(jié)束后環(huán)上節(jié)點的度恰好等于2因為是簡單環(huán)。但是如果環(huán)上的某個節(jié)點在原始圖中還連接了樹枝即該節(jié)點是環(huán)與樹的連接點那么它的初始度大于2。在剝離過程中連接樹枝的邊會被刪除但環(huán)上的邊不會被刪。因此最終環(huán)上節(jié)點的度至少為2。用1判斷是穩(wěn)妥的。對于純環(huán)無樹枝最終所有節(jié)點度等于2對于環(huán)帶樹枝連接點度大于2。常見問題為什么不用degree[i] 2來判斷因為如果環(huán)上的節(jié)點是多個樹枝的根最終它的度可能大于2。用1可以涵蓋所有情況。5. DFS找環(huán)法的實現(xiàn)細(xì)節(jié)與難點雖然拓?fù)渑判蚋唵蔚獶FS法是更通用的技能值得深入理解。5.1 DFS遞歸實現(xiàn)框架vectorvectorint graph; vectorint parent; // 記錄父節(jié)點 vectorint visited; // 0未訪問, 1訪問中, 2已訪問完畢 vectorint pathStack; // 當(dāng)前DFS路徑棧 vectorint cycle; // 存儲找到的環(huán) bool dfs(int u, int p) { // u當(dāng)前節(jié)點 p父節(jié)點 visited[u] 1; // 標(biāo)記為“訪問中” parent[u] p; pathStack.push_back(u); for (int v : graph[u]) { if (v p) continue; // 忽略父節(jié)點避免把無向邊來回走當(dāng)成環(huán) if (visited[v] 0) { // 樹邊繼續(xù)遞歸 if (dfs(v, u)) return true; // 如果下層遞歸找到環(huán)提前返回 } else if (visited[v] 1) { // 找到回邊發(fā)現(xiàn)環(huán) // 從當(dāng)前棧中提取環(huán) cycle.clear(); auto it find(pathStack.begin(), pathStack.end(), v); for (; it ! pathStack.end(); it) { cycle.push_back(*it); } // 此時cycle中存儲了從v到u在棧中的部分的路徑正好是環(huán) return true; } // visited[v] 2 的情況是已處理完的節(jié)點忽略對于無向圖找簡單環(huán)通常不會遇到 } visited[u] 2; // 標(biāo)記為“已訪問完畢” pathStack.pop_back(); return false; }難點解析三色標(biāo)記法visited數(shù)組使用三種狀態(tài)這是處理圖中環(huán)和復(fù)雜依賴關(guān)系的常用技巧。“訪問中”(1)狀態(tài)是關(guān)鍵它標(biāo)識了當(dāng)前遞歸棧上的節(jié)點。當(dāng)遇到一個狀態(tài)為1的鄰居時就意味著找到了一條指向祖先的回邊即環(huán)。環(huán)的提取找到回邊u-v時v是環(huán)的起點在棧中更早的位置。我們只需要從棧中找到v的位置然后將其到棧頂?shù)乃泄?jié)點取出這些節(jié)點就按順序構(gòu)成了環(huán)。注意這個環(huán)的起點和終點可能不是最小節(jié)點最后需要排序輸出。父節(jié)點判斷if (v p) continue;這行代碼至關(guān)重要。在無向圖DFS中每條邊都會被訪問兩次A-B 和 B-A。如果沒有這個判斷我們會立即把“從父節(jié)點來”的這條邊誤判為環(huán)。5.2 迭代DFS棧實現(xiàn)與路徑記錄遞歸DFS可能面臨棧溢出風(fēng)險盡管本題N通常不大。迭代版本使用顯式棧更便于控制。bool findCycleIterative(int start) { stackpairint, int stk; // pair當(dāng)前節(jié)點, 下一個要訪問的鄰居索引 vectorint visited(N 1, 0); vectorint path; stk.push({start, 0}); visited[start] 1; // 需要額外數(shù)據(jù)結(jié)構(gòu)記錄路徑上每個節(jié)點的前驅(qū)以便回溯找環(huán) vectorint dfsParent(N 1, -1); while (!stk.empty()) { auto [u, idx] stk.top(); if (idx graph[u].size()) { int v graph[u][idx]; idx; if (v dfsParent[u]) continue; // 跳過父節(jié)點 if (visited[v] 0) { visited[v] 1; dfsParent[v] u; stk.push({v, 0}); path.push_back(v); } else if (visited[v] 1) { // 找到環(huán) cycle.clear(); int cur u; while (cur ! v) { cycle.push_back(cur); cur dfsParent[cur]; } cycle.push_back(v); reverse(cycle.begin(), cycle.end()); return true; } } else { visited[u] 2; stk.pop(); if (!path.empty()) path.pop_back(); } } return false; }迭代實現(xiàn)的邏輯更復(fù)雜但避免了遞歸深度限制。它顯式地維護(hù)了一個路徑棧path變量或通過dfsParent回溯當(dāng)遇到已訪問且在當(dāng)前路徑中的節(jié)點時通過父節(jié)點鏈回溯構(gòu)建環(huán)。6. 性能分析與算法選擇策略面對不同的場景和數(shù)據(jù)規(guī)模選擇哪種方法算法時間復(fù)雜度空間復(fù)雜度優(yōu)點缺點適用場景拓?fù)渑判騉(N E)O(N E)思路簡單代碼易寫效率穩(wěn)定僅適用于基環(huán)樹或類似可“剝皮”的圖競賽題如本題、依賴分析中剔除葉子節(jié)點DFS遞歸O(N E)O(N) (遞歸棧)通用性強可找任意環(huán)能記錄路徑遞歸深度可能受限需要仔細(xì)處理父節(jié)點和狀態(tài)通用圖環(huán)檢測、需要輸出環(huán)路徑、圖規(guī)模不大DFS迭代O(N E)O(N E)無遞歸深度限制可控性強代碼復(fù)雜度高需要手動維護(hù)棧和狀態(tài)圖深度很大、避免遞歸棧溢出并查集判環(huán)O(E α(N)) 找全環(huán)需額外O(NE)O(N)判斷環(huán)存在極快添加邊時實時判環(huán)找出整個環(huán)需要額外操作實現(xiàn)稍復(fù)雜需要動態(tài)加邊并實時判斷是否成環(huán)的場景如Kruskal算法對于本題藍(lán)橋杯“發(fā)現(xiàn)環(huán)”的選擇建議首選拓?fù)渑判蛲昝榔鹾项}目“基環(huán)樹”的條件代碼不到50行邏輯清晰幾乎不可能寫錯運行效率高。這是競賽中的“正解”。練習(xí)通用性選DFS如果你旨在掌握更通用的算法練習(xí)DFS方法很有價值。注意處理無向圖回邊判斷的陷阱。并查集在本小題中優(yōu)勢不大因為需要額外步驟找全環(huán)。性能實測心得在N達(dá)到10^5級別時幾種O(NE)的算法都能輕松通過。拓?fù)渑判虻某?shù)項通常更小。DFS遞歸需要注意系統(tǒng)??臻g在極端情況下比如一條長鏈可能爆棧但藍(lán)橋杯評測環(huán)境通常??臻g足夠。穩(wěn)妥起見寫遞歸DFS時可以加上#pragma指令或改用迭代棧。7. 常見“坑點”與調(diào)試技巧即便理解了算法實現(xiàn)時也可能掉進(jìn)坑里。下面是我和學(xué)生們在練習(xí)中總結(jié)的常見問題。7.1 輸入與初始化坑頂點編號從1開始很多題目頂點編號是1~N但開發(fā)者習(xí)慣從0開始。創(chuàng)建數(shù)組時一定要是N1的大小訪問時注意下標(biāo)。這是最常見的運行時錯誤數(shù)組越界來源。多組數(shù)據(jù)未重置如果題目包含多組測試數(shù)據(jù)本題通常只有一組務(wù)必在每組數(shù)據(jù)開始前清空全局的graph、degree、visited等數(shù)組。否則上一組數(shù)據(jù)會污染下一組。鄰接表存儲重復(fù)邊題目雖未明確但通常保證無重邊。如果有重邊鄰接表存儲時需要去重或特別處理否則可能影響度的計算。7.2 算法邏輯坑DFS中忽略父節(jié)點判斷這是最大的坑。無向圖DFS訪問鄰居時必須判斷if (v parent[u]) continue;。否則程序會將A-B和B-A誤判為環(huán)導(dǎo)致死循環(huán)或錯誤結(jié)果。拓?fù)渑判蜿犃谐跏蓟欢ㄒ阉谐跏级葹?的節(jié)點都入隊而不是邊排序邊入隊。漏掉一個可能導(dǎo)致剝離過程無法進(jìn)行到底。環(huán)節(jié)點判斷條件拓?fù)渑判蚝笈袛喹h(huán)節(jié)點的條件是degree[i] 1而不是degree[i] 2。原因前面已解釋。環(huán)的輸出順序題目要求升序輸出。用拓?fù)渑判蚍ǖ玫降沫h(huán)節(jié)點列表是無序的必須排序。DFS法得到的環(huán)路徑可能有順序但起點不一定是最小值也需要排序。7.3 調(diào)試與驗證技巧小數(shù)據(jù)手工模擬對于N5,6的小圖在紙上畫出圖手動模擬算法的每一步如拓?fù)渑判騽児?jié)點DFS遞歸棧與程序輸出對比。這是最有效的debug方法。打印中間狀態(tài)在懷疑的地方打印關(guān)鍵變量。例如在拓?fù)渑判蛑忻繌年犃袕棾龉?jié)點時打印節(jié)點編號和當(dāng)前所有節(jié)點的度在DFS中進(jìn)入和退出遞歸時打印節(jié)點和父節(jié)點信息。構(gòu)造特殊測試用例最小環(huán)N3邊為1-2, 2-3, 3-1。測試基本功能。環(huán)帶長鏈一個環(huán)其中一個節(jié)點連接一條很長的鏈。測試DFS遞歸深度和拓?fù)渑判虻膫鞑?。星型圖加一邊一個中心節(jié)點連接所有其他節(jié)點再加一條邊連接兩個葉子節(jié)點形成環(huán)。測試算法的魯棒性。使用靜態(tài)分析工具在本地IDE中開啟所有編譯器警告如-Wall -Wextra注意是否有未初始化變量、類型不匹配等警告。8. 從競賽到工程環(huán)檢測的進(jìn)階應(yīng)用掌握基礎(chǔ)算法后我們可以看看更復(fù)雜的場景。8.1 在有向圖中檢測環(huán)拓?fù)渑判蚺cDFS這是更常見的工程需求例如任務(wù)調(diào)度、依賴解析。拓?fù)渑判騅ahn算法同樣適用。統(tǒng)計每個節(jié)點的入度。不斷將入度為0的節(jié)點移除并減少其鄰居的入度。如果最終所有節(jié)點都被移除則無環(huán)否則剩下的節(jié)點構(gòu)成了環(huán)或多個環(huán)。DFS三色法與無向圖類似但不需要判斷父節(jié)點。狀態(tài)遷移為0(未訪問) - 1(訪問中) - 2(已訪問)。如果在訪問中狀態(tài)遇到了另一個訪問中狀態(tài)的節(jié)點就存在環(huán)。8.2 找出圖中所有的環(huán)這是一個更難的問題找所有簡單環(huán)。常用算法是Johnson算法基于DFS的回溯剪枝時間復(fù)雜度很高。在大多數(shù)工程場景中我們只關(guān)心是否存在環(huán)或者找出一個環(huán)即可。8.3 處理權(quán)值與更復(fù)雜的目標(biāo)有時環(huán)本身不是目標(biāo)目標(biāo)是環(huán)上的某些屬性。例如尋找最小權(quán)值環(huán)可以枚舉邊暫時刪除該邊后求最短路徑路徑長度邊權(quán)即為包含該邊的環(huán)的權(quán)值。取最小值。判斷負(fù)權(quán)環(huán)使用Bellman-Ford算法進(jìn)行第N次松弛操作后如果還能松弛說明存在負(fù)權(quán)環(huán)。8.4 在特定領(lǐng)域中的應(yīng)用變形數(shù)據(jù)庫中的死鎖檢測通常使用“等待圖”節(jié)點是事務(wù)邊表示事務(wù)A等待事務(wù)B釋放鎖。用DFS或拓?fù)渑判驒z測環(huán)一旦發(fā)現(xiàn)數(shù)據(jù)庫系統(tǒng)會選擇犧牲回滾其中一個事務(wù)來打破死鎖。編譯器中的循環(huán)依賴檢測將模塊視為節(jié)點依賴關(guān)系視為有向邊。在編譯或構(gòu)建開始前進(jìn)行環(huán)檢測發(fā)現(xiàn)循環(huán)依賴則報錯提示開發(fā)者重構(gòu)代碼?;剡^頭看“發(fā)現(xiàn)環(huán)”這道題它就像一顆種子包含了圖論中環(huán)檢測這個龐大主題最核心的DNA。從理解題意、抽象建模到選擇并實現(xiàn)算法最后考慮邊界情況和優(yōu)化整個過程是一個完整的解決問題訓(xùn)練。我個人的體會是刷題的價值不在于記住某道題的答案而在于通過這道題掌握了一類問題的思考方法和工具。下次當(dāng)你遇到任何需要檢測“循環(huán)”、“回路”、“閉環(huán)”的場景時希望你能立刻聯(lián)想到今天討論的這些策略這才是真正的舉一反三。