
1. 項目概述一次算法競賽的深度復盤提起“藍橋杯”在國內的程序員圈子里尤其是學生群體和算法愛好者中幾乎無人不曉。它早已從一個單純的軟件和信息技術專業人才大賽演變成了檢驗個人算法與編程基本功的“試金石”。而國賽更是這場年度技術盛宴的巔峰對決。今天我想和大家深入聊聊2020年第十一屆藍橋杯國賽的C B組賽題。這不僅僅是一次對過往題目的回顧更是一次站在參賽者與出題人雙重角度下的技術拆解。對于正在備賽的同學你可以從中窺見國賽的命題風格、難度階梯以及那些隱藏在題目背后的、對時間復雜度和空間復雜度的極致要求對于已經工作的開發者這或許能幫你重溫那種在有限時間內用清晰邏輯和扎實代碼解決復雜問題的“競技狀態”這種能力在解決實際工程中的性能瓶頸和復雜邏輯時同樣珍貴。2020年的這場國賽身處一個特殊的時期很多選手是在線上完成比賽的這本身就對比賽環境和心理素質提出了不同以往的要求。C B組的題目一如既往地涵蓋了從模擬、枚舉、搜索、動態規劃到數論、圖論等經典算法領域但每一道題都經過了精心的“包裝”和“設障”。直接看題面可能覺得似曾相識但上手實現時才會發現處處是細節步步有陷阱。接下來我將以一名老選手兼出題觀察者的視角帶大家重新走進這套題目不僅給出“怎么做”的參考更重點剖析“為什么這么做”以及“如何想到這么做”并分享一些在高壓比賽環境下的實戰技巧與避坑指南。2. 賽題整體風格與解題策略總覽2.1 難度分布與核心考點解析縱觀2020年C B組的整套題目其難度呈現出典型的“紡錘形”結構。開頭幾題側重于基礎邏輯和精密計算用于穩定軍心和熱身中間部分則集中了整場考試的核心區分度題目涉及深度優先搜索DFS、廣度優先搜索BFS、動態規劃DP的經典變形以及一些需要數學思維的問題最后的壓軸題則往往需要綜合運用多種算法知識或者對某個經典模型有深刻的理解才能解決。這一年國賽的一個顯著特點是“重思維更重實現”。很多題目在思維上突破后代碼實現的細節決定了最終的得分。例如一道關于矩陣路徑或者狀態壓縮的題目可能思路并不算奇詭但如何高效地表示狀態、如何進行記憶化搜索、如何剪枝以避免超時這些實現上的技巧成為了關鍵。另一個特點是“對邊界條件和特殊情況的考察極為嚴格”。題目中常常會設置數據范圍上的“坑”比如最大值最小值、整型溢出、浮點數精度等問題稍有不慎就會丟分。對于參賽者而言一套有效的解題策略至關重要。我的建議是“先通覽后深耕保簡單爭難題”。拿到試題后花5-10分鐘快速瀏覽所有題目對每道題的題意、數據范圍和可能涉及的算法有一個初步判斷。優先解決那些一眼就有思路、或者屬于經典模板題的題目確保這些分數穩穩到手。這不僅能建立信心也能為后續攻克難題節省出寶貴時間。對于中等難度的題目要仔細分析畫出草圖列舉小規模樣例確保思路完全正確后再開始編碼。對于難題不要輕易放棄至少寫出暴力搜索的解法如果數據范圍允許或者嘗試找出規律爭取部分分數。2.2 環境準備與編碼習慣工欲善其事必先利其器。雖然比賽環境通常是固定的如Windows下的Dev-C或Linux下的G但在日常練習中養成一套高效的編碼習慣能讓你在賽場上如虎添翼。1. 頭文件與模板準備比賽時提前準備好一個包含常用頭文件和宏定義的模板可以節省大量時間。一個基礎的C模板可能如下#include iostream #include cstdio #include cstring #include algorithm #include vector #include queue #include set #include map #include cmath using namespace std; typedef long long ll; const int INF 0x3f3f3f3f; const int MAXN 1e5 10; // 根據題目常見數據范圍調整 int main() { // 關閉同步提升cin/cout速度但之后不能混用scanf/printf ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); // 你的代碼邏輯 return 0; }注意使用ios::sync_with_stdio(false);后C的流操作會變快但切記不能再與C標準的scanf,printf混用否則可能導致輸出順序錯亂。2. 調試與測試技巧靜態查錯編碼時對于循環變量、數組下標、條件判斷等要格外小心。例如for (int i 0; i n; i)和for (int i 0; i n; i)往往差之毫厘謬以千里。樣例測試一定要使用題目給出的樣例進行測試并且要自己構造一些邊界情況的樣例比如 n0, n1, 數組元素全為0或全為負數等情況。輸出中間變量在無法通過樣例時在關鍵步驟輸出中間變量的值是定位bug最直接的方法。比賽結束后記得刪除這些調試輸出。3. 時間與空間復雜度估算這是算法競賽的核心技能。在確定算法后必須根據題目給出的數據范圍如 n 10^5, m 10^3估算你的算法在最壞情況下的運行次數。例如O(n^2)的算法在 n10^5 時肯定超時10^10次操作必須優化為 O(n log n) 或 O(n)。同樣要估算內存使用避免開過大的數組導致內存超限。3. 典型賽題深度剖析與實現由于無法獲取2020年國賽B組的全部原題我將結合歷年國賽的常見題型和“藍橋杯”的命題風格構建幾道具有代表性的虛擬題目進行深度剖析。這些題目融合了當年可能考察的核心考點分析過程將完全模擬實戰。3.1 例題A精密計算與模擬——“齒輪傳動比”題目描述虛擬 在一個復雜的機械系統中有 N 個齒輪排成一條直線相鄰齒輪相互嚙合。已知每個齒輪的齒數。當第一個齒輪順時針轉動一定圈數后需要計算最后一個齒輪的轉動方向和圈數用最簡分數表示。齒輪傳動規律相鄰齒輪轉動方向相反傳動比等于齒數之比的倒數。輸入第一行一個整數 N (2 ≤ N ≤ 1000)。第二行 N 個整數表示每個齒輪的齒數1 ≤ 齒數 ≤ 10^4。第三行兩個整數 a, b表示第一個齒輪順時針轉了 a/b 圈a, b 為正整數且 1 ≤ a, b ≤ 10^9。輸出輸出一行。如果最后一個齒輪順時針轉動輸出“”逆時針輸出“-”然后輸出一個空格接著輸出最后一個齒輪轉動圈數的最簡分數形式 “分子/分母”。如果結果為整數則分母為1。樣例輸入4 30 20 25 50 3 2樣例輸出- 9/20解析與實現 這道題完美體現了藍橋杯對“基礎能力”的考察——它不涉及高深算法但極其考驗選手的邏輯嚴謹性、模擬能力以及對分數運算的處理精度。1. 核心思路拆解方向判斷第一個齒輪順時針記為“”。每經過一個齒輪方向反轉一次。因此從第1個齒輪到第N個齒輪方向反轉了 (N-1) 次。如果 (N-1) 是偶數則方向相同為“”奇數則方向相反為“-”??梢杂?N-1) % 2來判斷。圈數計算傳動比是齒數之比的倒數。設齒數數組為c[]。從齒輪1到齒輪2的傳動比為c[0]/c[1]齒輪2的圈數/齒輪1的圈數。因此最后一個齒輪齒輪N的圈數相對于第一個齒輪為result (a/b) * (c[0]/c[1]) * (c[2]/c[3]) * ...注意觀察分子是a * c[0] * c[2] * ...分母是b * c[1] * c[3] * ...。即所有奇數索引從0開始的齒數在分子所有偶數索引的齒數在分母再乘上初始的 a 和 b。分數化簡計算出的分子分母可能非常大最大可達 (10^9) * (10^4)^500遠超64位整數但題目數據范圍暗示我們最終結果需要化簡。這里的關鍵是在連乘的過程中不斷約分而不是先算出巨大整數再求最大公約數GCD后者會導致溢出。2. 代碼實現與細節#include iostream #include vector #include algorithm using namespace std; // 使用輾轉相除法求最大公約數 long long gcd(long long a, long long b) { return b 0 ? a : gcd(b, a % b); } int main() { ios::sync_with_stdio(false); cin.tie(0); int N; cin N; vectorlong long teeth(N); for (int i 0; i N; i) { cin teeth[i]; } long long a, b; cin a b; // 1. 判斷方向 char direction ((N - 1) % 2 0) ? : -; // 2. 計算最終圈數分數邊乘邊約分 long long numerator a; // 分子 long long denominator b; // 分母 // 齒輪傳動比連乘 for (int i 0; i N - 1; i) { // 根據推導第i個齒輪對第i1個齒輪的影響 // 如果i是偶數 teeth[i] 乘到分子teeth[i1]乘到分母 // 如果i是奇數 teeth[i] 乘到分母teeth[i1]乘到分子 // 但更簡單的理解從齒輪1到齒輪N的總傳動比 (c[0]/c[1]) * (c[2]/c[3]) * ... // 即下標為偶數的在分子下標為奇數的在分母從0開始計數 // 注意最后一個齒輪的齒數 c[N-1] 不參與連乘不對仔細分析 // 齒輪1-2: 比例 c0/c1 // 齒輪2-3: 比例 c1/c2? 錯誤應該是 c2/c1? 不對。 // 正確傳動相鄰齒輪傳動比 驅動輪齒數 / 被動輪齒數 這里題目定義為“齒數之比的倒數”。 // 設齒輪i齒數Ci齒輪j齒數Cji驅動j則 j的圈數/i的圈數 Ci/Cj。 // 因此從齒輪1到齒輪N圈數_N 圈數_1 * (C0/C1) * (C2/C3) * (C4/C5) * ... ? 這不對因為齒輪2同時是前一次的被動輪和后一次的驅動輪。 // 讓我們重新嚴謹推導設圈數為R齒數為C。 // R1 * C1 R2 * C2 (因為嚙合點線速度相同且齒數比等于周長比) // 所以 R2 R1 * (C1/C2) // 同理 R3 R2 * (C2/C3) R1 * (C1/C2) * (C2/C3) R1 * (C1/C3) // R4 R3 * (C3/C4) R1 * (C1/C3) * (C3/C4) R1 * (C1/C4) // 因此規律是R_last R_first * (C_first / C_last) // 方向每傳動一次反向所以方向與 (N-1) 的奇偶性相關。 // 所以我們不需要循環連乘直接計算即可。 } // 根據上述推導代碼可以簡化為 long long final_numerator a * teeth[0]; long long final_denominator b * teeth[N-1]; // 3. 化簡分數 long long g gcd(final_numerator, final_denominator); final_numerator / g; final_denominator / g; // 4. 輸出 cout direction final_numerator / final_denominator endl; return 0; }實操心得這道題在思路上給了我們一個深刻的教訓——不要急于編碼必須先用小樣本如N2,3,4完全推導演算找到最簡的數學規律。最初的“連乘”思路是思維定勢通過嚴謹推導發現結果是簡潔的(a*C0)/(b*C_{last})。這節省了大量計算也避免了中間結果溢出的風險。在競賽中這種“數學化簡”的能力往往比編碼能力更重要。3.2 例題B搜索與剪枝——“迷宮寶藏”題目描述虛擬 一個大小為 N x M 的迷宮每個格子可能是墻‘#’、路‘.’、起點‘S’、終點‘E’或寶藏‘T’數量不超過10。從起點出發找到達終點的最短路徑并且需要收集所有寶藏。每次可以向上、下、左、右四個方向移動到非墻的相鄰格子移動計數為1。求滿足條件的最短路徑長度。如果無法做到輸出-1。輸入第一行兩個整數 N, M (1 ≤ N, M ≤ 50)。接下來 N 行每行 M 個字符描述迷宮。保證恰有一個‘S’和一個‘E’寶藏‘T’的數量 K1 ≤ K ≤ 10。輸出一個整數表示最短路徑長度。樣例輸入5 5 S.... .##.. .##.. .##.. ...TE樣例輸出12解析與實現 這是一道典型的狀態壓縮廣度優先搜索BFS題目。如果只是求起點到終點的最短路徑標準BFS即可。但加入了“收集所有寶藏”的條件后狀態就不僅僅是坐標 (x, y) 了還需要記錄當前已經收集了哪些寶藏。1. 核心思路拆解狀態定義狀態 (x坐標, y坐標, 寶藏收集狀態)。我們可以用一個整數的二進制位來表示寶藏收集情況。例如有K個寶藏那么狀態數就是 N * M * (2^K)。當K10時2^101024總狀態數約為 50501024 2.5e6在BFS的可行范圍內。搜索過程從起點狀態 (sx, sy, 0) 開始BFS。每次向四個方向擴展如果新坐標合法且不是墻則判斷新坐標如果是寶藏‘T’更新狀態new_state old_state | (1 treasure_id)。需要預先給每個寶藏一個唯一的ID0到K-1。如果是終點‘E’檢查當前狀態new_state是否等于(1K)-1即所有寶藏位都為1。如果是則找到了滿足條件的最短路徑。如果是普通路‘.’或其他狀態不變。剪枝與優化使用一個三維數組vis[N][M][1K]來記錄每個狀態是否被訪問過避免重復搜索。2. 代碼實現與細節#include iostream #include queue #include cstring #include vector using namespace std; struct State { int x, y; // 坐標 int mask; // 寶藏收集狀態掩碼 int step; // 已走步數 State(int _x, int _y, int _m, int _s) : x(_x), y(_y), mask(_m), step(_s) {} }; int dirs[4][2] {{-1,0}, {1,0}, {0,-1}, {0,1}}; int main() { ios::sync_with_stdio(false); cin.tie(0); int N, M; cin N M; vectorstring maze(N); int sx, sy, ex, ey; vectorpairint, int treasures; for (int i 0; i N; i) { cin maze[i]; for (int j 0; j M; j) { if (maze[i][j] S) { sx i; sy j; } else if (maze[i][j] E) { ex i; ey j; } else if (maze[i][j] T) { treasures.push_back({i, j}); } } } int K treasures.size(); // 給寶藏編號并記錄坐標到ID的映射便于快速查找 vectorvectorint treasure_id(N, vectorint(M, -1)); for (int id 0; id K; id) { int tx treasures[id].first, ty treasures[id].second; treasure_id[tx][ty] id; } // BFS queueState q; // 訪問標記數組維度為 N * M * (1K) vectorvectorvectorbool vis(N, vectorvectorbool(M, vectorbool(1K, false))); q.push(State(sx, sy, 0, 0)); vis[sx][sy][0] true; int ans -1; while (!q.empty()) { State cur q.front(); q.pop(); // 如果到達終點并且收集了所有寶藏 if (cur.x ex cur.y ey cur.mask ((1K)-1)) { ans cur.step; break; } for (int d 0; d 4; d) { int nx cur.x dirs[d][0]; int ny cur.y dirs[d][1]; if (nx 0 || nx N || ny 0 || ny M) continue; if (maze[nx][ny] #) continue; int new_mask cur.mask; // 檢查新位置是否是寶藏 int tid treasure_id[nx][ny]; if (tid ! -1) { new_mask | (1 tid); } if (!vis[nx][ny][new_mask]) { vis[nx][ny][new_mask] true; q.push(State(nx, ny, new_mask, cur.step 1)); } } } cout ans endl; return 0; }注意事項狀態壓縮BFS的關鍵在于狀態的設計和表示。mask這個整數巧妙地用二進制位記錄了集合信息。在競賽中遇到“需要記錄經過某些特定點或收集某些物品”的最短路問題狀態壓縮DP或BFS是標準解法。另外vis數組一定要開夠維度并且用vector動態創建時要注意內存本題N,M≤50K≤101K最大1024總大小約505010242.5M個bool在內存限制內。3.3 例題C動態規劃與優化——“乘積最大子序列”題目描述虛擬 給定一個長度為 N 的整數序列包含正數、負數和零找出一個連續子序列至少包含一個數使得該子序列中所有數的乘積最大。輸出這個最大的乘積。由于結果可能很大要求輸出結果除以 (10^97) 的余數。注意這里的乘積是數學上的乘積不是異或。輸入第一行一個整數 N (1 ≤ N ≤ 10^5)。第二行 N 個整數每個數的絕對值不超過 10^4。輸出一個整數表示最大乘積模 10^97 的結果。樣例輸入5 2 3 -2 4 -1樣例輸出48解析與實現 這是經典的“乘積最大子數組”問題是“最大子序和”問題的升級版也是動態規劃的經典例題。難點在于負數乘以負數會變成正數因此不能只維護一個最大值。1. 核心思路拆解狀態定義設dp_max[i]表示以第 i 個元素結尾的連續子序列的最大乘積。dp_min[i]表示以第 i 個元素結尾的連續子序列的最小乘積可能是負數。狀態轉移方程對于每個新來的數字nums[i]有三種選擇自己單獨成為一個子序列接在dp_max[i-1]后面接在dp_min[i-1]后面。因此dp_max[i] max(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])dp_min[i] min(nums[i], dp_max[i-1] * nums[i], dp_min[i-1] * nums[i])最終的答案就是所有dp_max[i]中的最大值。模運算處理由于結果要對 MOD1e97 取模而轉移方程中有乘法和比較大小。不能先取模再比較因為取模后大小關系可能改變。一種方法是使用long long類型暫存中間結果在比較出最大值/最小值后再對結果取模存儲。但需要注意乘積可能溢出long long當 N 很大且數字絕對值也大時。更穩妥的方法是使用__int128如果編譯器支持或高精度但競賽中通常數據會避免這種情況或者要求輸出取模后的值比較時用原始值。這里我們假設數據范圍下long long足夠。2. 代碼實現與細節#include iostream #include vector #include algorithm using namespace std; const int MOD 1e9 7; int main() { ios::sync_with_stdio(false); cin.tie(0); int N; cin N; vectorint nums(N); for (int i 0; i N; i) { cin nums[i]; } // 初始化注意用long long long long dp_max nums[0]; long long dp_min nums[0]; long long ans nums[0]; // 最終答案 for (int i 1; i N; i) { long long num nums[i]; // 由于dp_max和dp_min在下一步會被更新需要先用臨時變量保存舊值 long long temp_max dp_max; long long temp_min dp_min; // 狀態轉移 dp_max max(num, max(temp_max * num, temp_min * num)); dp_min min(num, min(temp_max * num, temp_min * num)); // 更新全局答案 if (dp_max ans) { ans dp_max; } } // 輸出答案對MOD取模的結果注意ans可能為負數需要先處理 // 但根據題意乘積最大ans應該不會是負數除非整個序列都是負數且個數為奇數此時最大乘積也是負數。 // 題目要求輸出模MOD的結果在C中負數取模需要調整到正數范圍。 long long output ans % MOD; if (output 0) output MOD; cout output endl; return 0; }避坑技巧這道題有兩個極易出錯的地方。第一是狀態轉移時dp_max和dp_min的舊值被覆蓋必須用臨時變量保存否則計算dp_min時用的dp_max已經是新值了。第二是取模與比較的順序。絕對不能先對temp_max * num取模再比較因為取模后數字變小可能影響最大值判斷。正確的做法是全程用long long或更大類型進行運算和比較只在最終輸出前取模。另外當序列中有0時這個算法也能正確處理因為max(0, ...)和min(0, ...)會自然將0納入考慮。4. 備賽策略與臨場問題排查4.1 長期備賽路線圖想要在藍橋杯國賽中取得好成績臨時抱佛腳是遠遠不夠的。需要一個系統性的、長期的訓練計劃。第一階段鞏固基礎1-2個月語言熟練度確保對C標準庫STL了如指掌。重點掌握vector,string,queue,stack,set/multiset,map/multimap,priority_queue以及algorithm頭文件下的sort,lower_bound,upper_bound,next_permutation等函數。不僅要會用還要清楚其時間復雜度。基礎算法徹底理解并能夠手寫實現排序快速排序、歸并排序、二分查找、遞歸、簡單動態規劃如背包問題、深度優先搜索DFS和廣度優先搜索BFS。這是所有復雜算法的基石。第二階段專題突破3-4個月分專題刷題針對藍橋杯常考考點進行集中訓練。搜索DFS、BFS、回溯、剪枝。練習迷宮問題、八皇后、數獨等。動態規劃線性DP、區間DP、樹形DP、狀態壓縮DP。從經典模型背包、LIS、LCS開始逐步過渡到復雜變形。圖論最短路Dijkstra, Floyd, SPFA、最小生成樹Kruskal, Prim、拓撲排序。數論最大公約數、最小公倍數、素數篩、快速冪、模運算。數據結構并查集、樹狀數組、線段樹。工具在洛谷、力扣、AcWing等OJ上找到相應的專題集進行練習。每做完一道題務必查看題解學習最優解并總結此類題目的套路。第三階段真題模擬與綜合訓練1-2個月限時模擬找歷年國賽、省賽真題嚴格按照比賽時間通常4小時進行全真模擬。這能有效提升時間管理能力和抗壓能力。錯題復盤建立自己的錯題本。不僅記錄錯題還要分析錯誤原因是思路錯誤、細節疏忽如邊界條件、算法復雜度估計錯誤還是代碼實現bug針對性地彌補弱點。思維提升嘗試一題多解思考是否存在更優的算法。多參加線上的周賽、月賽鍛煉快速解題能力。4.2 臨場常見問題與應急方案即使在充分準備后賽場上也可能遇到各種突發狀況。以下是一些常見問題及應對策略問題現象可能原因排查與解決思路樣例通過提交全錯1. 邊界條件未考慮如n0,1。2. 數組開小或下標越界。3. 初始化錯誤如全局變量未重置。4. 數據類型溢出未用long long。1. 構造極端數據最小、最大、全零、負數測試。2. 檢查數組大小是否滿足最大數據范圍10的余量。3. 對于多組數據輸入檢查每組數據前是否重置了全局變量和容器。4. 檢查乘法、加法運算必要時全部升級為long long。部分測試點超時算法時間復雜度太高未滿足數據范圍要求。1. 重新分析題目數據范圍估算你的算法最壞復雜度。2. 思考是否存在更優算法如O(n^2)優化為O(n log n)。3. 檢查循環中是否存在重復計算能否用前綴和、哈希表等預處理。4. 對于搜索題剪枝是否充分部分測試點答案錯誤邏輯存在漏洞對題目理解有偏差。1. 重新仔細讀題注意“連續”與“非連續”、“恰好”與“至少”等關鍵詞。2. 用自己構造的小數據手動模擬你的算法過程與暴力枚舉如果可能的結果對比。3. 輸出中間過程觀察在哪一步開始出現偏差。編譯錯誤語法錯誤或編譯器版本問題。1. 檢查頭文件、分號、括號是否匹配。2. 避免使用競賽環境可能不支持的C新特性如auto在早期版本可能不支持。3. 檢查變量名是否與關鍵字沖突。運行錯誤如段錯誤幾乎肯定是數組越界、空指針訪問、遞歸過深導致棧溢出。1. 檢查所有數組訪問下標是否在[0, size-1]范圍內。2. 檢查指針或迭代器在解引用前是否有效如vector為空時訪問front()。3. 遞歸深度過大時考慮改用迭代BFS或手動棧。最后的叮囑比賽時保持平和心態至關重要。遇到難題卡住時不妨先放一放去做其他有把握的題目。一道題如果想了20分鐘還沒有清晰思路先寫一個暴力解法保底再回頭思考優化。合理分配時間確保會做的題目不丟分就是勝利。國賽的題目往往比拼的不僅是知識儲備更是冷靜、細致和穩定的發揮。每一次調試每一次對邊界條件的深思都是通往獎杯的堅實臺階。