专业定制网站,做印刷网站公司简介,建立一个网站的费用,软件开发微信小程序这是一个非常核心的问题。#xff1a;DFS#xff08;深度优先搜索#xff09;首先是一种思想#xff0c;然后才是具体题目中的代码实现。
理解 DFS 的关键在于#xff1a;“不撞南墙不回头”。1. DFS 的本质思想
DFS 的核心逻辑是#xff1a;从一个起点出发#xff0c;选…这是一个非常核心的问题。DFS深度优先搜索首先是一种思想然后才是具体题目中的代码实现。理解 DFS 的关键在于“不撞南墙不回头”。1. DFS 的本质思想DFS 的核心逻辑是从一个起点出发选定一个方向死磕到底直到走不通了撞到边界、遇到水、或者已经访问过再退回上一步换个方向继续死磕。二叉树中是一路向下扎到叶子节点。网格岛屿中是一路向上下左右某一个方向走到底。这种“回退”的动作在代码里通过“递归”或“栈”来实现。2. DFS 的通用“三部曲”模板无论什么题目DFS 的代码结构几乎都是固定的边界判定Base Case我是不是出界了我是不是已经来过这了防止死循环标记访问Marking既然我来了就给这里打个标记免得一会儿又绕回来。递归搜索Recursion往所有可能的方向继续走。3. 为什么《岛屿数量》的操作就是 DFSimportsys# 设置递归深度限制防止大矩阵导致栈溢出sys.setrecursionlimit(200000)defsolve():# 1. 处理输入 (ACM 模式常见写法)input_datasys.stdin.read().split()ifnotinput_data:return# 获取行数和列数rowsint(input_data[0])colsint(input_data[1])# 将剩余的一维数据填充进二维网格 grid[rows][cols]grid[]foriinrange(rows):# 每一行取 cols 个元素start2i*cols grid.append(list(input_data[start:startcols]))# 2. 定义 DFS 沉岛函数defdfs(r,c):# 边界条件坐标越界 或 遇到水(0) 则返回ifr0orrrowsorc0orccolsorgrid[r][c]0:return# 沉岛操作将当前陆地标记为 0防止重复访问grid[r][c]0# 递归访问上下左右四个方向dfs(r-1,c)# 上dfs(r1,c)# 下dfs(r,c-1)# 左dfs(r,c1)# 右# 3. 遍历网格寻找岛屿island_count0forrinrange(rows):forcinrange(cols):# 只要发现一块陆地就是一个新岛屿的开始ifgrid[r][c]1:island_count1# 使用 DFS 把它连通的所有陆地全部“淹没”dfs(r,c)# 4. 输出结果print(island_count)if__name____main__:solve()对比上面的模板你看《岛屿数量》的代码逻辑起点遍历整个网格遇到1陆地就开工。死磕到底dfs(r1, c)、dfs(r-1, c)… 这就是在往四个方向死磕。撞墙回头遇到边界或0水就return。标记把1变成0。索引是从 0 开始的0-indexed。在编程中这是一个非常经典且容易出错的“差一错误”Off-by-one error。我们可以通过一个小例子看明白1. 为什么r rows必须拦截假设你的网格有3 行即rows 3第 1 行的索引是0第 2 行的索引是1第 3 行的索引是2最大的合法索引是rows - 1也就是 2。如果你此时代码里的r等于3即r rows你去访问grid[3]程序会立刻报错IndexError: list index out of range。因为第 4 行索引为 3 的行根本不存在。2. 这个条件的逻辑拆解这一行判定条件其实是给递归设定的**“安全护栏”**ifr0orrrowsorc0orccolsorgrid[r][c]0:r 0: 往“上”走过头了出了天花板。r rows: 往“下”走过头了出了地板。c 0: 往“左”走过头了出了左墙壁。c cols: 往“右”走过头了出了右墙壁。grid[r][c] 0: 没出界但踩到水了不需要处理。只有避开了以上所有“雷区”我们才能安全地执行grid[r][c]的操作。3. 一个帮你记住的口诀在处理矩阵题目时你可以记住这个**“合法区间”**索引的范围是 。只要“等于”或“超过”了长度就是非法。 进阶小技巧为什么顺序很重要在这一行代码中条件的顺序其实救了程序的命# 正确顺序先判断边界再访问数组ifr0orrrowsor...orgrid[r][c]0:Python 在执行or时有**“短路逻辑”**如果r rows成立比如3 3为真Python 会认为整个if已经成立了。它不会再去执行后面的grid[r][c] 0。这样就巧妙地避免了在索引越界时去访问数组导致的崩溃。如果你把顺序反过来写if grid[r][c] 0 or r rows ...程序会先尝试访问grid[3][c]然后直接报错死掉根本没机会运行到后面的边界检查。为什么这叫 DFS因为它会先把某一个方向相连的所有陆地全部摸透然后再返回去摸另一条分支。这完美符合“深度优先”的定义。4. 具体情况具体分析不同结构的 DFS虽然思想统一但针对不同数据结构DFS 的“方向”和“标记”方式不同A. 二叉树的 DFS方向只有两个左孩子、右孩子。标记通常不需要手动标记因为树的结构是向下的不会往回走。代码dfs(node.left)dfs(node.right)。B. 二维数组网格/岛屿的 DFS方向四个或八个上下左右有时加斜向。标记必须标记沉岛或visited数组因为网格是连通的不标记会死循环。代码dfs(r1, c)、dfs(r, c1)等。C. 排列组合题的 DFS回溯方向数组中还没用过的数字。标记used[i] True递归used[i] False回溯的关键撤销标记。区别岛屿题不需要撤销标记沉了就沉了但排列题需要撤销为了尝试下一种排列。5. 总结DFS 的通用细节表维度二叉树 DFS网格岛屿 DFS全排列 DFS (回溯)终止条件if not nodeif 越界 or grid[i][j]0if 路径长度 目标长度访问标记无需通常沉岛将 1 改为 0used 数组扩散方式left/right上下左右for i in range(len(nums))是否回溯否否是必须恢复现场 备考建议你在面试中如果听到“连通性”、“所有路径”、“寻找所有可能的组合”脑子里第一反应就应该是DFS。《单词搜索 (LeetCode 79)》。这题也是网格但它比岛屿数量多了一个**“回溯”的动作因为路走不通要退回来把标记还原。这道题是DFS 回溯 (Backtracking)的教科书级案例。它和《岛屿数量》非常像但多了一个关键动作“恢复现场”**。在《岛屿数量》中岛沉了就沉了但在《单词搜索》中如果你这一条路没走通你得把踩过的脚印擦掉给别的路径留机会。1. 核心思路DFS 回溯遍历网格在 的网格里找到第一个和单词word[0]匹配的字母作为 DFS 的起点。深度搜索从起点开始向四个方向扩散寻找word[1],word[2]…标记与回溯为了防止“同一个格子重复使用”我们在进入递归前把当前格子标记比如改成空字符。关键点如果这条路最终没能找到完整的单词在return之前必须把当前格子的字符改回来。这就是回溯。2. 手写代码实现LeetCode 风格classSolution:defexist(self,board:list[list[str]],word:str)-bool:rowslen(board)colslen(board[0])defdfs(r,c,k): r, c: 当前网格坐标 k: 当前匹配到 word 的第几个字符 # 1. 边界判定 字母匹配判定# 利用短路逻辑先看坐标是否越界再看字符是否匹配ifr0orrrowsorc0orccolsorboard[r][c]!word[k]:returnFalse# 2. 成功条件如果匹配到了单词的最后一个字母ifklen(word)-1:returnTrue# 3. 标记访问防止原地转圈同一个格子重复使用tempboard[r][c]board[r][c]# 临时清空# 4. 四个方向死磕只要有一个方向通了就返回 True# 注意k1 代表去找下一个字母res(dfs(r1,c,k1)ordfs(r-1,c,k1)ordfs(r,c1,k1)ordfs(r,c-1,k1))# 5. 回溯擦掉脚印恢复现场# 无论 res 是 True 还是 False都要把字母还回去board[r][c]tempreturnres# 主逻辑寻找起点forrinrange(rows):forcinrange(cols):ifdfs(r,c,0):returnTruereturnFalse3. 这题和《岛屿数量》的细节差异细节岛屿数量 (Island)单词搜索 (Word Search)匹配目标只要是 ‘1’ 就行必须匹配word[k]递归深度直到把整块岛淹没直到k len(word) - 1标记处理永久修改(把 1 变成 0)暂时修改(回溯时要改回来)返回值不需要返回 (void)必须返回bool(通了还是没通)在《岛屿数量》里我们是把陆地炸掉炸了就炸了但在《单词搜索》里我们要寻找的是一条特定的路径。1. 为什么要“标记” (步骤 3)假设你要找单词ABA而网格里是A - B两个格子。你站在第一个A上匹配成功。此时你往右走到B匹配成功。接下来你要在B的四周找下一个A。如果没有标记你会回头看向左边那个刚刚才用过的A。程序会觉得“哎左边刚好有个A” 于是判定ABA存在。但这不符合题意因为同一个格子不能重复使用。所以我们每踩到一个格子就要把它暂时“变成空”让后面的递归看不见它。2. 为什么要“回溯/恢复现场” (步骤 5)这是最难理解的地方。我们用一个真实的例子来说明假设网格如下你要找单词ABCE[A] [B] C D [E] F路径有两条可能路径 1A - B - C - … (走不通了没找到 E)路径 2A - B - E (成功)关键点来了程序先试探路径 1。当它走到B的时候它会尝试往右走去摸C。为了防止回头它会把B改成空。如果往C走的那条路彻底失败了程序会回退Return到B这个位置。如果不恢复B当程序尝试路径 2从A走到下面的D或者尝试其他分身时它发现B还是空的原本能通过B到达E的路因为之前失败的尝试被永久封死了。3. 视觉化流程做实验想象你是一个实验员在一个培养皿网格里找路径拿起temp board[r][c]你把当前的显微镜玻片拿起来观察。遮住board[r][c] ‘’在这一片放个遮光板防止等会儿光线乱跳。观察四周res dfs…看看上下左右能不能接上。放下board[r][c] temp不管观察结果如何你走的时候必须把遮光板拿走把玻片放回原处。因为下一个实验员另一条路径的递归可能需要用到这片玻片。4. 代码执行的细节顺序tempboard[r][c]# 1. 记住这里本来是 Bboard[r][c]# 2. 把它涂掉让递归里的下一层看不见它res(dfs...)# 3. 递归像分身一样出去找它们看到的 board[r][c] 都是空的board[r][c]temp# 4. 分身回来了赶紧把 B 涂回来returnres# 5. 带着结果向上级汇报总结标记是为了约束当前这一条路径不走回头路。回溯是为了不影响其他可能的路径。面试官追问“回溯的时间复杂度很高怎么优化”你“对于这道题回溯是必须的但我们可以做剪枝Pruning。比如先统计网格里各字母的数量如果网格里A只有 1 个而单词里需要 2 个A直接返回False连 DFS 都不用进。”面试官经常会问“如果不允许修改原数组board你该怎么做”回答“我可以维护一个同样大小的二维布尔数组visited[m][n]或者使用一个哈希表Set来记录访问过的坐标 。”