# 103. 水流问题 [卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1175) 题目描述: 现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。 矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。 输入描述: 第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。 后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。 输出描述: 输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。 输入示例: ``` 5 5 1 3 1 2 4 1 2 1 3 2 2 4 7 2 1 4 5 6 1 1 1 4 1 2 1 ``` 输出示例: ``` 0 4 1 3 2 2 3 0 3 1 3 2 4 0 4 1 ``` 提示信息: ![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240517115816.png) 图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。 数据范围: 1 <= M, N <= 50 ## 思路 一个比较直白的想法,其实就是 遍历每个点,然后看这个点 能不能同时到达第一组边界和第二组边界。 至于遍历方式,可以用dfs,也可以用bfs,以下用dfs来举例。 那么这种思路的实现代码如下: ```CPP #include #include using namespace std; int n, m; int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 从 x,y 出发 把可以走的地方都标记上 void dfs(vector>& grid, vector>& visited, int x, int y) { if (visited[x][y]) return; visited[x][y] = true; for (int i = 0; i < 4; i++) { int nextx = x + dir[i][0]; int nexty = y + dir[i][1]; if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; if (grid[x][y] < grid[nextx][nexty]) continue; // 高度不合适 dfs (grid, visited, nextx, nexty); } return; } bool isResult(vector>& grid, int x, int y) { vector> visited(n, vector(m, false)); // 深搜,将x,y出发 能到的节点都标记上。 dfs(grid, visited, x, y); bool isFirst = false; bool isSecond = false; // 以下就是判断x,y出发,是否到达第一组边界和第二组边界 // 第一边界的上边 for (int j = 0; j < m; j++) { if (visited[0][j]) { isFirst = true; break; } } // 第一边界的左边 for (int i = 0; i < n; i++) { if (visited[i][0]) { isFirst = true; break; } } // 第二边界右边 for (int j = 0; j < m; j++) { if (visited[n - 1][j]) { isSecond = true; break; } } // 第二边界下边 for (int i = 0; i < n; i++) { if (visited[i][m - 1]) { isSecond = true; break; } } if (isFirst && isSecond) return true; return false; } int main() { cin >> n >> m; vector> grid(n, vector(m, 0)); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> grid[i][j]; } } // 遍历每一个点,看是否能同时到达第一组边界和第二组边界 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { if (isResult(grid, i, j)) { cout << i << " " << j << endl; } } } } ``` 这种思路很直白,但很明显,以上代码超时了。 来看看时间复杂度。 遍历每一个节点,是 m * n,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是: m * n 那么整体时间复杂度 就是 O(m^2 * n^2) ,这是一个四次方的时间复杂度。 ## 优化 那么我们可以 反过来想,从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。 同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。 然后**两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点**。 从第一组边界边上节点出发,如图: ![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240522120036.png) 从第二组边界上节点出发,如图: ![](https://code-thinking-1253855093.file.myqcloud.com/pics/20240522120122.png) 按照这样的逻辑,就可以写出如下遍历代码:(详细注释) ```CPP #include #include using namespace std; int n, m; int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; void dfs(vector>& grid, vector>& visited, int x, int y) { if (visited[x][y]) return; visited[x][y] = true; for (int i = 0; i < 4; i++) { int nextx = x + dir[i][0]; int nexty = y + dir[i][1]; if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; if (grid[x][y] > grid[nextx][nexty]) continue; // 注意:这里是从低向高遍历 dfs (grid, visited, nextx, nexty); } return; } int main() { cin >> n >> m; vector> grid(n, vector(m, 0)); for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { cin >> grid[i][j]; } } // 标记从第一组边界上的节点出发,可以遍历的节点 vector> firstBorder(n, vector(m, false)); // 标记从第一组边界上的节点出发,可以遍历的节点 vector> secondBorder(n, vector(m, false)); // 从最上和最下行的节点出发,向高处遍历 for (int i = 0; i < n; i++) { dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 } // 从最左和最右列的节点出发,向高处遍历 for (int j = 0; j < m; j++) { dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 } for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { // 如果这个节点,从第一组边界和第二组边界出发都遍历过,就是结果 if (firstBorder[i][j] && secondBorder[i][j]) cout << i << " " << j << endl;; } } } ``` 时间复杂度分析, 关于dfs函数搜索的过程 时间复杂度是 O(n * m),这个大家比较容易想。 关键看主函数,那么每次dfs的时候,上面还是有for循环的。 第一个for循环,时间复杂度是:n * (n * m) 。 第二个for循环,时间复杂度是:m * (n * m)。 所以本题看起来 时间复杂度好像是 : n * (n * m) + m * (n * m) = (m * n) * (m + n) 。 其实这是一个误区,大家再自己看 dfs函数的实现,其实 有visited函数记录 走过的节点,而走过的节点是不会再走第二次的。 所以 调用dfs函数,**只要参数传入的是 数组 firstBorder,那么地图中 每一个节点其实就遍历一次,无论你调用多少次**。 同理,调用dfs函数,只要 参数传入的是 数组 secondBorder,地图中每个节点也只会遍历一次。 所以,以下这段代码的时间复杂度是 2 * n * m。 地图用每个节点就遍历了两次,参数传入 firstBorder 的时候遍历一次,参数传入 secondBorder 的时候遍历一次。 ```CPP // 从最上和最下行的节点出发,向高处遍历 for (int i = 0; i < n; i++) { dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 } // 从最左和最右列的节点出发,向高处遍历 for (int j = 0; j < m; j++) { dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 } ``` 那么本题整体的时间复杂度其实是: 2 * n * m + n * m ,所以最终时间复杂度为 O(n * m) 。 空间复杂度为:O(n * m) 这个就不难理解了。开了几个 n * m 的数组。 ## 其他语言版本 ### Java ### Python ### Go ### Rust ### Javascript ### TypeScript ### PhP ### Swift ### Scala ### C# ### Dart ### C