
<p align="center"><strong><a href="./qita/join.md">参与本项目</a>，贡献其他语言版本的代码，拥抱开源，让更多学习算法的小伙伴们受益！</strong></p>

# 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 <iostream>
#include <vector>
using namespace std;
int n, m;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1};

// 从 x，y 出发 把可以走的地方都标记上
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& 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<vector<int>>& grid, int x, int y) {
    vector<vector<bool>> visited(n, vector<bool>(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<vector<int>> grid(n, vector<int>(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 <iostream>
#include <vector>
using namespace std;
int n, m;
int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& 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<vector<int>> grid(n, vector<int>(m, 0));

    for (int i = 0; i < n; i++) {
        for (int j = 0; j < m; j++) {
            cin >> grid[i][j];
        }
    }
    // 标记从第一组边界上的节点出发，可以遍历的节点
    vector<vector<bool>> firstBorder(n, vector<bool>(m, false));

    // 标记从第一组边界上的节点出发，可以遍历的节点
    vector<vector<bool>> secondBorder(n, vector<bool>(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 
```Java
public class Main {

    // 采用 DFS 进行搜索
    public static void dfs(int[][] heights, int x, int y, boolean[][] visited, int preH) {
        // 遇到边界或者访问过的点，直接返回
        if (x < 0 || x >= heights.length || y < 0 || y >= heights[0].length || visited[x][y]) return;
        // 不满足水流入条件的直接返回
        if (heights[x][y] < preH) return;
        // 满足条件，设置为true，表示可以从边界到达此位置
        visited[x][y] = true;

        // 向下一层继续搜索
        dfs(heights, x + 1, y, visited, heights[x][y]);
        dfs(heights, x - 1, y, visited, heights[x][y]);
        dfs(heights, x, y + 1, visited, heights[x][y]);
        dfs(heights, x, y - 1, visited, heights[x][y]);
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int m = sc.nextInt();
        int n = sc.nextInt();

        int[][] heights = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                heights[i][j] = sc.nextInt();
            }
        }

        // 初始化两个二位boolean数组，代表两个边界
        boolean[][] pacific = new boolean[m][n];
        boolean[][] atlantic = new boolean[m][n];

        // 从左右边界出发进行DFS
        for (int i = 0; i < m; i++) {
            dfs(heights, i, 0, pacific, Integer.MIN_VALUE);
            dfs(heights, i, n - 1, atlantic, Integer.MIN_VALUE);
        }

        // 从上下边界出发进行DFS
        for (int j = 0; j < n; j++) {
            dfs(heights, 0, j, pacific, Integer.MIN_VALUE);
            dfs(heights, m - 1, j, atlantic, Integer.MIN_VALUE);
        }

        // 当两个边界二维数组在某个位置都为true时，符合题目要求
        List<List<Integer>> res = new ArrayList<>();
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (pacific[i][j] && atlantic[i][j]) {
                    res.add(Arrays.asList(i, j));
                }
            }
        }

        // 打印结果
        for (List<Integer> list : res) {
            for (int k = 0; k < list.size(); k++) {
                if (k == 0) {
                    System.out.print(list.get(k) + " ");
                } else {
                    System.out.print(list.get(k));
                }
            }
            System.out.println();
        }
    }
}

```

### Python
```Python
first = set()
second = set()
directions = [[-1, 0], [0, 1], [1, 0], [0, -1]]

def dfs(i, j, graph, visited, side):
    if visited[i][j]:
        return
    
    visited[i][j] = True
    side.add((i, j))
    
    for x, y in directions:
        new_x = i + x
        new_y = j + y
        if (
            0 <= new_x < len(graph)
            and 0 <= new_y < len(graph[0])
            and int(graph[new_x][new_y]) >= int(graph[i][j])
        ):
            dfs(new_x, new_y, graph, visited, side)

def main():
    global first
    global second
    
    N, M = map(int, input().strip().split())
    graph = []
    for _ in range(N):
        row = input().strip().split()
        graph.append(row)
    
    # 是否可到达第一边界
    visited = [[False] * M for _ in range(N)]
    for i in range(M):
        dfs(0, i, graph, visited, first)
    for i in range(N):
        dfs(i, 0, graph, visited, first)
    
    # 是否可到达第二边界
    visited = [[False] * M for _ in range(N)]
    for i in range(M):
        dfs(N - 1, i, graph, visited, second)
    for i in range(N):
        dfs(i, M - 1, graph, visited, second)

    # 可到达第一边界和第二边界
    res = first & second
    
    for x, y in res:
        print(f"{x} {y}")
    
    
if __name__ == "__main__":
    main()
```

### Go

### Rust

### Javascript

#### 深搜

```javascript
const r1 = require('readline').createInterface({ input: process.stdin });
// 创建readline接口
let iter = r1[Symbol.asyncIterator]();
// 创建异步迭代器
const readline = async () => (await iter.next()).value;

let graph // 地图
let N, M // 地图大小
const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向


// 读取输入，初始化地图
const initGraph = async () => {
  let line = await readline();
  [N, M] = line.split(' ').map(Number);
  graph = new Array(N).fill(0).map(() => new Array(M).fill(0))

  for (let i = 0; i < N; i++) {
    line = await readline()
    line = line.split(' ').map(Number)
    for (let j = 0; j < M; j++) {
      graph[i][j] = line[j]
    }
  }
}


/**
 * @description: 从（x，y）开始深度优先遍历地图
 * @param {*} graph 地图
 * @param {*} visited 可访问节点
 * @param {*} x 开始搜索节点的下标
 * @param {*} y 开始搜索节点的下标
 * @return {*}
 */
const dfs = (graph, visited, x, y) => {
  if (visited[x][y]) return
  visited[x][y] = true // 标记为可访问

  for (let i = 0; i < 4; i++) {
    let nextx = x + dir[i][0]
    let nexty = y + dir[i][1]
    if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界,跳过
    if (graph[x][y] < graph[nextx][nexty]) continue //不能流过.跳过
    dfs(graph, visited, nextx, nexty)
  }
}


/**
 * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界
 * @param {*} x 坐标
 * @param {*} y 坐标
 * @return {*} true可以到达，false不可以到达
 */
const isResult = (x, y) => {
  let visited = new Array(N).fill(false).map(() => new Array(M).fill(false))

  let isFirst = false //是否可到达第一边界
  let isSecond = false //是否可到达第二边界

  // 深搜，将(x, y)可到达的所有节点做标记
  dfs(graph, visited, x, y)

  // 判断能否到第一边界左边
  for (let i = 0; i < N; i++) {
    if (visited[i][0]) {
      isFirst = true
      break
    }
  }

  // 判断能否到第一边界上边
  for (let j = 0; j < M; j++) {
    if (visited[0][j]) {
      isFirst = true
      break
    }
  }

  // 判断能否到第二边界右边
  for (let i = 0; i < N; i++) {
    if (visited[i][M - 1]) {
      isSecond = true
      break
    }
  }

  // 判断能否到第二边界下边
  for (let j = 0; j < M; j++) {
    if (visited[N - 1][j]) {
      isSecond = true
      break
    }
  }

  return isFirst && isSecond
}

(async function () {

  // 读取输入，初始化地图
  await initGraph()

  // 遍历地图，判断是否能到达第一组边界和第二组边界
  for (let i = 0; i < N; i++) {
    for (let j = 0; j < M; j++) {
      if (isResult(i, j)) console.log(i + ' ' + j);
    }
  }
})()
```



#### 广搜-解法一

```java
const r1 = require('readline').createInterface({ input: process.stdin });
// 创建readline接口
let iter = r1[Symbol.asyncIterator]();
// 创建异步迭代器
const readline = async () => (await iter.next()).value;

let graph // 地图
let N, M // 地图大小
const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向


// 读取输入，初始化地图
const initGraph = async () => {
  let line = await readline();
  [N, M] = line.split(' ').map(Number);
  graph = new Array(N).fill(0).map(() => new Array(M).fill(0))

  for (let i = 0; i < N; i++) {
    line = await readline()
    line = line.split(' ').map(Number)
    for (let j = 0; j < M; j++) {
      graph[i][j] = line[j]
    }
  }
}


/**
 * @description: 从（x，y）开始广度优先遍历地图
 * @param {*} graph 地图
 * @param {*} visited 可访问节点
 * @param {*} x 开始搜索节点的下标
 * @param {*} y 开始搜索节点的下标
 * @return {*}
 */
const bfs = (graph, visited, x, y) => {
  let queue = []
  queue.push([x, y])
  visited[x][y] = true

  while (queue.length) {
    const [xx, yy] = queue.shift()
    for (let i = 0; i < 4; i++) {
      let nextx = xx + dir[i][0]
      let nexty = yy + dir[i][1]
      if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过

      // 可访问或者不能流过, 跳过 (注意这里是graph[xx][yy] < graph[nextx][nexty], 不是graph[x][y] < graph[nextx][nexty])
      if (visited[nextx][nexty] || graph[xx][yy] < graph[nextx][nexty]) continue

      queue.push([nextx, nexty])
      visited[nextx][nexty] = true

    }
  }
}


/**
 * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界
 * @param {*} x 坐标
 * @param {*} y 坐标
 * @return {*} true可以到达，false不可以到达
 */
const isResult = (x, y) => {
  let visited = new Array(N).fill(false).map(() => new Array(M).fill(false))

  let isFirst = false //是否可到达第一边界
  let isSecond = false //是否可到达第二边界

  // 深搜，将(x, y)可到达的所有节点做标记
  bfs(graph, visited, x, y)

  // console.log(visited);

  // 判断能否到第一边界左边
  for (let i = 0; i < N; i++) {
    if (visited[i][0]) {
      isFirst = true
      break
    }
  }

  // 判断能否到第一边界上边
  for (let j = 0; j < M; j++) {
    if (visited[0][j]) {
      isFirst = true
      break
    }
  }

  // 判断能否到第二边界右边
  for (let i = 0; i < N; i++) {
    if (visited[i][M - 1]) {
      isSecond = true
      break
    }
  }

  // 判断能否到第二边界下边
  for (let j = 0; j < M; j++) {
    if (visited[N - 1][j]) {
      isSecond = true
      break
    }
  }

  return isFirst && isSecond
}

(async function () {

  // 读取输入，初始化地图
  await initGraph()

  // 遍历地图，判断是否能到达第一组边界和第二组边界
  for (let i = 0; i < N; i++) {
    for (let j = 0; j < M; j++) {
      if (isResult(i, j)) console.log(i + ' ' + j);
    }
  }
})()
```



#### 广搜-解法二

从第一边界和第二边界开始向高处流, 标记可以流到的位置, 两个边界都能到达的位置就是所求结果

```javascript
 const r1 = require('readline').createInterface({ input: process.stdin });
 // 创建readline接口
 let iter = r1[Symbol.asyncIterator]();
 // 创建异步迭代器
 const readline = async () => (await iter.next()).value;
 
 let graph // 地图
 let N, M // 地图大小
 const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向
 
 
 // 读取输入，初始化地图
 const initGraph = async () => {
   let line = await readline();
   [N, M] = line.split(' ').map(Number);
   graph = new Array(N).fill(0).map(() => new Array(M).fill(0))
 
   for (let i = 0; i < N; i++) {
     line = await readline()
     line = line.split(' ').map(Number)
     for (let j = 0; j < M; j++) {
       graph[i][j] = line[j]
     }
   }
 }
 
 
 /**
  * @description: 从（x，y）开始广度优先遍历地图
  * @param {*} graph 地图
  * @param {*} visited 可访问节点
  * @param {*} x 开始搜索节点的下标
  * @param {*} y 开始搜索节点的下标
  * @return {*}
  */
 const bfs = (graph, visited, x, y) => {
   if(visited[x][y]) return
   
   let queue = []
   queue.push([x, y])
   visited[x][y] = true
 
   while (queue.length) {
     const [xx, yy] = queue.shift()
     for (let i = 0; i < 4; i++) {
       let nextx = xx + dir[i][0]
       let nexty = yy + dir[i][1]
       if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过
 
       // 可访问或者不能流过, 跳过 (注意因为是从边界往高处流, 所以这里是graph[xx][yy] >= graph[nextx][nexty], 还要注意不是graph[xx][yy] >= graph[nextx][nexty])
       if (visited[nextx][nexty] || graph[xx][yy] >= graph[nextx][nexty]) continue
 
       queue.push([nextx, nexty])
       visited[nextx][nexty] = true
     }
   }
 }
 
 (async function () {
 
   // 读取输入，初始化地图
   await initGraph()
 
   // 记录第一边界可到达的节点
   let firstBorder = new Array(N).fill(false).map(() => new Array(M).fill(false))
 
   // 记录第二边界可到达的节点
   let secondBorder = new Array(N).fill(false).map(() => new Array(M).fill(false))
 
   // 第一边界左边和第二边界右边
   for (let i = 0; i < N; i++) {
     bfs(graph, firstBorder, i, 0)
     bfs(graph, secondBorder, i, M - 1)
   }
 
   // 第一边界上边和第二边界下边
   for (let j = 0; j < M; j++) {
     bfs(graph, firstBorder, 0, j)
     bfs(graph, secondBorder, N - 1, j)
   }
 
   // 遍历地图，判断是否能到达第一组边界和第二组边界
   for (let i = 0; i < N; i++) {
     for (let j = 0; j < M; j++) {
       if (firstBorder[i][j] && secondBorder[i][j]) console.log(i + ' ' + j);
     }
   }
 })()
```



### TypeScript

### PhP

### Swift

### Scala

### C#

### Dart

### C



