目录
BFS与DFS相关算法题
BFS
1.二进制矩阵中的最短路径
2.完全平方数
3.单词接龙
DFS
1.岛屿的最大面积
2.岛屿数量
3.岛屿的周长
4.朋友圈
5.被围绕的区域
6.太平洋大西洋水流问题
广度优先搜索(也称宽度优先搜索,缩写BFS,以下采用广度来描述)是连通图的一种遍历算法这一算法也是很多重要的图的算法的原型。Dijkstra单源最短路径算法和Prim最小生成树算法都采用了和宽度优先搜索类似的思想。其别名又叫BFS,属于一种盲目搜寻法,目的是系统地展开并检查图中的所有节点,以找寻结果。换句话说,它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。基本过程,BFS是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。一般用队列数据结构来辅助实现BFS算法。
基本步奏:
(1)给出一连通图,如图,初始化全是白色(未访问);
(2)搜索起点V1(灰色);
(3)已搜索V1(黑色),即将搜索V2,V3,V4(标灰);
(4)对V2,V3,V4重复以上操作;
(5)直到终点V7被染灰,终止;
(6)最短路径为V1,V4,V7.
BFS算法实现框架:
BFS:使用队列保存未被检测的结点。结点按照宽度优先的次序被访问和进出队列。
BFS可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
在程序实现 BFS 时需要考虑以下问题:
(力扣1091)在一个 N × N 的方形网格中,每个单元格有两种状态:空(0)或者阻塞(1)。
一条从左上角到右下角、长度为 k 的畅通路径,由满足下述条件的单元格 C_1, C_2, ..., C_k 组成:
相邻单元格 C_i 和 C_{i+1} 在八个方向之一上连通(此时,C_i 和 C_{i+1} 不同且共享边或角)
C_1 位于 (0, 0)(即,值为 grid[0][0])
C_k 位于 (N-1, N-1)(即,值为 grid[N-1][N-1])
如果 C_i 位于 (r, c),则 grid[r][c] 为空(即,grid[r][c] == 0)
返回这条从左上角到右下角的最短畅通路径的长度。如果不存在这样的路径,返回 -1 。
示例 1:
输入:[[0,1],[1,0]]
输出:2
示例 2:
输入:[[0,0,0],[1,1,0],[1,1,0]]
输出:4
思路分析:
要找到左上角到右下角的最短路径,最短路径嘛,自然就想到了使用BFS。
然后开始使用队列模拟BFS:
时间复杂度为O(n)O(n),因为每个元素遍历了一次,n为元素的个数。空间复杂度为O(k)O(k),k为过程中队列的最大元素个数。
代码实现:
public class shortestPathBinaryMatrix {
private static int[][] directions = {{0,1}, {0, -1}, {1, -1}, {1, 0}, {1, 1}, {-1, -1}, {-1, 0}, {-1, 1}};
private int row, col;
public int shortestPathBinaryMatrix(int[][] grid) {
row = grid.length;
col = grid[0].length;
if(grid[0][0] == 1 || grid[row - 1][col - 1] == 1) return -1;
Queue pos = new LinkedList<>();
grid[0][0] = 1; // 直接用grid[i][j]记录从起点到这个点的最短路径长。按照题意 起点也有长度1
pos.add(new int[]{0,0});
while(!pos.isEmpty() && grid[row - 1][col - 1] == 0){ // 求最短路径 使用BFS
int[] xy = pos.remove();
int preLength = grid[xy[0]][xy[1]]; // 当前点的路径长度
for(int i = 0; i < 8; i++){
int newX = xy[0] + directions[i][0];
int newY = xy[1] + directions[i][1];
if(inGrid(newX, newY) && grid[newX][newY] == 0){
pos.add(new int[]{newX, newY});
grid[newX][newY] = preLength + 1; // 下一个点的路径长度要+1
}
}
}
return grid[row - 1][col - 1] == 0 ? -1 : grid[row - 1][col - 1]; // 如果最后终点的值还是0,说明没有到达
}
private boolean inGrid(int x, int y){
return x >= 0 && x < row && y >= 0 && y < col;
}
}
(力扣279)给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
示例 1:
输入: n = 12
输出: 3
解释: 12 = 4 + 4 + 4.
示例 2:
输入: n = 13
输出: 2
解释: 13 = 4 + 9.
官方题解:https://leetcode-cn.com/problems/perfect-squares/solution/wan-quan-ping-fang-shu-by-leetcode/
算法:
注意:在典型的 BFS 算法中,queue 变量通常是数组或列表类型。但是,这里我们使用 set 类型,以消除同一级别中的剩余项的冗余。事实证明,这个小技巧甚至可以增加 5 倍的运行加速。
在下图中,我们以 numSquares(7) 为例说明队列的布局。
代码实现:
class Solution {
public int numSquares(int n) {
ArrayList square_nums = new ArrayList();
for (int i = 1; i * i <= n; ++i) {
square_nums.add(i * i);
}
Set queue = new HashSet();
queue.add(n);
int level = 0;
while (queue.size() > 0) {
level += 1;
Set next_queue = new HashSet();
for (Integer remainder : queue) {
for (Integer square : square_nums) {
if (remainder.equals(square)) {
return level;
} else if (remainder < square) {
break;
} else {
next_queue.add(remainder - square);
}
}
}
queue = next_queue;
}
return level;
}
}
public int numSquares(int n) {
List squares = generateSquares(n);
Queue queue = new LinkedList<>();
boolean[] marked = new boolean[n + 1];
queue.add(n);
marked[n] = true;
int level = 0;
while (!queue.isEmpty()) {
int size = queue.size();
level++;
while (size-- > 0) {
int cur = queue.poll();
for (int s : squares) {
int next = cur - s;
if (next < 0) {
break;
}
if (next == 0) {
return level;
}
if (marked[next]) {
continue;
}
marked[next] = true;
queue.add(next);
}
}
}
return n;
}
/**
* 生成小于 n 的平方数序列
* @return 1,4,9,...
*/
private List generateSquares(int n) {
List squares = new ArrayList<>();
int square = 1;
int diff = 3;
while (square <= n) {
squares.add(square);
square += diff;
diff += 2;
}
return squares;
}
(力扣127)给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
说明:
输入:
beginWord = "hit",
endWord = "cog",
wordList = ["hot","dot","dog","lot","log","cog"]
输出: 5
解释: 一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。
示例 2:
输入:
beginWord = "hit"
endWord = "cog"
wordList = ["hot","dot","dog","lot","log"]
输出: 0
解释: endWord "cog" 不在字典中,所以无法进行转换。
解题思路:本题要求的是最短转换序列的长度,看到最短首先想到的就是广度优先搜索。想到广度优先搜索自然而然的就能想到图,但是本题并没有直截了当的给出图的模型,因此我们需要把它抽象成图的模型。
我们可以把每个单词都抽象为一个点,如果两个单词可以只改变一个字母进行转换,那么说明他们之间有一条双向边。因此我们只需要把满足转换条件的点相连,就形成了一张图。
基于该图,我们以 beginWord 为图的起点,以 endWord 为终点进行广度优先搜索,寻找 beginWord 到 endWord 的最短路径。
算法
基于上面的思路我们考虑如何编程实现。
首先为了方便表示,我们先给每一个单词标号,即给每个单词分配一个 id。创建一个由单词 word 到 id 对应的映射 wordId,并将 beginWord 与 wordList 中所有的单词都加入这个映射中。之后我们检查 endWord 是否在该映射内,若不存在,则输入无解。我们可以使用哈希表实现上面的映射关系。
然后我们需要建图,依据朴素的思路,我们可以枚举每一对单词的组合,判断它们是否恰好相差一个字符,以判断这两个单词对应的节点是否能够相连。但是这样效率太低,我们可以优化建图。
具体地,我们可以创建虚拟节点。对于单词 hit,我们创建三个虚拟节点 *it、h*t、hi*,并让 hit 向这三个虚拟节点分别连一条边即可。如果一个单词能够转化为 hit,那么该单词必然会连接到这三个虚拟节点之一。对于每一个单词,我们枚举它连接到的虚拟节点,把该单词对应的 id 与这些虚拟节点对应的 id 相连即可。
最后我们将起点加入队列开始广度优先搜索,当搜索到终点时,我们就找到了最短路径的长度。注意因为添加了虚拟节点,所以我们得到的距离为实际最短路径长度的两倍。同时我们并未计算起点对答案的贡献,所以我们应当返回距离的一半再加一的结果。
实现步骤:
代码实现:
//解法一
class Solution {
public int ladderLength(String beginWord, String endWord, List wordList) {
// Since all words are of same length.
int L = beginWord.length();
// Dictionary to hold combination of words that can be formed,
// from any given word. By changing one letter at a time.
Map> allComboDict = new HashMap<>();
wordList.forEach(
word -> {
for (int i = 0; i < L; i++) {
// Key is the generic word
// Value is a list of words which have the same intermediate generic word.
String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);
List transformations = allComboDict.getOrDefault(newWord, new ArrayList<>());
transformations.add(word);
allComboDict.put(newWord, transformations);
}
});
// Queue for BFS
Queue> Q = new LinkedList<>();
Q.add(new Pair(beginWord, 1));
// Visited to make sure we don't repeat processing same word.
Map visited = new HashMap<>();
visited.put(beginWord, true);
while (!Q.isEmpty()) {
Pair node = Q.remove();
String word = node.getKey();
int level = node.getValue();
for (int i = 0; i < L; i++) {
// Intermediate words for current word
String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);
// Next states are all the words which share the same intermediate state.
for (String adjacentWord : allComboDict.getOrDefault(newWord, new ArrayList<>())) {
// If at any point if we find what we are looking for
// i.e. the end word - we can return with the answer.
if (adjacentWord.equals(endWord)) {
return level + 1;
}
// Otherwise, add it to the BFS Queue. Also mark it visited
if (!visited.containsKey(adjacentWord)) {
visited.put(adjacentWord, true);
Q.add(new Pair(adjacentWord, level + 1));
}
}
}
}
return 0;
}
}
//解法二
public int ladderLength(String beginWord, String endWord, List wordList) {
wordList.add(beginWord);
int N = wordList.size();
int start = N - 1;
int end = 0;
while (end < N && !wordList.get(end).equals(endWord)) {
end++;
}
if (end == N) {
return 0;
}
List[] graphic = buildGraphic(wordList);
return getShortestPath(graphic, start, end);
}
private List[] buildGraphic(List wordList) {
int N = wordList.size();
List[] graphic = new List[N];
for (int i = 0; i < N; i++) {
graphic[i] = new ArrayList<>();
for (int j = 0; j < N; j++) {
if (isConnect(wordList.get(i), wordList.get(j))) {
graphic[i].add(j);
}
}
}
return graphic;
}
private boolean isConnect(String s1, String s2) {
int diffCnt = 0;
for (int i = 0; i < s1.length() && diffCnt <= 1; i++) {
if (s1.charAt(i) != s2.charAt(i)) {
diffCnt++;
}
}
return diffCnt == 1;
}
private int getShortestPath(List[] graphic, int start, int end) {
Queue queue = new LinkedList<>();
boolean[] marked = new boolean[graphic.length];
queue.add(start);
marked[start] = true;
int path = 1;
while (!queue.isEmpty()) {
int size = queue.size();
path++;
while (size-- > 0) {
int cur = queue.poll();
for (int next : graphic[cur]) {
if (next == end) {
return path;
}
if (marked[next]) {
continue;
}
marked[next] = true;
queue.add(next);
}
}
}
return 0;
}
深度优先搜索属于图算法的一种,是一个针对图和树的遍历算法,英文缩写为DFS即Depth First Search。深度优先搜索是图论中的经典算法,利用深度优先搜索算法可以产生目标图的相应拓扑排序表,利用拓扑排序表可以方便的解决很多相关的图论问题,如最大路径问题等等。一般用堆数据结构来辅助实现DFS算法。其过程简要来说是对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次。
基本步骤:
(1)对于下面的树而言,DFS方法首先从根节点1开始,其搜索节点顺序是1,2,3,4,5,6,7,8(假定左分枝和右分枝中优先选择左分枝)。
(2)从stack中访问栈顶的点;
(3)找出与此点邻接的且尚未遍历的点,进行标记,然后放入stack中,依次进行;
(4)如果此点没有尚未遍历的邻接点,则将此点从stack中弹出,再按照(3)依次进行;
(5)直到遍历完整个树,stack里的元素都将弹出,最后栈为空,DFS遍历完成。
思想:一直往深处走,直到找到解或者走不下去为止
DFS:使用栈保存未被检测的结点,结点按照深度优先的次序被访问并依次被压入栈中,并以相反的次序出栈进行新的检测。
从一个节点出发,使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
在程序实现 DFS 时需要考虑以下问题:
(力扣695)给定一个包含了一些 0 和 1 的非空二维数组 grid 。
一个 岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在水平或者竖直方向上相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
找到给定的二维数组中最大的岛屿面积。(如果没有岛屿,则返回面积为 0 )
示例 1:
[[0,0,1,0,0,0,0,1,0,0,0,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,1,1,0,1,0,0,0,0,0,0,0,0],
[0,1,0,0,1,1,0,0,1,0,1,0,0],
[0,1,0,0,1,1,0,0,1,1,1,0,0],
[0,0,0,0,0,0,0,0,0,0,1,0,0],
[0,0,0,0,0,0,0,1,1,1,0,0,0],
[0,0,0,0,0,0,0,1,1,0,0,0,0]]
对于上面这个给定矩阵应返回 6。注意答案不应该是 11 ,因为岛屿只能包含水平或垂直的四个方向的 1 。
示例 2:
[[0,0,0,0,0,0,0,0]]
对于上面这个给定的矩阵, 返回 0。
代码实现:
class Solution {
public int maxAreaOfIsland(int[][] grid) {
int res = 0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[i].length; j++) {
if (grid[i][j] == 1) {
res = Math.max(res, dfs(i, j, grid));
}
}
}
return res;
}
// 每次调用的时候默认num为1,进入后判断如果不是岛屿,则直接返回0,就可以避免预防错误的情况。
// 每次找到岛屿,则直接把找到的岛屿改成0,这是传说中的沉岛思想,就是遇到岛屿就把他和周围的全部沉默。
// ps:如果能用沉岛思想,那么自然可以用朋友圈思想。有兴趣的朋友可以去尝试。
private int dfs(int i, int j, int[][] grid) {
if (i < 0 || j < 0 || i >= grid.length || j >= grid[i].length || grid[i][j] == 0) {
return 0;
}
grid[i][j] = 0;
int num = 1;
num += dfs(i + 1, j, grid);
num += dfs(i - 1, j, grid);
num += dfs(i, j + 1, grid);
num += dfs(i, j - 1, grid);
return num;
}
}
(力扣200)给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
[
['1','1','1','1','0'],
['1','1','0','1','0'],
['1','1','0','0','0'],
['0','0','0','0','0']
]
输出: 1
示例 2:
输入:
[
['1','1','0','0','0'],
['1','1','0','0','0'],
['0','0','1','0','0'],
['0','0','0','1','1']
]
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
思路:我们可以将二维网格看成一个无向图,竖直或水平相邻的 11 之间有边相连。
为了求出岛屿的数量,我们可以扫描整个二维网格。如果一个位置为 11,则以其为起始节点开始进行深度优先搜索。在深度优先搜索的过程中,每个搜索到的 11 都会被重新标记为 00。
最终岛屿的数量就是我们进行深度优先搜索的次数。
代码实现:
class Solution {
void dfs(char[][] grid, int r, int c) {
int nr = grid.length;
int nc = grid[0].length;
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}
grid[r][c] = '0';
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
dfs(grid, r, c);
}
}
}
return num_islands;
}
}
给定一个包含 0 和 1 的二维网格地图,其中 1 表示陆地 0 表示水域。
网格中的格子水平和垂直方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。
示例 :
输入:
[[0,1,0,0],
[1,1,1,0],
[0,1,0,0],
[1,1,0,0]]
输出: 16
解释: 它的周长是下面图片中的 16 个黄色的边:
代码实现:深度优先遍历
class Solution {
public int islandPerimeter(int[][] grid) {
if(grid==null || grid.length==0){
return 0;
}
int r = grid.length;
int l = grid[0].length;
for(int i=0;i=grid.length || j<0 || j>=grid[0].length || grid[i][j]==0){
return 1;
}
if(grid[i][j]==2){
return 0;
}
grid[i][j] = 2;
return dfs(grid,i+1,j)+dfs(grid,i-1,j)+dfs(grid,i,j+1)+dfs(grid,i,j-1);
}
}
(力扣547)班上有 N 名学生。其中有些人是朋友,有些则不是。他们的友谊具有是传递性。如果已知 A 是 B 的朋友,B 是 C 的朋友,那么我们可以认为 A 也是 C 的朋友。所谓的朋友圈,是指所有朋友的集合。
给定一个 N * N 的矩阵 M,表示班级中学生之间的朋友关系。如果M[i][j] = 1,表示已知第 i 个和 j 个学生互为朋友关系,否则为不知道。你必须输出所有学生中的已知的朋友圈总数。
示例 1:
输入:
[[1,1,0],
[1,1,0],
[0,0,1]]
输出:2
解释:已知学生 0 和学生 1 互为朋友,他们在一个朋友圈。第2个学生自己在一个朋友圈。所以返回 2 。
示例 2:
输入:
[[1,1,0],
[1,1,1],
[0,1,1]]
输出:1
解释:已知学生 0 和学生 1 互为朋友,学生 1 和学生 2 互为朋友,所以学生 0 和学生 2 也是朋友,所以他们三个在一个朋友圈,返回 1 。
算法
给定的矩阵可以看成图的邻接矩阵。这样我们的问题可以变成无向图连通块的个数。为了方便理解,考虑如下矩阵:
M= [1 1 0 0 0 0
1 1 0 0 0 0
0 0 1 1 1 0
0 0 1 1 0 0
0 0 1 0 1 0
0 0 0 0 0 1]
如果我们把 M 看成图的邻接矩阵,则图为:
在这个图中,点的编号表示矩阵 M 的下标,ii 和 jj 之间有一条边当且仅当 M[i][j]M[i][j] 为 1。
为了找到连通块的个数,一个简单的方法就是使用深度优先搜索,从每个节点开始,我们使用一个大小为 NN 的 visitedvisited 数组(MM 大小为 N \times NN×N),这样 visited[i]visited[i] 表示第 i 个元素是否被深度优先搜索访问过。
我们首先选择一个节点,访问任一相邻的节点。然后再访问这一节点的任一相邻节点。这样不断遍历到没有未访问的相邻节点时,回溯到之前的节点进行访问。
代码实现:
//力扣官方题解:https://leetcode-cn.com/problems/friend-circles/solution/peng-you-quan-by-leetcode/
public class Solution {
public void dfs(int[][] M, int[] visited, int i) {
for (int j = 0; j < M.length; j++) {
if (M[i][j] == 1 && visited[j] == 0) {
visited[j] = 1;
dfs(M, visited, j);
}
}
}
public int findCircleNum(int[][] M) {
int[] visited = new int[M.length];
int count = 0;
for (int i = 0; i < M.length; i++) {
if (visited[i] == 0) {
dfs(M, visited, i);
count++;
}
}
return count;
}
}
(力扣130)给定一个二维的矩阵,包含 'X' 和 'O'(字母 O)。
找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。
示例:
X X X X
X O O X
X X O X
X O X X
运行你的函数后,矩阵变为:
X X X X
X X X X
X X X X
X O X X
解释:
被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
思路:本题给定的矩阵中有三种元素:
本题要求将所有被字母 X 包围的字母 O都变为字母 X ,但很难判断哪些 O 是被包围的,哪些 O 不是被包围的。
注意到题目解释中提到:任何边界上的 O 都不会被填充为 X。 我们可以想到,所有的不被包围的 O 都直接或间接与边界上的 O 相连。我们可以利用这个性质判断 O 是否在边界上,具体地说:
1.对于每一个边界上的 O,我们以它为起点,标记所有与它直接或间接相连的字母 O;
2.最后我们遍历这个矩阵,对于每一个字母:
方法一:深度优先搜索
思路及解法:我们可以使用深度优先搜索实现标记操作。在下面的代码中,我们把标记过的字母 O 修改为字母 A。
class Solution {
int n, m;
public void solve(char[][] board) {
n = board.length;
if (n == 0) {
return;
}
m = board[0].length;
for (int i = 0; i < n; i++) {
dfs(board, i, 0);
dfs(board, i, m - 1);
}
for (int i = 1; i < m - 1; i++) {
dfs(board, 0, i);
dfs(board, n - 1, i);
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j] == 'A') {
board[i][j] = 'O';
} else if (board[i][j] == 'O') {
board[i][j] = 'X';
}
}
}
}
public void dfs(char[][] board, int x, int y) {
if (x < 0 || x >= n || y < 0 || y >= m || board[x][y] != 'O') {
return;
}
board[x][y] = 'A';
dfs(board, x + 1, y);
dfs(board, x - 1, y);
dfs(board, x, y + 1);
dfs(board, x, y - 1);
}
}
方法二:广度优先搜索
思路及解法:我们可以使用广度优先搜索实现标记操作。在下面的代码中,我们把标记过的字母 O
修改为字母 A
。
class Solution {
int[] dx = {1, -1, 0, 0};
int[] dy = {0, 0, 1, -1};
public void solve(char[][] board) {
int n = board.length;
if (n == 0) {
return;
}
int m = board[0].length;
Queue queue = new LinkedList();
for (int i = 0; i < n; i++) {
if (board[i][0] == 'O') {
queue.offer(new int[]{i, 0});
}
if (board[i][m - 1] == 'O') {
queue.offer(new int[]{i, m - 1});
}
}
for (int i = 1; i < m - 1; i++) {
if (board[0][i] == 'O') {
queue.offer(new int[]{0, i});
}
if (board[n - 1][i] == 'O') {
queue.offer(new int[]{n - 1, i});
}
}
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int x = cell[0], y = cell[1];
board[x][y] = 'A';
for (int i = 0; i < 4; i++) {
int mx = x + dx[i], my = y + dy[i];
if (mx < 0 || my < 0 || mx >= n || my >= m || board[mx][my] != 'O') {
continue;
}
queue.offer(new int[]{mx, my});
}
}
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (board[i][j] == 'A') {
board[i][j] = 'O';
} else if (board[i][j] == 'O') {
board[i][j] = 'X';
}
}
}
}
}
(力扣417)给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
示例:给定下面的 5x5 矩阵:
太平洋 ~ ~ ~ ~ ~
~ 1 2 2 3 (5) *
~ 3 2 3 (4) (4) *
~ 2 4 (5) 3 1 *
~ (6) (7) 1 4 5 *
~ (5) 1 1 2 4 *
* * * * * 大西洋
返回: [[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
思路分析:
二维平面上的上下左右四个方向可以行走,与其余类似题目一样,固定的写法有:
要同时满足可以到达大西洋与太平洋,所以一个点需要进行两次路径的行走,一次以太平洋为目标,一次以大西洋为目标。从内部的点以边界为目标去进行路径行走比较麻烦,但是如果换一个思路,从边缘往里面走。
从边缘向里走就修改通行规则,要往高度比当前点高或者相等的点走。
定义函数dfs(int x, int y, boolean[][] canReach),第三个参数代表大西洋/太平洋相邻的点可以访问到的点,这些点也就是可以流到相应大洋的点。
主函数中,首先将各个成员变量初始化。然后生成表示大西洋/太平洋访问状态的 boolean[][] canReachP/canReachA = new boolean[m][n];。然后对于矩形的上下左右四条边界的点分别调用dfs(),进行从大西洋/太平洋到内部的访问。
最后对二维平面内的所有点进行遍历,找到canReachA[i][j] && canReachP[i][j]的点,就是可以同时到达两个大洋。
时间复杂度为O(n),因为只对每一个点进行了最多三次遍历,n表示坐标点的个数。空间复杂度除去递归调用占用的空间为O(n)。
DFS解法:
//方法一
public class pacificAtlantic {
private static int[][] dires = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
private int m, n;
private int[][] matrix;
public List> pacificAtlantic(int[][] matrix) {
List> res = new ArrayList<>();
m = matrix.length;
if (m == 0)
return res;
n = matrix[0].length;
if (n == 0)
return res;
this.matrix = matrix;
boolean[][] canReachP = new boolean[m][n];
boolean[][] canReachA = new boolean[m][n];
for (int i = 0; i < n; i++) {
dfs(0, i, canReachP);
dfs(m - 1, i, canReachA);
}
for (int i = 0; i < m; i++) {
dfs(i, 0, canReachP);
dfs(i, n - 1, canReachA);
}
for(int i = 0; i < m; i++){
for(int j = 0; j < n; j++){
if(canReachA[i][j] && canReachP[i][j]){
List temp = new ArrayList<>();
temp.add(i);
temp.add(j);
res.add(temp);
}
}
}
return res;
}
/**
* 换一种思路,从边界往里面走,只能走到比自己更高或者等高的地方。边界能走到的地方,就是能流入对应海洋的地方。
*/
private void dfs(int x, int y, boolean[][] canReach) {
canReach[x][y] = true;
for (int i = 0; i < 4; i++) {
int newX = x + dires[i][0];
int newY = y + dires[i][1];
if (isIn(newX, newY) && matrix[x][y] <= matrix[newX][newY] && !canReach[newX][newY]) {
dfs(newX, newY, canReach);
}
}
}
private boolean isIn(int x, int y) {
return x >= 0 && x < m && y >= 0 && y < n;
}
}
//方法二
class Solution{
public List> pacificAtlantic(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return new ArrayList<>();
}
int m = matrix.length;
int n = matrix[0].length;
int[][] pacific = new int[m][n];
int[][] atlantic = new int[m][n];
//从海洋边界开始
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dfs(matrix, pacific, i, j, matrix[i][j]);
}
if (i == m - 1 || j == n - 1) {
dfs(matrix, atlantic, i, j, matrix[i][j]);
}
}
}
List> res = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (pacific[i][j] == 1 && atlantic[i][j] == 1) {
res.add(Arrays.asList(i, j));
}
}
}
return res;
}
private void dfs(int[][] matrix, int[][] aux, int i, int j, int pre) {
//判断边界
if (i < 0 || j < 0 || i > matrix.length - 1 || j > matrix[0].length - 1
//已经流到过了
|| aux[i][j] == 1
//不能流动
|| matrix[i][j] < pre) {
return;
}
aux[i][j] = 1;
dfs(matrix, aux, i - 1, j, matrix[i][j]);
dfs(matrix, aux, i + 1, j, matrix[i][j]);
dfs(matrix, aux, i, j - 1, matrix[i][j]);
dfs(matrix, aux, i, j + 1, matrix[i][j]);
}
}
BFS解法:
class Solution{
public List> pacificAtlantic(int[][] matrix) {
if (matrix.length == 0 || matrix[0].length == 0) {
return new ArrayList<>();
}
int m = matrix.length;
int n = matrix[0].length;
Queue pacificQueue = new LinkedList<>();
Queue atlanticQueue = new LinkedList<>();
int[][] pacificAux = new int[m][n];
int[][] atlanticAux = new int[m][n];
//从海洋边界开始
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
pacificQueue.add(new int[]{i, j});
}
if (i == m - 1 || j == n - 1) {
atlanticQueue.add(new int[]{i, j});
}
}
}
bfs(matrix, pacificAux, pacificQueue);
bfs(matrix, atlanticAux, atlanticQueue);
List> res = new ArrayList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (pacificAux[i][j] == 1 && atlanticAux[i][j] == 1) {
res.add(Arrays.asList(i, j));
}
}
}
return res;
}
private void bfs(int[][] matrix, int[][] aux , Queue queue) {
while (!queue.isEmpty()) {
int[] array = queue.remove();
int i = array[0];
int j = array[1];
//流到的区域就置为1
aux[i][j] = 1;
if (i - 1 >= 0 && matrix[i][j] <= matrix[i - 1][j] && aux[i - 1][j] != 1) {
queue.add(new int[]{i - 1, j});
}
if (i + 1 < matrix.length && matrix[i][j] <= matrix[i + 1][j] && aux[i + 1][j] != 1) {
queue.add(new int[]{i + 1, j});
}
if (j - 1 >= 0 && matrix[i][j] <= matrix[i][j - 1] && aux[i][j - 1] != 1) {
queue.add(new int[]{i, j - 1});
}
if (j + 1 < matrix[0].length && matrix[i][j] <= matrix[i][j + 1] && aux[i][j + 1] != 1) {
queue.add(new int[]{i, j + 1});
}
}
}
}
非原创,整合了多方资源,仅作为个人日后复习之用,侵删。