news 2026/2/6 3:57:57

从“水往低处流”到“逆流而上”:BFS搜索巧解太平洋大西洋水流问题

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从“水往低处流”到“逆流而上”:BFS搜索巧解太平洋大西洋水流问题

在算法世界中,许多问题的表面描述往往会引导我们走向一条直观但低效的道路。而真正的解题乐趣,恰恰在于洞察问题本质,找到那条巧妙而高效的捷径。今天,我们将一同剖析经典的“太平洋大西洋水流问题”(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 <queue>
using namespace std;

class Solution {
private:
// 定义4个方向偏移量:上、下、左、右,用于遍历相邻格子
vector<vector<int>> 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(vector<vector<int>>& heights, queue<pair<int, int>>& q, vector<vector<bool>>& visited) {
// 队列不为空时持续处理
while (!q.empty()) {
// 取出队首坐标(C++17结构化绑定语法)
// 低版本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() 将其从队列中移除。这里使用了C++17的结构化绑定 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 的水要能流到点 B,A 的高度必须大于等于 B。所以,反过来,如果我们从 B 出发,能“到达”的点 A 必须满足 height[A] >= height[B]。因此,这个条件保证了我们只向“上游”(地势更高或相等)的方向探索。

6. 标记与入队:如果 (nx, ny) 满足所有条件,说明它能通过 (x, y) 流向海洋。我们将其在 visited 矩阵中标记为 true,并将其入队,以便后续从它出发继续探索更高处的格子。
public:
/**
* 主函数:找出所有既能流向太平洋又能流向大西洋的格子坐标
* @param heights 输入的地形高度网格
* @return 满足条件的坐标列表,每个坐标为{行, 列}
*/
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
// 存储最终结果:满足条件的坐标对
vector<vector<int>> res;
// 边界处理:如果输入网格为空,直接返回空结果
if (heights.empty()) return res;

// 初始化总行数和总列数
rows = heights.size(), cols = heights[0].size();
// pacific[i][j]:标记(i,j)能否流向太平洋
vector<vector<bool>> pacific(rows, vector<bool>(cols, false));
// atlantic[i][j]:标记(i,j)能否流向大西洋
vector<vector<bool>> atlantic(rows, vector<bool>(cols, false));
// 定义两个队列,分别存储太平洋和大西洋的边界初始点
queue<pair<int, 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准备了初始“火种”。

• 太平洋边界:第一行 (i=0) 和第一列 (j=0) 的所有格子都直接与太平洋相连。因此,它们都能流向太平洋。我们将这些坐标 push 到 pq 队列中,并将 pacific 矩阵中对应的位置设为 true。

• 大西洋边界:最后一行 (i=rows-1) 和最后一列 (j=cols-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逻辑封装成一个独立的、参数化的函数,使得主逻辑清晰明了,并且可以轻松地复用该函数来处理不同的搜索任务。

这个问题的解法不仅展示了算法的精妙,更重要的是传递了一种解决问题的思维方式:不要被问题的字面意思困住,勇于转换视角,你可能会发现一个全新的、更广阔的天地。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/2 19:05:18

固定头尾、中间滚动?用Flex + vh轻松搞定三栏布局

固定头尾、中间滚动&#xff1f;用Flex vh轻松搞定三栏布局固定头尾、中间滚动&#xff1f;用Flex vh轻松搞定三栏布局引言&#xff1a;为什么页面头尾固定这么让人头疼CSS Flex 布局快速上手指南——从“ Flex 是谁”到“ Flex 是我兄弟”1. 激活 Flex 模式2. 主轴与交叉轴—…

作者头像 李华
网站建设 2026/2/2 23:56:23

微电网恒功率PQ控制策略下的LCL并网仿真研究

微电网恒功率PQ控制&#xff0c;LCL并网仿真最近在搞微电网并网控制时发现个有意思的事——并网逆变器的PQ控制策略和LCL滤波器配合使用时&#xff0c;参数整定能把人绕晕。今天咱们就手撕个MATLAB仿真&#xff0c;看看这个经典组合到底怎么玩。先说说控制逻辑的核心&#xff1…

作者头像 李华
网站建设 2026/2/5 10:31:44

【青岛理工】25年计网期末A卷回忆版

一、简答题43分1.TCP/IP协议体系结构各层的核心功能2.简述CDMA的工作原理&#xff0c;计算过程见PPT/作业对于CDMA原理的理解&#xff0c;这里附上我在学习的时候自己的想法和思考&#xff08;仅供参考&#xff0c;并非教科书式权威的理解&#xff09;&#xff1a;考虑&#xf…

作者头像 李华
网站建设 2026/2/5 8:29:44

51单片机数字电压表

51单片机的数字电压表(数码管显示)–可提供C程序、proteus仿真、原理图、PCB、元件清单 功能说明 主要由51单片机最小系统、四位共阴数码管、ADC0832模数转换芯片组成。 可测DC5V以内的电压&#xff0c;显示精度为0. 001V玩单片机的小伙伴应该都想过自己做个电压表吧&#xff1…

作者头像 李华
网站建设 2026/2/5 14:45:56

新的spring boot3.x和spring-security6.x的流程

以下是Spring Boot 3.x与Spring Security 6.x的核心流程及关键配置要点&#xff1a;依赖配置在pom.xml或build.gradle中添加依赖&#xff1a;<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</a…

作者头像 李华
网站建设 2026/2/3 0:43:44

主动配电网故障恢复的重构与孤岛划分模型 关键词:分布式电源 故障网络重构 主动配电网 孤岛划分...

主动配电网故障恢复的重构与孤岛划分模型 关键词&#xff1a;分布式电源 故障网络重构 主动配电网 孤岛划分 参考文档&#xff1a; [1]《A New Model for Resilient Distribution Systems by Microgrids Formation》 [2]《主动配电网故障恢复的重构与孤岛划分统一模型》 仿真软…

作者头像 李华