图论相关经典题目练习及详解
图
200. 岛屿数量
https://leetcode.cn/problems/number-of-islands/description/
class Solution {public int numIslands(char[][] grid) {// 只要出现1的地方一定能构成陆地int count = 0;int m = grid.length;int n = grid[0].length;for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == '1') { // 是陆地但是不是其他岛屿的一部分dfs(grid, i, j);count++;}}}return count;}private void dfs(char[][] grid, int i, int j) {// 越界或不是未访问过的陆地if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] != '1') return;// 深度遍历// 先将当前遍历到的陆地改成2grid[i][j] = '2';dfs(grid, i-1, j);dfs(grid, i, j-1);dfs(grid, i+1, j);dfs(grid, i, j+1);}
}
解题思路:这道题主要需要注意到,只要出现1就说明这块区域连着的至少是有一个岛屿的,那么我们的思路就是不断的找1,如果从这个位置开始向四周扩散占领,我们可以将占领的陆地直接修改成2,表示已经被占领了,然后每次都是从1(未被占领的陆地)开始向四周找
695. 岛屿的最大面积
https://leetcode.cn/problems/max-area-of-island/description/
class Solution {public int maxAreaOfIsland(int[][] grid) {int res = 0;int m = grid.length;int n = grid[0].length;for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1) {res = Math.max(dfs(grid, i, j), res);}}}return res;}private int dfs(int[][] grid, int i, int j) {if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] != 1) return 0;grid[i][j] = 2;int area = 0;area += dfs(grid, i-1, j);area += dfs(grid, i, j-1);area += dfs(grid, i+1, j);area += dfs(grid, i, j+1);return area;}
}
解题思路:这道题其实和 200. 岛屿数量 是差不多的,都是使用到DFS,然后这里其实就是直接递归累加面积就行了,并且把遍历过的面积直接改成2,表示已经遍历过了,避免重复计算
463. 岛屿的周长
https://leetcode.cn/problems/island-perimeter/description/
class Solution {public int islandPerimeter(int[][] grid) {int m = grid.length;int n = grid[0].length;for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1) {return dfs(grid, i, j);}}}return 0;}private int dfs(int[][] grid, int i, int j) {// 到边界了或是遇到水的区域都要加1if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] == 0) return 1;// 遇到遍历过的直接返回0if (grid[i][j] == 2) return 0;// 标志已经遍历过grid[i][j] = 2;return dfs(grid, i+1, j)+ dfs(grid, i, j+1)+ dfs(grid, i-1, j)+ dfs(grid, i, j-1);}
}
解题思路:这道题就是直接通过深度遍历就行了,然后如果是遇到边界或是水的话就说明周长要加1,遇到遍历过的就直接返回0。从每一个节点开始遍历四个方向进行叠加
1020. 飞地的数量
https://leetcode.cn/problems/number-of-enclaves/description/
class Solution {public int numEnclaves(int[][] grid) {// 直接使用flood fill进行解决// 这道题其实就是只有岛屿原本就是连接到边界的,最终才能离开,如果是被水包起来的,是离开不了的int m = grid.length;int n = grid[0].length;int res = 0;// 进行flood fillfor (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {// 从边界入手if (i == 0 || j == 0 || i == m-1 || j == n-1) {if (grid[i][j] == 1) {dfs(grid, i, j);}}}}// 开始统计1的数量for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1) res++;}}return res;}private void dfs(int[][] grid, int i, int j) {if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] != 1) return;grid[i][j] = 2;dfs(grid, i+1, j);dfs(grid, i-1, j);dfs(grid, i, j+1);dfs(grid, i, j-1);}
}
解题思路:这道题很明显就是直接用填充来进行实现,但是难的点其实在于理解题意,题目说白了就是要找最后能跳出来的那些岛屿之外的岛屿,但是只有那些原本就和边界接壤的岛屿才能跳出来,所以我们可以从边界入手,将所有和边界接壤的岛屿都消除掉,剩下的就是题目要求的(被水围起来的)
130. 被围绕的区域
https://leetcode.cn/problems/surrounded-regions/description/
class Solution {public void solve(char[][] board) {int m = board.length;int n = board[0].length;// 从边缘入手,把所有 O 区域都设置成 Y,标识为无法进行围绕for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (i == 0 || j == 0 || i == m-1 || j == n-1) dfs(board, i, j, 'Y'); }}// 然后遍历一遍把剩下的所有 O 都变成 Xfor (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (board[i][j] == 'O') board[i][j] = 'X';}}// 把所有的Y变回Ofor (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (board[i][j] == 'Y') board[i][j] = 'O';}}}private void dfs(char[][] board, int i, int j, char newVal) {if (i > board.length - 1 || i < 0 || j > board[i].length - 1 || j < 0 || board[i][j] != 'O') return;board[i][j] = newVal;dfs(board, i+1, j, newVal);dfs(board, i-1, j, newVal);dfs(board, i, j+1, newVal);dfs(board, i, j-1, newVal);}
}
解题思路:这道题其实很好理解的,重点就是要看懂题意,其实就是只要 O块区域不和边界接壤,就不算是被围绕,那么就不能变成X,相反,如果一整块O块都是被X包起来的,就可以直接替换成X,那么这道题就变得很简单了
步骤:
* 首先就是从边缘入手,把所有 O 区域都设置成 Y,标识为无法进行围绕
* 然后遍历一遍把剩下的所有 O 都变成 X
* 最后再遍历一遍,把所有的Y变回O
417. 太平洋大西洋水流问题
https://leetcode.cn/problems/pacific-atlantic-water-flow/description/
class Solution {public List<List<Integer>> pacificAtlantic(int[][] heights) {// 逆向思维,直接水从大西洋/太平洋流向陆地,并且往高处流List<List<Integer>> res = new ArrayList<>();int m = heights.length;int n = heights[0].length;boolean[][] pac = new boolean[m][n];boolean[][] atl = new boolean[m][n];// 分别求出从大西洋能流到的地方和从太平洋能流到的地方for (int i = 0; i < m; i++) {dfs(pac, heights, i, 0);dfs(atl, heights, i, n-1);}for (int i = 0; i < n; i++) {dfs(pac, heights, 0, i);dfs(atl, heights, m-1, i);}// 重叠的点就是既能到大西洋,也能到太平洋的for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (pac[i][j] && atl[i][j]) res.add(Arrays.asList(i, j));}}return res;}private void dfs(boolean[][] search, int[][] heights, int i, int j) {// 已经扫描过是可以到达的就直接结束此次递归if (search[i][j]) return;search[i][j] = true;// 开始向四周进行扩展if (i-1 >= 0 && heights[i-1][j] >= heights[i][j]) dfs(search, heights, i-1, j);if (i+1 < heights.length && heights[i+1][j] >= heights[i][j]) dfs(search, heights, i+1, j);if (j-1 >= 0 && heights[i][j-1] >= heights[i][j]) dfs(search, heights, i, j-1);if (j+1 < heights[0].length && heights[i][j+1] >= heights[i][j]) dfs(search, heights, i, j+1);}
}
解题思路:这道题其实可以用逆向思维来解决,既然要找的是能同时流向两边海的那些格子,那么我们完全可以直接让水反过来流,直接从低处流向高处,从海流向岛屿,然后最后进行比对,只要是两片海都同时能流到的不就是我们要找的那些格子吗
所以到这里,这题就可以变成,分别用两个数组来记录两边海的流向情况,最后进行比对找到同时能留到的那些地方
827.最大人工岛
https://leetcode.cn/problems/making-a-large-island/description/
class Solution {public int largestIsland(int[][] grid) {// 可能存在多个岛,那么我该如何找到那个加上1之后或是不加上1还是最大的那个// 先找到最大的那个,如何判断一下有没有加上1的条件int m = grid.length;int n = grid[0].length;int res = 0;// map 用来存储每一块岛初始的编号和大小(编号从2开始,为了避免冲突)Map<Integer, Integer> map = new HashMap<>();int index = 2; // 起始编号从2开始for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1) {int area = updateIndex(grid, i, j, index);map.put(index, area);index++;res = Math.max(res, area);}}}// 没有岛,所以只能自己构建一个if (res == 0) return 1;// 但是还有一个问题就是,假如我构建的这个1,刚好能连接上两个岛,那这种也是要处理的// 开始处理海洋人工岛连接情况for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {int towLand = 1; // 初始值为1if (grid[i][j] == 0) {Set<Integer> set = connect(grid, i, j);if (set.isEmpty()) continue;for (Integer num : set) {towLand += map.get(num);}res = Math.max(res, towLand);}}}return res;}/*** 将这块陆地的编号改了并且返回这一块的面积*/private int updateIndex(int[][] grid, int i, int j, int index) {if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] != 1) return 0;grid[i][j] = index;return updateIndex(grid, i-1, j, index)+ updateIndex(grid, i+1, j, index)+ updateIndex(grid, i, j-1, index)+ updateIndex(grid, i, j+1, index)+ 1;}/*** 把海洋格子四周的陆地编号返回*/private Set<Integer> connect(int[][] grid, int i, int j) {Set<Integer> set = new HashSet<>();if (inLine(grid, i-1, j) && grid[i-1][j] != 0) set.add(grid[i-1][j]);if (inLine(grid, i+1, j) && grid[i+1][j] != 0) set.add(grid[i+1][j]);if (inLine(grid, i, j-1) && grid[i][j-1] != 0) set.add(grid[i][j-1]);if (inLine(grid, i, j+1) && grid[i][j+1] != 0) set.add(grid[i][j+1]);return set;}/*** 判断是否越界*/private boolean inLine(int[][] grid, int i, int j) {return i >= 0 && i < grid.length && j >= 0 && j < grid[0].length;}
}
解题思路:这道题需要分成好几种情况来考虑,首先就是一块陆地都没有,那就是直接返回1,还有就是即使我们构建了陆地之后不会导致原本相邻的两块岛连起来以及会导致连起来这几种情况
处理的方式:先单独算一遍各个岛原始的大小,并且给它进行编号,用一个map存起来,然后这里就可以处理没有岛的情况了(res是否为0),接着就是考虑会连接起来的情况,我们的做法就是用一个set记录每一块海洋空格的四周是否有岛的情况。
具体的可以直接看代码,代码已经给出详细的解释了
127. 单词接龙
https://leetcode.cn/problems/word-ladder/description/
双向BFS:class Solution {public int ladderLength(String beginWord, String endWord, List<String> wordList) {// 先判断 endWord 在不在 wordList中Set<String> set = new HashSet<>(wordList);if (!set.contains(endWord)) return 0;// 开两个队列Queue<String> bQ = new LinkedList<>();Queue<String> eQ = new LinkedList<>();bQ.offer(beginWord);eQ.offer(endWord);// 记录遍历过的单词以及走的步数Map<String, Integer> bVisited = new HashMap<>();Map<String, Integer> eVisited = new HashMap<>();bVisited.put(beginWord, 1);eVisited.put(endWord, 1);while (!bQ.isEmpty() && !eQ.isEmpty()) {int result;// 优先处理比较小的那个队列,可能会快一点if (bQ.size() <= eQ.size()) {result = findWord(bVisited, eVisited, bQ, set);} else {result = findWord(eVisited, bVisited, eQ, set);}if (result != -1) return result;}return 0;}/*** 对这个queue进行遍历,然后判断是否已经找到了相遇的那个单词*/private int findWord(Map<String, Integer> curVisited, Map<String, Integer> otherVisited, Queue<String> queue, Set<String> wordSet) {while (!queue.isEmpty()) {int size = queue.size();for (int i = 0; i < size; i++) {String word = queue.poll();Integer curStep = curVisited.get(word);char[] charArray = word.toCharArray();// 替换所有的字母进行比对for (int j = 0; j < charArray.length; j++) {char originalChar = charArray[j];for (char k = 'a'; k <= 'z'; k++) {if (k == originalChar) continue;charArray[j] = k;// 对比一下是否存在于wordList中String newWord = new String(charArray);if (wordSet.contains(newWord)) {// 相遇if (otherVisited.containsKey(newWord)) {return curStep + otherVisited.get(newWord);}// curVisited 不包含 newWordif (!curVisited.containsKey(newWord)) {curVisited.put(newWord, curStep + 1);queue.offer(newWord);}}}// 回溯charArray[j] = originalChar;}}}return -1;}
}单向BFS:
class Solution {public int ladderLength(String beginWord, String endWord, List<String> wordList) {// 先判断 endWord 在不在 wordList中Set<String> set = new HashSet<>(wordList);if (!set.contains(endWord)) return 0;// 实现BFS的队列Queue<String> queue = new LinkedList<>();queue.offer(beginWord);// 记录已经访问过的单词Set<String> visited = new HashSet<>();visited.add(beginWord);int step = 1; // 要经历的步数(beginWord也算一步)while (!queue.isEmpty()) {int size = queue.size();for (int i = 0; i < size; i++) {String word = queue.poll();char[] charArray = word.toCharArray();// 替换所有的字母进行比对for (int j = 0; j < charArray.length; j++) {char originalChar = charArray[j];for (char k = 'a'; k <= 'z'; k++) {if (k == originalChar) continue;charArray[j] = k;// 对比一下是否存在于wordList中String newWord = new String(charArray);// 新单词就是要找的那个if (newWord.equals(endWord)) return step + 1;// wordList中存在且没有被访问过if (set.contains(newWord) && !visited.contains(newWord)) {// 入队并记录访问queue.offer(newWord);visited.add(newWord);}}// 回溯charArray[j] = originalChar;}}step++; // 步数加1(同一层只加1)}return 0;}
}
解题思路:这道题其实就是直接通过一个队列来实现广度优先遍历,并且还用了一个set来记录某个单词是否访问过,然后就是通过枚举每一个单词的每一个位置进行字母替换,并判断是否出现在wordSet中,如果出现的话说明这个也是接龙链路上的一个,直接入队,最后就是操作完这一个单词之后要进行回溯。
具体的可以直接看代码
不过上面介绍的是单向BFS,如果想要更加的快的话,可以使用双向BFS,也就是从endWord也开一个,什么时候两个方向访问到相同的单词就说明可以结束了
841.钥匙和房间
https://leetcode.cn/problems/keys-and-rooms/description/
DFS:
class Solution {public boolean canVisitAllRooms(List<List<Integer>> rooms) {int size = rooms.size();List<Boolean> visited = new ArrayList<>(size);for (int i = 0; i < size; i++) {visited.add(false);}dfs(0, visited, rooms);for (int i = 0; i < size; i++) {if (!visited.get(i)) return false;}return true;}private void dfs(int key, List<Boolean> visited, List<List<Integer>> rooms) {// 避免重复处理if (visited.get(key)) return;// 标记一下已经访问visited.set(key, true);// 去获取到房间里的keyList<Integer> list = rooms.get(key);for (Integer i : list) {dfs(i, visited, rooms);}}
}BFS:
class Solution {public boolean canVisitAllRooms(List<List<Integer>> rooms) {int len = rooms.size(); // 房间数量Queue<Integer> queue = new LinkedList<>();Set<Integer> visited = new HashSet<>(); // 已经访问过的房间号// bfsqueue.offer(0);while (!queue.isEmpty()) {int size = queue.size();// 访问这一层所有节点for (int i = 0; i < size; i++) {// 当前房间Integer poll = queue.poll();// 访问过了if (visited.contains(poll)) continue;// 记录一下已经访问过了visited.add(poll);// 拿到这个房间的所有钥匙List<Integer> keys = rooms.get(poll);// 钥匙都塞进来for (Integer key : keys) {// 不要重复加入if (visited.contains(key)) continue;queue.offer(key);}}// 判断访问完了没有if (visited.size() == len) return true;}return false;}
}
解题思路:这道题可以用DFS/BFS解决
DFS的话就是用一个visited来标记当前房间号是否已经处理过了,如果处理过的话就不进行处理了,并且枚举这个房间的所有钥匙进行递归,最后判断一下是不是每一个都访问过了
BFS的话就是和常规的BFS一样,只不过就是判断到处理过了也是直接跳过,并在处理完每一层都会判断一下是否已经处理完全部房间
994. 腐烂的橘子
https://leetcode.cn/problems/rotting-oranges/description/
class Solution {public int orangesRotting(int[][] grid) {int m = grid.length;int n = grid[0].length;int cnt = 0;Queue<int[]> queue = new LinkedList<>();// 统计好苹果数量,并将第一轮坏苹果入队for (int i = 0; i < m; i++) {for (int j = 0; j < n; j++) {if (grid[i][j] == 1) cnt++;if (grid[i][j] == 2) queue.offer(new int[]{i, j});}}// 开始进行bfsint time = 0;while (!queue.isEmpty() && cnt > 0) {int size = queue.size();for (int i = 0; i < size; i++) {int[] poll = queue.poll();// 扩散到四周int x = poll[0];int y = poll[1];if (expand(grid, x-1, y)) {queue.offer(new int[]{x-1, y});cnt--;}if (expand(grid, x+1, y)) {queue.offer(new int[]{x+1, y});cnt--;}if (expand(grid, x, y-1)) {queue.offer(new int[]{x, y-1});cnt--;}if (expand(grid, x, y+1)) {queue.offer(new int[]{x, y+1});cnt--;}}time++;}if (cnt > 0) return -1;return time;}private boolean expand(int[][] grid, int i, int j) {if (i > grid.length - 1 || i < 0 || j > grid[i].length - 1 || j < 0 || grid[i][j] != 1) return false;grid[i][j] = 2;return true;}
}
解题思路:这道题很明显就是从一个腐烂的格子向四周扩散,所以应使用BFS,每一轮都将腐烂的水果四周的水果全部感染掉,并将心感染的那些水果入队,当然要注意进行while循环的条件还应该是cnt>0的情况,也就是存在好水果才进行,否则无意义
207. 课程表
https://leetcode.cn/problems/course-schedule/description/
class Solution {public boolean canFinish(int numCourses, int[][] prerequisites) {// 每一门课的入度int[] degree = new int[numCourses];// key 是先修课程,value 是依赖它的后续课程列表Map<Integer, List<Integer>> map = new HashMap<>();for (int[] prerequisite : prerequisites) {int a = prerequisite[0]; // 后修课int b = prerequisite[1]; // 前修课// 后修课入度加1degree[a]++;// 维护邻接表map.computeIfAbsent(b, k -> new ArrayList<>()).add(a);}// 用一个栈开始拓扑排序Queue<Integer> queue = new LinkedList<>();int count = 0; // 入度为0的节点的数量// 把入度为0的入队for (int i = 0; i < numCourses; i++) {if (degree[i] == 0) {queue.offer(i);count++;}}while (!queue.isEmpty()) {// 把队头元素出队int pre = queue.poll();// 把这个元素对应的后修课程的入度全部减1for (Integer cur : map.getOrDefault(pre, new ArrayList<>())) {// 入度为0,直接入队if (--degree[cur] == 0) {queue.offer(cur);count++;}}}return count == numCourses;}
}
解题思路:这道题就是直接使用一个邻接来记录下先修课程和依赖它的后序课程有哪些,并且使用一个数组维护了每一个节点的入度。
前置工作:把入度为0的节点都先入队。
然后就是开始进行拓扑排序,每次都是把队首元素拿出来,并且要对依赖它的课程的入度都减1,然后判断入度是否变成0,如果是的话也要入队,最后就是通过前面遍历过程中统计到的入度数量为0的那些节点是否已经和总的节点数量相同来确定是否可以完成