2026/4/1 9:16:14
网站建设
项目流程
公司网站建设图片素材怎么找,新闻营销发稿平台,c2c电子商务平台有哪些,如何开科技软件在算法世界中#xff0c;许多问题的表面描述往往会引导我们走向一条直观但低效的道路。而真正的解题乐趣#xff0c;恰恰在于洞察问题本质#xff0c;找到那条巧妙而高效的捷径。今天#xff0c;我们将一同剖析经典的“太平洋大西洋水流问题”#xff08;Pacific Atlantic…在算法世界中许多问题的表面描述往往会引导我们走向一条直观但低效的道路。而真正的解题乐趣恰恰在于洞察问题本质找到那条巧妙而高效的捷径。今天我们将一同剖析经典的“太平洋大西洋水流问题”Pacific Atlantic Water Flow并通过一段优雅的C代码学习如何运用“反向思维”和“多源广度优先搜索”Multi-source BFS来攻克它。1. 问题引入地图上的“分水岭”想象一下你有一张二维的地形高度图代表着一片大陆。大陆的左上角是太平洋右下角是大西洋。雨水落在陆地上会遵循“水往低处流”的原则最终汇入海洋。问题是请找出所有那些既能让雨水流向太平洋又能让雨水流向大西洋的格子坐标。这就像在寻找大陆上的“分水岭”地带但并非传统意义上的山脊而是那些特殊的点它们的水流拥有“双重国籍”可以选择东去或西归。(图片来源LeetCode)2. 常规思路的困境暴力解法的“时间陷阱”拿到这个问题最直观的想法是什么对于地图上的每一个格子我们都进行一次深度优先搜索DFS或广度优先搜索BFS模拟水的流动过程。在搜索中我们只移动到比当前格子更低或等高的相邻格子。如果在某次搜索中我们既能到达太平洋的边界即网格的第一行或第一列又能到达大西洋的边界即网格的最后一行或最后一列那么这个起始格子就是我们要找的答案之一。这个思路虽然正确但效率极低。假设网格有 m 行 n 列总共有 m*n 个格子。对于每个格子BFS/DFS的时间复杂度最坏情况下是 O(m*n)当整个地图是一个平缓的斜坡时。因此总的时间复杂度将达到 O((m*n)^2)。对于一个 100x100 的网格这意味着需要执行 10000 * 10000 1亿 次操作这在实际应用中是完全不可接受的。我们需要一个更聪明的办法。3. 核心洞见逆流而上化繁为简让我们转换一下视角。既然从每个点出发去判断能否到达两个大洋的成本太高那我们反过来想哪些点能到达海洋 海洋边界上的点以及所有比这些边界点更高、并且与它们相连的点。这就是“反向思维”的精髓。我们不再从内陆点出发“顺流而下”去寻找海洋而是从海洋边界出发“逆流而上”去标记所有能流到该海洋的内陆点。具体来说1. 对于太平洋所有能流到它的点必然是从其边界第一行、第一列开始并且沿着不降低的高度向内陆延伸的区域。2. 对于大西洋同理所有能流到它的点必然是从其边界最后一行、最后一列开始并且沿着不降低的高度向内陆延伸的区域。这样一来问题就分解为了两个独立的子问题• 找出所有能“逆流”到达太平洋的点。• 找出所有能“逆流”到达大西洋的点。最终的答案就是这两个区域的交集。这种方法的优势在于我们只需要进行两次BFS/DFS一次从太平洋的所有边界点开始一次从大西洋的所有边界点开始。总的时间复杂度降为 O(m*n)因为每个格子和每条边都最多被访问两次一次为太平洋一次为大西洋这是一个巨大的性能飞跃。4. 代码逐行深度解析现在让我们结合您提供的C代码来看看这个巧妙的思路是如何被完美实现的。#include iostream#include vector#include queueusing namespace std;class Solution {private:// 定义4个方向偏移量上、下、左、右用于遍历相邻格子vectorvectorint dirs {{-1,0}, {1,0}, {0,-1}, {0,1}};// 全局变量存储网格的总行数和总列数避免重复计算int rows, cols;代码结构概览• dirs 是一个方向数组是图遍历算法中的标准配置用于快速获取当前格子的四个邻居坐标。• rows 和 cols 作为私有成员变量在主函数中初始化后可供 bfs 函数直接访问避免了在函数调用中反复传递或计算。/*** 广度优先搜索BFS函数从队列中的边界点出发标记所有能流向对应海洋的格子* param heights 地形高度网格核心判断依据* param q 存储待处理坐标的队列初始为海洋边界点* param visited 标记数组visited[i][j]为true表示(i,j)能流向该海洋*/void bfs(vectorvectorint heights, queuepairint, int q, vectorvectorbool visited) {// 队列不为空时持续处理while (!q.empty()) {// 取出队首坐标C17结构化绑定语法// 低版本C需替换为int x q.front().first; int y q.front().second;auto [x, y] q.front();// 弹出队首元素避免重复处理q.pop();// 遍历当前格子的4个相邻方向for (auto dir : dirs) {// 计算相邻格子的坐标int nx x dir[0], ny y dir[1];// 边界与条件判断// 1. 新坐标在网格范围内不越界// 2. 该格子未被标记过// 3. 相邻格子高度 当前格子高度反向搜索水往低处流 → 高处能流向低处反向则低处能到达高处if (nx 0 nx rows ny 0 ny cols !visited[nx][ny] heights[nx][ny] heights[x][y]) {// 标记该格子能流向对应海洋visited[nx][ny] true;// 将该格子入队继续处理其相邻格子q.push({nx, ny});}}}}BFS核心函数深度解析这是整个算法的引擎。它接收一个初始队列 q 和一个标记矩阵 visited然后执行标准的BFS。1. 队列初始化q 中包含了所有能直接“接触”到某个海洋的边界点。例如对于太平洋队列里就是第一行和第一列的所有坐标。2. BFS循环while (!q.empty()) 是BFS的经典框架。3. 出队与处理q.front() 取出队首元素即当前要处理的坐标 (x, y)。q.pop() 将其从队列中移除。这里使用了C17的结构化绑定 auto [x, y]使代码非常简洁。4. 四向探索for (auto dir : dirs) 循环遍历四个方向。5. 关键判断 if 语句这是理解“逆流而上”的核心。◦ nx 0 nx rows ny 0 ny cols确保新坐标 (nx, ny) 在网格内防止数组越界。◦ !visited[nx][ny]确保我们不会重复处理已经标记过的格子避免无限循环和无效计算。◦ heights[nx][ny] heights[x][y]这是反向思维的点睛之笔。◦ 在正向思维中水流向低处条件是 heights[neighbor] heights[current]。◦ 在我们的反向BFS中我们从海洋低处出发寻找所有能流到这里的点。一个点 A 的水要能流到点 BA 的高度必须大于等于 B。所以反过来如果我们从 B 出发能“到达”的点 A 必须满足 height[A] height[B]。因此这个条件保证了我们只向“上游”地势更高或相等的方向探索。6. 标记与入队如果 (nx, ny) 满足所有条件说明它能通过 (x, y) 流向海洋。我们将其在 visited 矩阵中标记为 true并将其入队以便后续从它出发继续探索更高处的格子。public:/*** 主函数找出所有既能流向太平洋又能流向大西洋的格子坐标* param heights 输入的地形高度网格* return 满足条件的坐标列表每个坐标为{行, 列}*/vectorvectorint pacificAtlantic(vectorvectorint heights) {// 存储最终结果满足条件的坐标对vectorvectorint res;// 边界处理如果输入网格为空直接返回空结果if (heights.empty()) return res;// 初始化总行数和总列数rows heights.size(), cols heights[0].size();// pacific[i][j]标记(i,j)能否流向太平洋vectorvectorbool pacific(rows, vectorbool(cols, false));// atlantic[i][j]标记(i,j)能否流向大西洋vectorvectorbool atlantic(rows, vectorbool(cols, false));// 定义两个队列分别存储太平洋和大西洋的边界初始点queuepairint, int pq, aq;主函数 pacificAtlantic 初始化1. 边界检查if (heights.empty()) return res; 是所有数组/容器处理函数的好习惯。2. 初始化尺寸rows heights.size(), cols heights[0].size(); 将网格尺寸存入成员变量。3. 创建标记矩阵pacific 和 atlantic 是两个 rows x cols 的布尔矩阵全部初始化为 false。它们将分别记录每个格子能否到达太平洋和大西洋。4. 创建队列pq (Pacific Queue) 和 aq (Atlantic Queue) 是两个队列用于启动针对两个海洋的BFS。// 初始化边界队列处理上下边界for (int j 0; j cols; j) {// 上边界第0行所有格子都能直接流向太平洋加入太平洋队列并标记pq.push({0, j});pacific[0][j] true;// 下边界最后一行所有格子都能直接流向大西洋加入大西洋队列并标记aq.push({rows-1, j});atlantic[rows-1][j] true;}// 初始化边界队列处理左右边界for (int i 0; i rows; i) {// 左边界第0列所有格子都能直接流向太平洋加入太平洋队列并标记pq.push({i, 0});pacific[i][0] true;// 右边界最后一列所有格子都能直接流向大西洋加入大西洋队列并标记atlantic[i][cols-1] true;aq.push({i, cols-1});}多源BFS的起点设置这部分代码为两个BFS准备了初始“火种”。• 太平洋边界第一行 (i0) 和第一列 (j0) 的所有格子都直接与太平洋相连。因此它们都能流向太平洋。我们将这些坐标 push 到 pq 队列中并将 pacific 矩阵中对应的位置设为 true。• 大西洋边界最后一行 (irows-1) 和最后一列 (jcols-1) 的所有格子都直接与大西洋相连。我们将这些坐标 push 到 aq 队列中并将 atlantic 矩阵中对应的位置设为 true。通过这种方式我们的BFS从一开始就是“多源”的即同时从所有边界点开始向外扩散。// 执行BFS分别标记能流向太平洋和大西洋的所有格子bfs(heights, pq, pacific);bfs(heights, aq, atlantic);执行两次BFS这两行代码是整个算法的执行核心。• bfs(heights, pq, pacific); 执行后pacific 矩阵中将准确地标记出所有能流向太平洋的格子。• bfs(heights, aq, atlantic); 执行后atlantic 矩阵中将准确地标记出所有能流向大西洋的格子。// 遍历整个网格收集同时能流向两个海洋的格子坐标for (int i 0; i rows; i) {for (int j 0; j cols; j) {// 两个标记数组同时为true说明该格子满足条件if (pacific[i][j] atlantic[i][j]) {res.push_back({i, j});}}}// 返回最终结果return res;}};结果收集最后一步非常简单。我们遍历整个网格对于每个格子 (i, j)检查它在两个标记矩阵中的值。如果 pacific[i][j] 和 atlantic[i][j] 都为 true说明这个格子既能流向太平洋也能流向大西洋是我们要找的答案。我们将其坐标 {i, j} 加入结果列表 res。最终res 包含了所有满足条件的坐标函数将其返回。5. 总结与思考通过对这段代码的深入剖析我们可以总结出解决这类网格搜索问题的关键技巧1. 反向思维当正向遍历从每个点出发成本过高时尝试反向遍历从目标点/边界出发往往能大幅降低问题的复杂度。2. 多源BFS/DFS当需要从多个起点同时开始搜索时只需将所有起点一次性加入队列或栈中算法就能自然地处理“多源”问题。3. 空间换时间使用额外的标记矩阵如 pacific 和 atlantic来记录中间结果避免了大量的重复计算是实现高效算法的常用手段。4. 代码复用将核心的BFS逻辑封装成一个独立的、参数化的函数使得主逻辑清晰明了并且可以轻松地复用该函数来处理不同的搜索任务。这个问题的解法不仅展示了算法的精妙更重要的是传递了一种解决问题的思维方式不要被问题的字面意思困住勇于转换视角你可能会发现一个全新的、更广阔的天地。