一、DFS和BFS相关概念
深度优先搜索(DFS)在进行图的搜索时,得到一个新的节点,立即对新节点向下遍历,然后回溯。使用 DFS 对一个图进行遍历时,能够遍历到的节点都是从初始节点可达的,DFS 常用来求解这种 可达性 问题。
广度优先搜索(BFS)在搜索的时候,是一层一层地进行遍历,每层遍历都是以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
每一层遍历的节点都与根节点距离相同。设 di 表示第 i 个节点与根节点的距离,推导出一个结论:对于先遍历的节点 i 与后遍历的节点 j,有 di <= dj。
利用这个结论,可以求解最短路径等 最优解 问题:第一次遍历到目的节点,其所经过的路径为最短路径。应该注意的是,使用 BFS 只能求解无权图的最短路径,无权图是指从一个节点到另一个节点的代价都记为 1。
二、DFS相关练习题
1.岛屿的最大面积
给定一个包含了一些 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。
解题思路:
当登陆某一个岛屿后,以该岛屿的东、西、南、北四个方向探索,同时为了保证不重复访问,将访问过得岛屿标记为2
,如果遇到‘0’
或者2
就退出。
class Solution {
private int[][] derection = {{-1,0},{1,0},{0,-1},{0,1}};
private int m, n;
private int[][] grid;
public int maxAreaOfIsland(int[][] grid) {
if(grid == null || grid.length == 0){
return 0;
}
m = grid.length;
n = grid[0].length;
int res = 0;
for(int i = 0; i < m; i++){
for(int j = 0; j < n;j++){
if(grid[i][j] == 1){
res = Math.max(res,dfs(grid,i,j));
}
}
}
return res;
}
public int dfs(int[][] grid,int r,int c) {
//判断边界,以及是否是岛屿或是否被访问过
if(r < 0 || r >= m || c < 0 || c >= n || grid[r][c] != 1){
return 0;
}
//访问过,设置为2
grid[r][c] = 2;
int res = 1;
//以当前节点,想四个方向搜索
for(int d[] : derection){
res += dfs(grid,r+d[0],c+d[1]);
}
return res;
}
}
2.岛屿的数量
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
class Solution {
private int[][] derection = {{-1,0},{1,0},{0,1},{0,-1}};
private int m, n;
private char[][] grid;
public int numIslands(char[][] grid) {
if(grid == null || grid.length == 0){
return 0;
}
m = grid.length;
n = grid[0].length;
int res = 0;
for(int i = 0;i < m; i++){
for(int j = 0;j < n; j++){
//只有是岛屿才进行搜索
if(grid[i][j] == '1'){
dfs(grid,i,j);
res++;
}
}
}
return res;
}
private void dfs(char[][] grid,int r, int c){
if(r < 0 || r >= m || c < 0 || c >= n || grid[r][c] != '1'){
return;
}
grid[r][c] = '2';
for(int[] d : derection){
dfs(grid,r+d[0],c+d[1]);
}
return;
}
}
3.朋友圈
班上有 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。
class Solution {
private int[][] M;
private int m;
public int findCircleNum(int[][] M) {
if(M == null || M.length == 0){
return 0;
}
m = M.length;
//设置一个辅助数组用来存储节点是否被访问过
boolean[] isVisited = new boolean[m];
int res =0;
//对矩阵一行行的判断
for(int i = 0; i < m; i++){
//如果没有被访问过,从这个定点,一直向下搜索
if(!isVisited[i]){
dfs(M,i,isVisited);
res++;
}
}
return res;
}
private void dfs(int[][]M,int i,boolean[] isVisited){
//首先设置为已访问
isVisited[i] = true;
//循环遍历每一列
for(int j = 0;j < m; j++){
//当两节点有联系,且未被访问,对该节点继续向下搜索。
if(M[i][j] == 1 && !isVisited[j]){
dfs(M,j,isVisited);
}
}
}
}
4.被围绕的区域
给定一个二维的矩阵,包含 ‘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’。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
class Solution {
private int[][] derection = {{-1,0},{1,0},{0,1},{0,-1}};
private int m,n;
private char[][] board;
public void solve(char[][] board) {
if(board == null || board.length == 0){
return;
}
m = board.length;
n = board[0].length;
//从第一行和最后一行开始搜索
for(int i = 0;i < n; i++){
dfs(board,0,i);
dfs(board,m-1,i);
}
//从第一列和最后一列开始搜索
for(int i = 0;i < m; i++){
dfs(board,i,0);
dfs(board,i,n-1);
}
for(int i = 0;i < m; i++){
for(int j = 0;j < n; j++){
//将所有的‘O’设置为‘X’
if(board[i][j] == 'O'){
board[i][j] = 'X';
}
//将所有的'#'改为‘O’
if(board[i][j] == '#'){
board[i][j] = 'O';
}
}
}
return;
}
private void dfs(char[][] board,int r,int c){
if(r < 0 || r >= m || c < 0 || c >= n || board[r][c] == 'X' || board[r][c] == '#'){
return;
}
//将与边界相连的'O'设置为‘#’
board[r][c] = '#';
//搜索与边界相连接的‘O‘’
for(int[] d : derection){
dfs(board,r+d[0],c+d[1]);
}
}
}
5.太平洋大西洋水流问题
给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。
提示:
加粗样式
输出坐标的顺序不重要
m 和 n 都小于150
示例:
给定下面的 5x5 矩阵:
返回:
[[0, 4], [1, 3], [1, 4], [2, 2], [3, 0], [3, 1], [4, 0]] (上图中带括号的单元).
class Solution {
private int[][] deriect = {{1,0},{-1,0},{0,1},{0,-1}};
private int m,n;
public List<List<Integer>> pacificAtlantic(int[][] matrix) {
List<List<Integer>> list = new ArrayList<>();
if(matrix == null || matrix.length == 0){
return list;
}
m = matrix.length;
n = matrix[0].length;
boolean[][] checkedP = new boolean[m][n];
boolean[][] checkedA = new boolean[m][n];
for(int i = 0;i < m;i++){
dfs(matrix,i,0,checkedP);
dfs(matrix,i,n-1,checkedA);
}
for(int i = 0;i < n;i++){
dfs(matrix,0,i,checkedP);
dfs(matrix,m-1,i,checkedA);
}
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (checkedP[i][j] && checkedA[i][j]) {
list.add(Arrays.asList(i, j));
}
}
}
return list;
}
private void dfs(int[][] matrix,int r, int c, boolean[][] checked){
if(checked[r][c]){
return;
}
checked[r][c] = true;
for(int[] d : deriect){
int nextR = r + d[0];
int nextC = c + d[1];
if(nextR < 0 || nextR >= m || nextC < 0 || nextC >= n || matrix[r][c] > matrix[nextR][nextC]){
continue;
}
dfs(matrix,nextR,nextC,checked);
}
return ;
}
}
三、BFS相关练习题
1. 二进制矩阵中的最短路径
在一个 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 。
思路参考:https://leetcode-cn.com/problems/shortest-path-in-binary-matrix/solution/1091java-bfszhi-jie-da-bai-98-xiang-jie-by-ustcyyw/
class Solution {
//八个方向联通
private int[][] derection = {{1,-1},{1,0},{1,1},{-1,-1},{-1,0},{-1,1},{0,1},{0,-1}};
private int m,n;
public int shortestPathBinaryMatrix(int[][] grid) {
if(grid == null || grid.length == 0){
return -1;
}
m = grid.length;
n = grid[0].length;
//如果起始点或者终点是阻塞状态1,代表永远不可达,不存在路径
if(grid[0][0] == 1 || grid[m-1][n-1] == 1){
return -1;
}
//直接使用grid存储路径长度
grid[0][0] = 1;
Queue<int[]> queue = new LinkedList<>();
//将起始点存入到队列中
queue.add(new int[]{0,0});
//队列不为空 且终点是可达的,也就是说 目前为止没有任何一条路经可以到达终点
while(!queue.isEmpty() && grid[m-1][n-1] == 0){
int[] xy = queue.remove();
int length = grid[xy[0]][xy[1]];
for(int[] d : derection){
int nextR = xy[0] + d[0];
int nextC = xy[1] + d[1];
if(inGrid(nextR,nextC) && grid[nextR][nextC] == 0){
queue.add(new int[]{nextR,nextC});
grid[nextR][nextC] = length + 1;
}
}
}
return grid[m-1][n-1] == 0 ? -1 : grid[m-1][n-1];
}
//判断边界,是否在矩阵范围之内
private boolean inGrid(int r,int c){
return r>=0 && r < m && c >=0 && c < n;
}
}
2.单词接龙
给定两个单词(beginWord 和 endWord)和一个字典,找到从 beginWord 到 endWord 的最短转换序列的长度。转换需遵循如下规则:
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典中的单词。
说明:
- 如果不存在这样的转换序列,返回 0。
- 所有单词具有相同的长度。
- 所有单词只由小写字母组成。
- 字典中不存在重复的单词。
- 你可以假设 beginWord 和 endWord 是非空的,且二者不相同。
示例 1:
输入:
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” 不在字典中,所以无法进行转换。
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
//首先将wordList存入到HashSet中
Set<String> wordSet = new HashSet<>(wordList);
if(wordSet.size() == 0 || !wordSet.contains(endWord)){
return 0;
}
wordSet.remove(beginWord);
Queue<String> queue = new LinkedList<>();
queue.add(beginWord);
Set<String> visited = new HashSet<>();
visited.add(beginWord);
int wordLen = beginWord.length();
int step = 1;
while(!queue.isEmpty()){
//计算某一层次,有多少节点
int size = queue.size();
//依次遍历当前节点中的单词
for(int i = 0;i < size;i++){
String word = queue.remove();
char[] charArray = word.toCharArray();
//修改每一个字符
for(int j = 0;j < wordLen;j++){
//保存原始的字符
char originChar = charArray[j];
for(char k = 'a';k <= 'z'; k++){
//如果修改之后跟原来的字符串相同,就跳过
if (k == originChar) {
continue;
}
charArray[j] = k;
//将字符数组转换成字符串
String nextWord = String.valueOf(charArray);
//判断字典中是否存在修改后的字符
if(wordSet.contains(nextWord)){
//如果修改后的单词与结束单词相同
if(nextWord.equals(endWord)){
return step+1;
}
//如果不同,且未被访问过
if(!visited.contains(nextWord)){
queue.add(nextWord);
visited.add(nextWord);
}
}
}
//把单词恢复成原始的单词
charArray[j] = originChar;
}
}
//这一层次全部遍历完毕,step+1
step++;
}
return 0;
}
}