尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

算法集训营第二周:递归、DFS、BFS与动态规划核心解析

算法集训营第二周:递归、DFS、BFS与动态规划核心解析 1. 项目概述算法集训营的核心价值与第二周定位如果你是一名计算机专业的学生或者是一名正在寻求技术突破的开发者那么“算法”这个词对你来说一定既熟悉又充满挑战。熟悉是因为它无处不在是编程的基石充满挑战是因为它抽象、多变常常让人在刷题时感到迷茫和挫败。最近我深度参与并完成了“思特奇杯·云上蓝桥-算法集训营”的第二周学习这不仅仅是一次简单的刷题活动更像是一次系统性的思维重塑和实战演练。我想通过这篇分享把这一周的核心收获、解题心路历程以及那些在标准答案里不会告诉你的“坑”和“技巧”完整地呈现出来。“云上蓝桥”这个形式本身就很有意思。它打破了传统线下竞赛的时空限制让我们可以随时随地沉浸在算法的世界里。而“集训营”的设定意味着这不是一场孤军奋战的比赛而是一次有节奏、有引导、有同伴的集体学习。第二周通常是一个承上启下的关键阶段它不再停留在基础语法的温习而是开始深入数据结构和算法的核心思想比如递归、搜索、动态规划等。对于参与者而言这一周的目标非常明确在巩固第一周基础的同时开始接触并尝试解决中等难度的综合性问题为后续更复杂的挑战打下坚实的思维基础。2. 第二周核心内容与解题思路全解析第二周的题目设计明显能感觉到出题人的用心。它不再是孤立的知识点考察而是开始注重知识点的串联和实际应用场景的模拟。题目通常覆盖以下几个核心板块递归与分治、深度优先搜索DFS、广度优先搜索BFS、简单的动态规划DP思想以及一些基础数学问题和模拟题。这些内容构成了算法入门到进阶的骨架。2.1 递归与分治理解“自相似”的哲学递归是许多高级算法如DFS、回溯、分治的根基。第二周的题目里几乎一定会出现需要用递归思想解决的问题比如经典的“汉诺塔”、“全排列”或者“斐波那契数列”的变种。核心思路拆解递归的本质是“将大规模问题分解为结构相同的小规模问题”。编写递归函数时务必明确三要素递归终止条件这是防止无限递归的“安全阀”。你必须清晰地定义问题规模小到何种程度时可以直接得出答案。递归调用函数如何调用自身并且每次调用时问题的规模参数必须向终止条件逼近。返回与合并如何将小规模问题的解合并得到大规模问题的解。以“全排列”问题为例给定一个不含重复数字的数组返回其所有可能的全排列。终止条件当当前需要排列的序列长度减为1或者索引到达末尾时说明已经生成了一个排列将其加入结果集。递归调用与合并我们固定第一个位置尝试将第一个位置与后面每一个位置包括自己交换。交换后对第一个位置之后的子序列递归地进行全排列。递归返回后需要再交换回来这就是“回溯”以保证原始顺序不被破坏从而尝试下一个可能性。实操心得很多新手在写递归时容易在“回溯”步骤上犯错。记住一个原则如果你在递归调用前修改了共享的状态如交换数组元素、向路径列表添加元素那么在递归调用返回后必须将其恢复原状。这就像你进入一个房间探索探索完后应该把物品归位以便从另一个门进入时房间是初始状态。2.2 深度优先搜索DFS与广度优先搜索BFS遍历的艺术DFS和BFS是解决图、树以及矩阵类问题的两把利剑。第二周的题目往往会包含“迷宫寻路”、“岛屿数量”、“二叉树层序遍历”等经典问题。DFS深度优先搜索它的策略是“一条路走到黑”不撞南墙不回头利用递归或栈实现。非常适合寻找所有可行解、判断连通性等场景。核心技巧在矩阵类DFS中如“岛屿数量”一定要在访问过一个格子如‘1’后立即将其标记为已访问如改为‘0’或一个特殊标记。否则程序会在相邻的格子间无限循环访问导致栈溢出。这被称为“沉岛思想”或“染色法”。代码框架递归版def dfs(grid, i, j): # 1. 边界判断与终止条件 if i 0 or i len(grid) or j 0 or j len(grid[0]) or grid[i][j] ! ‘1’: return # 2. 处理当前节点如计数、标记 grid[i][j] ‘0’ # 标记为已访问 # 3. 递归访问四个方向 dfs(grid, i1, j) dfs(grid, i-1, j) dfs(grid, i, j1) dfs(grid, i, j-1)BFS广度优先搜索它的策略是“层层推进”利用队列实现。非常适合寻找最短路径、最少步数等问题。核心技巧BFS通常需要记录“层”的概念或者记录到达每个节点的步数。在队列中我们不仅可以存储节点的坐标还可以存储额外的信息如当前步数。代码框架from collections import deque def bfs(grid, start): queue deque([start]) visited set([start]) # 使用集合记录已访问节点比修改原数组更通用 steps 0 while queue: # 如果需要按层处理这里可以记录当前层的节点数 level_size len(queue) for _ in range(level_size): x, y queue.popleft() # 判断是否到达目标 if (x, y) target: return steps # 遍历四个方向 for dx, dy in directions: nx, ny x dx, y dy if 0 nx rows and 0 ny cols and (nx, ny) not in visited and grid[nx][ny] is valid: visited.add((nx, ny)) queue.append((nx, ny)) steps 1 # 一层遍历完步数加一 return -1 # 未找到目标选择DFS还是BFS一个简单的判断原则当需要找到“一个解”或“最短路径”时优先考虑BFS当需要找到“所有解”或问题与路径深度关系不大时DFS的代码通常更简洁。在第二周的题目中明确题目要求是关键。2.3 动态规划DP初探从记忆化搜索到状态转移第二周可能会引入最简单的动态规划问题比如“爬楼梯”、“最小路径和”等。对于初学者理解DP的关键在于先理解“重叠子问题”和“最优子结构”。思路演进以“爬楼梯”每次可以爬1或2阶到n阶有多少种方法为例。暴力递归f(n) f(n-1) f(n-2)。这会存在大量重复计算例如计算f(5)需要f(4)和f(3)计算f(4)又需要f(3)和f(2)f(3)被计算了多次。记忆化搜索自顶向下在递归的基础上增加一个数组memo计算f(n)前先查memo[n]是否已计算过是则直接返回否则计算并存入memo。这是最符合直觉的DP入门方式。递推自底向上我们直接从基础情况f(1)1, f(2)2开始用循环逐步计算到f(n)。这是最标准的DP写法空间和时间效率通常更高。# 递推解法 def climbStairs(n): if n 2: return n dp [0] * (n 1) # dp[i]表示到第i阶的方法数 dp[1], dp[2] 1, 2 for i in range(3, n 1): dp[i] dp[i-1] dp[i-2] return dp[n]实操要点定义清晰的dp数组含义是解决所有DP问题的第一步。问自己dp[i]到底代表什么是某个位置的状态是前i个元素的最优值这个定义必须能让你顺利地写出状态转移方程。3. 典型题目实战与代码实现细节光说不练假把式。我们挑第二周可能遇到的一道综合性较强的题目——“岛屿的最大面积”LeetCode 695来详细拆解它完美融合了DFS/BFS和矩阵处理。题目描述给定一个包含0和1的二维网格1代表陆地0代表水域。假设网格的四周都被水域包围计算网格中岛屿的最大面积。岛屿由水平或垂直方向上相邻的陆地连接而成。解题思路核心算法遍历网格中的每一个格子。触发搜索当遇到一个未被访问过的‘1’陆地时以此点为起点进行DFS或BFS目的是标记所有与之相连的陆地并统计这片连通的陆地包含多少个格子即面积。更新结果在每次完整的搜索完成后将得到的面积与当前记录的最大面积进行比较和更新。遍历完成整个网格遍历结束后记录的最大面积即为答案。DFS实现带详细注释def maxAreaOfIsland(grid): :type grid: List[List[int]] :rtype: int if not grid or not grid[0]: return 0 rows, cols len(grid), len(grid[0]) max_area 0 def dfs(r, c): # 递归终止条件越界或当前格子不是陆地 if r 0 or r rows or c 0 or c cols or grid[r][c] ! 1: return 0 # 标记当前格子为已访问沉岛 grid[r][c] 0 # 当前格子面积为1并加上四个方向探索的面积 area 1 # 方向数组代表上下左右四个移动方向 directions [(0, 1), (0, -1), (1, 0), (-1, 0)] for dr, dc in directions: area dfs(r dr, c dc) return area # 遍历整个网格 for r in range(rows): for c in range(cols): if grid[r][c] 1: # 发现新岛屿 current_area dfs(r, c) max_area max(max_area, current_area) return max_area关键细节与避坑指南原地修改标记代码中通过将访问过的1改为0来实现标记。这样做节省了额外visited数组的空间但修改了输入数据。如果题目要求不能修改原输入则需要创建一个同样大小的二维布尔数组来记录访问状态。方向数组的使用使用directions [(0,1),(0,-1),(1,0),(-1,0)]来管理四个方向的移动比写四个dfs调用更清晰也更容易扩展到八个方向如“被围绕的区域”问题。面积累加dfs函数返回的是以(r,c)为根的这片连通区域的面积。注意area的初始化是1当前格子然后累加四个方向递归调用的结果。这个递归返回值的设计是简洁实现的关键。4. 集训过程中的常见问题与调试技巧在第二周高强度的练习中我遇到了不少共性问题也总结出一些调试方法这些可能比解出某道题更重要。4.1 递归导致的栈溢出或超时这是新手最常遇到的问题。原因1缺少终止条件或终止条件永远达不到。仔细检查你的递归函数确保所有可能的分支最终都能“触底”。原因2存在环状递归调用。在图或矩阵的DFS中如果没有对已访问节点进行标记就会A访问BB又访问A形成无限循环。务必记住“标记已访问”。原因3递归深度过深。Python默认递归深度有限约1000层。对于深度可能很大的问题如链状链表递归解法可能不适合需考虑迭代如BFS或显式使用栈的DFS。调试技巧在递归函数入口打印当前参数如print(f”Calling dfs with ({r},{c})”)可以清晰看到递归的路径和深度帮助你判断是否陷入了循环。4.2 边界条件处理不当数组越界、空输入处理是另一个错误高发区。防御式编程在访问数组元素grid[i][j]之前先判断i和j是否在合法范围内。这是一个必须养成的习惯。空值判断对于函数输入尤其是列表、字符串先判断其是否为None或空if not grid:。一个健壮的程序应该能优雅地处理边缘输入。特殊测试用例自己多想想极端情况。矩阵只有一行或一列怎么办所有格子都是0或都是1怎么办输入为空怎么办在提交前用这些案例测试你的代码。4.3 算法选择错误导致效率低下最典型的例子是该用BFS求最短路径却用了DFS。DFS在寻找所有路径时可能会探索大量无效分支而BFS由于层层推进找到的第一条路径就是最短的。决策清单求最短路径/最小步数-优先BFS。求所有可能解/连通分量- DFS通常更直观。问题规模很大且DFS可能深度极深 - 考虑迭代加深搜索IDS或直接使用BFS。复杂度估算在动手前粗略估算一下最坏情况下的时间复杂度。如果矩阵是N x N一个O(N^2)的算法是可接受的但一个O(2^N)的暴力搜索很可能超时。4.4 状态管理混乱尤其在回溯和DP中回溯问题如前所述记住“恢复现场”。在递归调用前后对共享数据结构如路径列表path的修改必须成对出现append后必有pop。DP问题dp数组的初始化至关重要。dp[0]和dp[1]往往需要根据题意手动初始化。确保你的状态转移方程在i2时就能正确运行。5. 第二周学习策略与心态调整经历了第一周的热身第二周的强度和对思维的要求会明显上一个台阶。如何高效学习避免陷入“刷题机器”的误区1. 专题突破而非散点刷题集训营的题目通常是按专题编排的。集中一天或两天时间专门攻克“DFS/BFS”或“DP”专题。连续解决同类问题有助于你快速掌握这类问题的思维定式和代码模板。当你再看到矩阵、图、树的问题时脑子里能立刻浮现出几个备选的算法框架。2. 重视“复盘”胜过“AC”一道题做出来了AC只是开始。花时间复盘问自己几个问题这是最优解吗时间空间复杂度是多少有没有更优雅的写法别人的优秀题解思路是什么我卡住的地方是因为哪个知识点不熟把一道题的来龙去脉想透比稀里糊涂做对十道题更有价值。3. 建立自己的“解题笔记”我习惯用Markdown或Notion为每一类经典问题建立一个笔记页面。里面包含核心思想一两句话概括。通用代码模板Python/Java等。2-3道经典例题及其变种。我自己容易犯错的点。 这份笔记是你个人的算法秘籍在后续复习和面试前价值连城。4. 保持平和心态拥抱“不会”第二周遇到难题太正常了。动态规划想不出状态转移方程搜索剪枝毫无头绪这都是学习的一部分。重要的是当你看了题解恍然大悟后不要满足于“哦我懂了”而是合上题解自己从头到尾独立默写一遍代码并尝试向别人或自己讲解清楚。这个“输入-理解-输出”的闭环是知识内化的关键。算法学习是一场马拉松而非冲刺。第二周的集训正是在帮你打下最核心的赛段基础。那些反复调试的夜晚那些灵光一现的时刻最终都会内化成你面对复杂问题时清晰的逻辑和沉稳的心态。坚持下去你会发现解题的乐趣不仅在于那个绿色的“Accepted”更在于你思考能力那切实可见的成长。
返回列表