1. 为什么叫“更弱智的算法学习”先解释一下这个标题。day50意思是我已经连续学了50天算法。至于“更弱智”这算是一种自我提醒算法这条路上最容易犯的错就是高估自己以为看懂了就是会了以为能跑通就是理解了。我给自己起的这个名字就是想每天开写之前先放低姿态承认自己仍然很弱然后老老实实把基础补扎实。这个系列其实不是什么高深教程。我给自己定的要求就三条第一每天至少解决一个算法问题并且能讲清楚思路第二每周挑一个算法做深度拆解把原理、代码、应用场景都串起来第三每十天回头把容易混淆的算法做一次对比总结。50天下来虽然离“大神”还差得远但回头翻这些笔记确实能看出自己哪里进步了哪里还在原地打转。如果你也在学算法或者正准备开始这篇内容会比较适合你。我不会堆一堆数学公式吓唬人也不会只丢代码让你自己悟而是把那些热搜词里频繁出现的算法——排序、KMP、贪心、动态规划、模拟退火、粒子群、PID——逐个聊一遍讲讲我实际怎么理解、怎么用、踩过哪些坑。学算法这件事最大的门槛其实不是智商而是方法。方法对了普通人也能扎扎实实往前走。这个day50的笔记记录了我在这个节点上对算法学习整体路线的反思也包含了最近一周实际练习的几个重点题和算法实现。希望能给同样在坚持的你一点参考。2. 50天算法路线的整体设计与思路拆解2.1 先定框架再填细节前10天我基本在“乱学”今天看到一个冒泡排序觉得挺简单写一遍就过明天看到KMP被next数组绕晕又跳到别的。结果就是学了忘、忘了学进度看似很快实际上什么都没留下。后来我换了个思路先搭一个覆盖主要算法类型的框架把这个框架当成一张地图。每个算法先学归类再学高频考法最后学工程应用。这样每天学什么、学到什么程度都心中有数。我的50天分成了五个阶段第1-10天基础数据结构与排序数组、链表、栈、队列以及冒泡、选择、插入、归并、快排、堆排。第11-20天字符串与查找重点啃KMP、二分查找、哈希表捎带补了Trie树。第21-30天树和图二叉树遍历、二叉搜索树、最短路径Dijkstra、Floyd、拓扑排序。第31-40天暴力搜索与优化回溯、贪心、动态规划、剪枝这是最痛苦也最关键的十天。第41-50天进阶与工程方向哈希、状态压缩、模拟退火、粒子群、蚁群以及PID这类控制领域的算法。回过头看这个顺序其实暗合了算法学习的规律先打牢数据结构底座再学查找和排序这些基础算法然后过渡到图论和搜索最后才是高级优化和工程应用。心急吃不了热豆腐顺序乱了后面全乱。2.2 为什么先排序后图论排序算法之所以值得花10天专门学是因为它几乎是所有算法的“基本功”。归并排序的思想引出了分治堆排序引出了优先队列和堆数据结构快速排序的partition思想被用在Top-K问题、荷兰国旗问题、快选算法里。举个例子你后面学Dijkstra最短路径如果不懂优先队列堆是怎么维护最小值的就很难理解为什么能每次O(log n)取出当前距离最短的节点。堆排序里那个下沉、上浮操作其实就是优先队列的核心。所以不是排序本身有多难而是排序算法里面藏着后续所有算法的“零件”。图论排在第三周是因为图论问题往往需要前面两章的基础。最短路径问题要求你理解邻接表、邻接矩阵、队列、堆甚至动态规划思想拓扑排序又要求你理解入度、出度、BFS。没有前面的底子直接上手图论很容易被各种概念砸晕。2.3 贪心、DP、回溯和剪枝的关系这几类算法经常被放在一起学因为它们的核心都是“搜索解空间”只是策略不同回溯把所有可能都走一遍走不通就回头典型代表是八皇后、全排列。贪心每一步都选当前看起来最优的不回头典型代表是区间调度、最小生成树。动态规划把大问题拆成重叠子问题记录中间结果避免重复计算典型代表是背包问题、最长公共子序列。剪枝不管哪种搜索提前判断某条路不可能得到答案就砍掉典型代表是数独求解、N皇后优化。以前我总觉得动态规划高不可攀后来发现它和暴力搜索其实是一家。暴力搜索之所以慢是因为重复计算太多动态规划之所以快是因为用一个表把已经算过的结果存起来。所以学DP之前我建议先老老实实把回溯写明白画递归树看重复子问题出现在哪里然后你自然就能理解为什么要用DP数组。3. 核心细节解析与实操要点3.1 排序算法不能只会写还得会选我见过不少人一提排序就能写个冒泡但问“500万条数据用什么排序”就懵了。算法学习不能停留在“能写出来”你得知道每个排序的脾气算法平均时间复杂度最坏时间复杂度空间复杂度稳定性适用场景冒泡排序O(n²)O(n²)O(1)稳定教学示范、近乎有序的短数组选择排序O(n²)O(n²)O(1)不稳定数据量小、交换代价高时插入排序O(n²)O(n²)O(1)稳定数据量小且基本有序效率极高归并排序O(n log n)O(n log n)O(n)稳定需要稳定排序、数据量大的外部排序快速排序O(n log n)O(n²)O(log n)不稳定通用排序工程中最常用堆排序O(n log n)O(n log n)O(1)不稳定需要O(1)额外空间或Top-K问题我的经验是如果你只是写业务代码语言自带的排序函数比如C的sort、Python的sorted通常已经是最好的选择它们的实现是混合策略性能很好不用担心。但如果你在面试或者刷题还是要能手撕快排和归并。因为这不仅是考排序还考递归、分治、指针操作这些基本功。快排的写法我推荐背下这段C版本它比网上一堆花哨写法更简洁也更容易记void quickSort(vectorint arr, int left, int right) { if (left right) return; int i left, j right; int pivot arr[(left right) / 2]; while (i j) { while (arr[i] pivot) i; while (arr[j] pivot) j--; if (i j) { swap(arr[i], arr[j]); i; j--; } } quickSort(arr, left, j); quickSort(arr, i, right); }注意这里pivot取的是中间元素不是第一个也不是最后一个。这样写的好处是面对基本有序的数组不容易退化成O(n²)。如果你取第一个元素做pivot而数组已经有序那每次划分都会极度不平衡递归深度就可能变成n甚至栈溢出。冒泡排序虽然性能差但我在学习初期仍然花费时间手写过。它的价值在于让你直观理解“交换相邻元素”“一轮把最大值送到末尾”这种过程。有了这个过程后面学奇偶排序、鸡尾酒排序、以及某些链表排序时理解成本都会降低很多。3.2 KMP算法和next数组别背画出来KMP是很多人的噩梦。我第一次学的时候看网上的代码两个小时没看懂后来发现关键不在代码而在你懂不懂next数组。next数组的语义是对于模式串的每个位置i它的最长相等前缀后缀长度。举个例子模式串“ABABC”它的前缀有A、AB、ABA、ABAB后缀有BABC、ABC、BC、C。到第4个字符从0开始时next[4]2因为“AB”既是前缀又是后缀。求next数组的代码其实很短vectorint buildNext(const string p) { int m p.size(); vectorint next(m, 0); for (int i 1, j 0; i m; i) { while (j 0 p[i] ! p[j]) j next[j - 1]; if (p[i] p[j]) j; next[i] j; } return next; }这里最让人迷惑的是while循环里j next[j - 1]。用生活化的类比你已经匹配了j个字符现在第j1个字符对不上你不能从头再来而是回退到之前已经匹配好的“最长相等前缀”的结尾处。这个回退过程可能不止一次所以用while。我自己学这个算法的体会是第一步先在纸上用暴力匹配推一遍定位每次失配的位置第二步把模式串所有前缀的前后缀列出来手动算出next数组第三步再对照代码看看j的回退和next数组的对应关系。这三步走完KMP才算真正通了。如果只看代码三天后必忘。3.3 贪心算法证明才是分水岭贪心算法的代码通常很短难的是证明“贪心策略是对的”。很多人学贪心时容易产生一种错觉这道题我“感觉”每次选最大的就行。但“感觉”不等于正确很多问题乍一看适合贪心实际上要用动态规划。我通常用两个标准判断局部最优是否能推出全局最优。满足这个才算具备贪心选择性质。问题是否存在重叠子问题。如果存在且子问题会互相影响大概率贪心不行。举一个经典例子活动选择问题。有n个活动每个有开始时间和结束时间问最多能参与多少个不冲突的活动。标准贪心策略是按结束时间排序每次选结束最早且与前面不冲突的活动。为什么按结束时间排序而不是按开始时间或持续时间因为结束时间越早给后面留出的时间越多这是直观上说得通的而通过归纳法可以严格证明。但换一个变体每个活动有权重现在要选权重和最大的无冲突集合贪心就直接失效了。必须用动态规划先按结束时间排序然后对每个活动二分找前面最后一个结束时间小于它开始时间的活动做状态转移。所以学贪心我的建议是每做一道题别急着写代码先把“为什么这样选是对的”写在题解旁边。哪怕写不严谨也要尝试论证。练多了你会有一种“贪心直觉”看到某些关键词就知道该用什么策略。3.4 动态规划从递归到递推的自然过渡动态规划是热搜词里出现频率最高的也是我踩坑最多的。我总结的学习路径是暴力递归 - 带备忘录的递归记忆化搜索 - 改成递推DP。以斐波那契数列为例最笨的递归写法int fib(int n) { if (n 1) return n; return fib(n - 1) fib(n - 2); }这个写法的问题是有大量重复计算。fib(5)要算fib(4)和fib(3)fib(4)又去算fib(3)和fib(2)fib(3)被算了两次。时间复杂度是O(2^n)n稍大就卡死。加一个数组缓存中间结果就是记忆化搜索vectorint memo(100, -1); int fib(int n) { if (n 1) return n; if (memo[n] ! -1) return memo[n]; return memo[n] fib(n - 1) fib(n - 2); }这已经和动态规划的核心思想一样了用空间换时间。但面试中更喜欢让你写递推版本int fib(int n) { if (n 1) return n; vectorint dp(n 1, 0); dp[0] 0; dp[1] 1; for (int i 2; i n; i) { dp[i] dp[i - 1] dp[i - 2]; } return dp[n]; }从这个例子可以看出动态规划的核心是找到状态定义找到状态转移方程找到初始条件确定遍历顺序。四步缺一不可。等到做背包问题、最长递增子序列、编辑距离这些问题时难度会陡增。我的建议是准备一个“DP模板笔记本”每道题记录四件事状态dp[i]代表什么、转移方程、初始值、答案在哪里取。50天下来这个本子是我最宝贵的复习资料。3.5 剪枝算法暴力搜索的加速器剪枝算法不是一种独立的算法而是一类优化技巧。它的核心是在搜索过程中如果一个分支已经确定不可能产生最优解或合法解就提前终止不再往下探索。经典应用场景八皇后问题放置棋子时检查是否同列、同对角线冲突冲突就回溯。数独求解每个空格只放合法数字不合法直接跳过。搜索最小操作次数如果当前步数已经大于已知最优解剪掉。从n个数中选k个数使和最大如果剩下的数全选都比当前候选小剪掉。剪枝的难点是找到好的“剪枝条件”。条件太松剪不掉多少速度提升有限条件太紧可能会剪掉真正的最优解导致答案错误。我曾经在一个“数字三角形最大路径”题里用贪心加剪枝结果答案错了原因就是我用了一个不严谨的剪枝条件把一条本来能到达最大值的路径提前砍掉了。后来学乖了剪枝条件必须要么是数学上可以证明的界要么是合法性判定不能用“感觉这个分支不会更好”来剪。这个教训价值很高。4. 实操过程与核心环节实现4.1 用C手写归并排序归并排序是分治思想的典型代表它非常适合用来练习递归和数组操作。我自己的实现如下并且在每一步都加了注释void merge(vectorint arr, int left, int mid, int right) { vectorint temp(right - left 1); int i left, j mid 1, k 0; while (i mid j right) { if (arr[i] arr[j]) temp[k] arr[i]; else temp[k] arr[j]; } while (i mid) temp[k] arr[i]; while (j right) temp[k] arr[j]; for (int p 0; p temp.size(); p) { arr[left p] temp[p]; } } void mergeSort(vectorint arr, int left, int right) { if (left right) return; int mid left (right - left) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid 1, right); merge(arr, left, mid, right); }有几个细节需要注意mid用left (right - left) / 2不用(left right) / 2是为了防止整形溢出。虽然现在多数题目数据量不至于溢出但养成好习惯总没错。merge函数里用临时数组然后再拷贝回原数组这是归并排序空间复杂度O(n)的来源。当左半边和右半边合并时用表示相等时先取左边的这样归并排序是稳定排序。如果你写相等元素的相对顺序就会变化排序就不稳定了。我当时用100万随机数测试归并排序大概需要0.3秒左右而冒泡排序在这个数据量下基本跑不完。这种性能对比能帮你建立直观感受为什么O(n²)和O(n log n)的差距不是线性增长而是指数级拉开的。4.2 手写二分查找的四种边界二分查找看起来简单但边界条件一旦写错就是死循环或者漏解。最经典的问题就在while循环条件是left right还是left right以及更新left和right时要不要加1减1。我总结了一套不容易出错的写法// 查找第一个等于target的下标不存在返回-1 int binarySearchFirst(const vectorint arr, int target) { int left 0, right arr.size(); while (left right) { int mid left (right - left) / 2; if (arr[mid] target) left mid 1; else right mid; } if (left arr.size() arr[left] target) return left; return -1; } // 查找最后一个等于target的下标不存在返回-1 int binarySearchLast(const vectorint arr, int target) { int left 0, right arr.size(); while (left right) { int mid left (right - left) / 2; if (arr[mid] target) left mid 1; else right mid; } if (left - 1 0 arr[left - 1] target) return left - 1; return -1; }这套写法的核心是维护一个左闭右开区间[left, right)循环不变量是target可能存在的区间始终是[left, right)。当arr[mid] target说明mid及其左边都不可能所以left mid 1否则right mid。初学阶段我不建议同时记多种边界写法只记一套左闭右开的模板就够了。等用熟了再看其他写法也能看懂。最忌讳的是今天背一种、明天换一种结果每次写都犹豫。4.3 Dijkstra最短路径理解还是背板Dijkstra算法是图论里高频出现的算法相关热搜词里“最短路径算法”“Dijkstra算法”都排在里面。它的核心是贪心动态规划每次从未确定最短距离的点中选一个距离最小的然后松弛它的所有出边。用优先队列实现的Dijkstravectorint dijkstra(vectorvectorpairint,int graph, int start) { int n graph.size(); vectorint dist(n, INT_MAX); dist[start] 0; priority_queuepairint,int, vectorpairint,int, greater pq; pq.push({0, start}); while (!pq.empty()) { auto [d, u] pq.top(); pq.pop(); if (d dist[u]) continue; for (auto [v, w] : graph[u]) { if (dist[u] w dist[v]) { dist[v] dist[u] w; pq.push({dist[v], v}); } } } return dist; }这里有个细节优先队列里pair的排序是先比较第一个元素再比较第二个元素。所以我把距离放在first节点编号放在second。用greater表示小顶堆每次取出的都是距离最小的节点。那个if (d dist[u]) continue是优化的关键。因为同一个节点可能被多次加入优先队列当我们从堆里取出它时如果当前记录的距离已经比这个旧值更小说明这一条是过期数据直接跳过。没有这个判断代码也能跑但会重复处理大量无效节点性能差很多。我在实际练习中发现Dijkstra最大的坑是“图有没有负权边”。只要存在负权边Dijkstra就不可靠因为负权边可能导致“先经过一个距离大的点再多走一段反而更近”的情况而Dijkstra一旦确定一个点的最短距离就不会再更新。遇到负权边应该用Bellman-Ford或SPFA。这个知识点我一开始没重视导致做带负权边的题时怎么都AC不了排查了半天才发现是算法选错了。4.4 从PID到MPPT工程算法也能学明白热搜词里出现了很多工程方向的算法PID、MPPT、混合整数线性规划、粒子群、蚁群、模拟退火等等。这些在传统算法题里不常出现但在实际工程项目里非常有用。以PID为例。PID是比例-积分-微分控制器的简称它根据目标值和当前值的误差计算出一个控制量。公式看起来简单u(t) Kp * e(t) Ki * ∫e(t)dt Kd * de(t)/dt但实际调参非常考验经验。Kp调大了系统反应快但容易震荡Ki可以帮助消除稳态误差但太大会引起超调Kd可以抑制 overshoot但对噪声敏感。我做过一个简单的温度控制模拟用增量式PID代码struct PID { double Kp, Ki, Kd; double target, lastError, integral; PID(double kp, double ki, double kd) : Kp(kp), Ki(ki), Kd(kd) { lastError 0; integral 0; } double update(double current, double dt) { double error target - current; integral error * dt; double derivative (error - lastError) / dt; double output Kp * error Ki * integral Kd * derivative; lastError error; return output; } };写这个代码不难难的是调参。网上有很多所谓的“整定口诀”比如先调P、再加I、最后加D但实际工程中往往要反复试很多组参数。我的体会是先让P足够大但不震荡再逐渐加I消除稳态误差最后加一点点D来抑制超调。这个过程至少要做几十次仿真实验。同理MPPT最大功率点跟踪算法用于光伏系统让太阳能板始终工作在最大功率输出点附近。常用的方法有扰动观察法和电导增量法。扰动观察法的原理是每过一段时间给工作电压加一个小扰动如果功率变大了就继续往这个方向扰动如果功率变小了就反方向扰动。这个逻辑简单但有效工程上很多设备用得都是它。学这类算法的时候我发现它们和纯理论算法最大的不同是一定有噪声一定有延迟一定要考虑硬件限制。书本上的完美模型到工程里往往要打八折。4.5 机器学习算法与搜索优化算法换个赛道继续刷热搜词里“机器学习算法”“深度学习算法”“强化学习算法”也出现了。我在day41到day45期间开始接触这类数据和智能优化算法因为传统的算法题做得再多和工业界之间还是有一道不小的鸿沟。比如粒子群算法PSO它模拟鸟群觅食的行为。每个粒子代表解空间里的一个候选解粒子有速度和位置每次迭代时根据个体历史最优和全局历史最优来更新速度和位置。这个算法的好处是不需要目标函数可导对很多黑盒优化问题直接可用。以C实现核心更新公式为例struct Particle { vectordouble position; vectordouble velocity; double fitness; vectordouble pBest; double pBestFitness; }; void updateParticle(Particle p, const vectordouble gBest, double w, double c1, double c2) { for (size_t i 0; i p.position.size(); i) { double r1 rand() / (double)RAND_MAX; double r2 rand() / (double)RAND_MAX; p.velocity[i] w * p.velocity[i] c1 * r1 * (p.pBest[i] - p.position[i]) c2 * r2 * (gBest[i] - p.position[i]); p.position[i] p.velocity[i]; } }这里w是惯性权重控制粒子保持原来速度的程度c1是自我认知系数c2是群体认知系数。调节这三个参数直接影响算法的收敛速度和全局搜索能力。一般来说w大一点有利于全局搜索w小一点有利于局部精细搜索。很多改进算法就是在调w的衰减策略。模拟退火算法也类似它以一定的概率接受比当前解更差的解从而跳出局部最优。这个“一定概率”通常用Metropolis准则如果新解更差概率为exp(-ΔE / T)其中T是当前温度ΔE是目标函数差值。温度高时接受差解的概率大所以前期可以四处试探温度慢慢降下来接受差解的概率越来越小最终收敛到一个稳定解。这些智能优化算法在很多传统算法搞不定的问题上很有效。但它们本质上都是“启发式”不保证找到全局最优。工程上通常要在“解的质量”和“计算时间”之间做权衡这本身也是算法设计的一门学问。5. 常见问题与排查技巧实录5.1 while循环死循环八成是边界没理清我写过很多次死循环最典型的就是二分查找和归并排序的合并过程。二分查找死循环问题往往出在left和right的更新上。比如你用left right的写法更新时写成left mid而不是left mid 1当区间缩小到两个相邻元素时mid永远等于leftleft永远不会增长于是陷入死循环。排查方法很简单挑一个只有两三个元素的小测试用例手动模拟循环过程。比如arr [1, 3], target 3走一遍就能看出问题在哪里。归并排序死循环一般不在mergeSort本身而在merge函数里某个while条件写错。比如你写while (i mid j right)但i的终止条件应该是i mid等于漏掉了中间那个元素。这种问题在数据量小的时候看不出来数据量大的时候会莫名少元素。5.2 KMP的next数组算错最容易在回退时看走眼next数组算错常见原因是“最长相等前后缀的长度”定义没理解透。记住这个长度不能等于整个子串的长度必须是真前缀和真后缀。比如模式串“A”它没有真前缀也没有真后缀所以next[0]应该是0。有些网上的版本把next[0]设为-1那是另一种KMP写法两种都对但混着看就会晕。我建议初学者先用手算把所有字符的next值写出来再对照代码验证。如果代码结果和你手算的不一致大概率是代码里j的回退逻辑没写对。这段代码需要反复理解它是KMP的灵魂。5.3 动态规划数组越界初始化是最常见的锅动态规划的边界条件非常多。最经典的锅是dp数组长度为n但你访问了dp[n]或者访问了dp[-1]。解决办法就是“在循环里多写几个if判断”或者在初始化时把dp数组长度设为n1把下标0当成边界哨兵。以“爬楼梯”问题为例一次可以爬1级或2级问爬到n级有多少种方法。定义dp[i]为爬到第i级的方法数dp[0]1, dp[1]1然后dp[i] dp[i-1] dp[i-2]i2。这样初始化dp[1]时就不会越界。如果你一开始就定义dp[0]0那dp[2]就会算错因为dp[2] dp[1] dp[0] 10但实际上到第2级有2种方法。这类问题在LeetCode上有太多变体每次做错我都会把错误的那种初始化方式也写在题解旁边提醒自己下次别犯。5.4 贪心不适用时答案不对检查是否有反例有一次做“跳跃游戏”题目要求判断能否从数组第一个位置跳到最后一个位置每个元素代表最大跳跃长度。我的第一反应是贪心每次跳最大步数。但很快发现反例数组[3, 2, 1, 0, 4]按最大步数跳第一步跳3到0就停了但实际上可以第一步只跳1步到2再跳到4。所以贪心失败。正确做法是动态维护一个“当前能到达的最远位置”遍历过程中不断更新如果某时刻i 最远位置说明无法继续前进。这本质上是“模拟贪心”的思路但和“每次跳最大步数”有本质区别。我的经验是只要贪心策略能被一个简单反例推翻就赶紧换思路不要死磕。如果能想到两个以上反例可能这个问题的正确答案是动态规划或BFS。5.5 递归栈溢出数据量大时优先转递推递归写起来爽但深度一大就爆炸。C默认栈空间一般只有8MB如果递归深度超过几十万次就会栈溢出。而Python的默认递归深度甚至只有1000层。所以写递归前先估算递归深度。如果深度可能超过1万建议改成显式栈或者递推。比如树的的前序遍历用递归可能很自然但树是链状结构时递归深度会达到节点数风险很高改成迭代栈实现就安全了。我一般会在代码里加一个计数器测试时打印一下最大递归深度这样能直观看到深度量级。如果太深直接重构成递推版本省得线上崩了再修。6. 50天经验汇总与后续规划6.1 值得保留的学习习惯这50天有几个习惯帮了我大忙也推荐给你第一每学一个算法至少写三种表述文字描述、伪代码、可运行代码。这样能逼着自己从不同角度理解一个算法而不只是“眼熟”。第二每天坚持做一道“复习题”而不是“新题”。每当学新算法学到烦我就回过头做一道排序或二分的简单题找回手感。这种“新旧交替”的节奏能有效避免学习疲劳。第三每10天做一次横向对比。比如把快排、归并、堆排放在一起从时间复杂度、稳定性、空间复杂度、实际性能四个维度比较。这种对比表能让你对每个算法的定位非常清晰。第四给每道题写“坑位记录”。很多题会反复错而且错的位置惊人地相似比如二分边界、DP初始化、图的邻接表构建。把这些坑记录下来下次遇到类似题目时直接翻效率提升明显。6.2 目前掌握的工具链除了算法本身我还不断优化自己的“工具链”保证学习效率刷题平台主要在LeetCode和洛谷。前者适合经典题和面试题后者适合数据结构和大数据量训练。调试工具C用Visual Studio Code gdbPython用pdb。调试不是“不会写才用”而是“想看到中间状态”时必备的手段。尤其是在解决递归和DP问题时用调试器观察变量变化比printf加日志高效得多。画图工具Excalidraw和draw.io。遇到树、图、DP状态转移先在图上画出来再动手写代码。算法题目里的很多坑画出来一眼就能看到。算法可视化网站VisuAlgo。它把排序、DFS/BFS、最短路过程做成动画非常适合建立直觉。6.3 未来30天的规划50天只是一个阶段后面我给自己排了这些内容第51-60天系统刷动态规划专题从基础背包到状态压缩DP目标是能独立解决中等难度的DP题。第61-70天刷字符串专题扩展KMP、manacher、AC自动机这些是热搜词“kmp算法”后面自然延伸的方向。第71-80天刷图论专题重点攻最小生成树、网络流、二分图匹配配合工程题做练习。第81-90天复习和复盘把前80天的笔记全部重写一遍每个算法改成更简洁版本的笔记并且用“费曼学习法”向自己讲解一遍。这个规划不算激进但足够扎实。我宁愿慢一点也不愿意囫囵吞枣。7. 一些想对同行者说的心里话50天过去我最真实的感受是学算法没有捷径但它也不是靠蛮力。每天一两个小时认真写代码、画图、做对比坚持下去效果自然会出现。这50天里我崩溃过很多次。印象最深的是第17天一个KMP问题我整整花了三个晚上只能看别人题解完全写不出来。后来我放下键盘拿起纸笔把模式串的每个前缀都画了一遍画到第三遍的时候突然就通了。那一刻的爽感比打游戏赢一把强太多。所以我特别想说遇到卡壳不要第一时间怀疑自己智商。大概率只是没找到合适的角度或者基础知识点有缺漏。这时候不妨后退一步把相关的基础内容补一补再回来继续。算法学习是一场马拉松一时的快慢不重要能不能持续才重要。最后再分享一个小技巧我会在每个周末把这一周学过的所有算法名字写在一张小卡片上随机抽一个然后凭记忆讲出它的思想并写一段代码实现。写不出来就再学一遍。这种“随机检验法”比按顺序复习更能暴露自己的薄弱点我强烈推荐你也试试。day60的时候我再来更新这个系列。如果这篇内容对你有一点帮助那这50天就算没白费。我们一起继续。
阅读完成 · 觉得有帮助?