图的遍历方式主要是深度优先搜索(DFS)和广度优先搜索(BFS),两者大概的区别:
- dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
- bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。
DFS
dfs是离不开回溯的,因为它怼着一个方向深搜,当一个方向的搜索满足不了的时候,就需要回溯,回到前面的点换一个方向搜索。递归和回溯是相辅相成的,回溯法的代码框架:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
dfs的代码框架:
void dfs(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
}
可以看出二者差别不大。
对于dfs同样是走着递归3步(来自代码随想录):
- 确认递归函数,参数:
通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。
一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多:
List<List<Integer>> result; // 保存符合条件的所有路径
List<Integer> path; // 起点到终点的路径
void dfs (图,目前搜索的节点)
- 确认终止条件
终止添加不仅是结束本层递归,同时也是我们收获结果的时候。
另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。
if (终止条件) {
存放结果;
return;
}
- 处理目前搜索节点出发的路径
一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点。
for (选择:本节点所连接的其他节点) {
处理节点;
dfs(图,选择的节点); // 递归
回溯,撤销处理结果
}
BFS
广搜适合于解决两个点之间的最短路径问题。
因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。
借用代码随想录的例子:
给出一个start起始位置,BFS就是从四个方向走出第一步。如果加上一个end终止位置,那么使用BFS的搜索过程如图所示:
从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。
而且地图还可以有障碍,如图所示:
从图中可以看出,如果添加了障碍,我们是第六步才能走到end终点,所以只要BFS只要搜到终点一定是一条最短路径。
至于用什么样的数据结构,其实只要可以保存我们要遍历过的元素就可以,用队列,还是用栈,甚至用数组,都是可以的。用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针。
因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。
如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历。
因为栈是先进后出,加入元素和弹出元素的顺序改变了。
而广搜不需要注意转圈搜索的顺序,只要能搜到就行了,但大家都习惯用队列了,所以大部分还是使用队列来存储遍历过的元素。
以下是代码随想录的c++版模板,针对四方格地图:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
queue<pair<int, int>> que; // 定义队列
que.push({x, y}); // 起始节点加入队列
visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
while(!que.empty()) { // 开始遍历队列里的元素
pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
int curx = cur.first;
int cury = cur.second; // 当前节点坐标
for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
int nextx = curx + dir[i][0];
int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过
if (!visited[nextx][nexty]) { // 如果节点没被访问过
que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点
visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
}
}
}
}
并查集
原理
并查集常用来解决连通性问题,而图相关问题也经常会涉及到连通性问题,所以并查集也会用在解决图论相关问题。
那么什么是连通性问题呢?
就是比如想要判断两个元素是不是在同一个集合中,这样的问题就是连通性问题,在图中,表现形式就可以是:给一堆顶点对表示顶点间的连线,问是否存在从一个指定顶点到另一个指定顶点的有效路径,代表题目是LeetCode 1971. 寻找图中是否存在路径。
并查集最核心的功能有2个:
- 将两个元素添加到一个集合中。
- 判断两个元素在不在同一个集合。
应该用什么样的数据结构来存储元素,才可以实现上面的两个功能呢?
很容易想到用数组或set或map,但是当集合很多的时候,就需要开辟非常多的这样的结构,这样显然是不行的。
其实最核心的就是将属于同一个集合的元素进行连通,这可以使用一个一维数组进行存储,比如有三个元素1、3、5,属于同一个集合,现在需要将它们添加进同一个集合,就可以这样记录:father[1]=3,father[3]=5,这样就表示1的根是3,3的根是5,这样1、3、5就连通了。加入集合的代码如下:
// 将v,u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;// 没有在前面返回,说明不是同一个根,进行连通,v的根是u
}
但是有人会想到,这样只是说明1到3的连通,但是如何可以证明3到1的连通呢?
因为只要判断1、3、5是不是在同一个集合就可以完成任务,所以只要知道1、3是连通的就可以了。
根据一定的寻根方法,当1、3、5在同一个根下,就可以判断它们是在同一集合中:比如指定元素1,就应当可以通过father[1]=3,找到1的根是3,再通过father[3]=5,找到3的根是5;指定元素3,通过father[3]=5,找到3的根是5。所以1和3是在同一个根下面,属于同一个集合。find寻根代码如下:
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u; // 如果根就是自己,直接返回
else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
这样作为最上层的根,为了表示自己也在集合中,就要求father[根]=根,即初始的时候,就需要将father[i]=i,初始化init代码如下:
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
father[i] = i;
}
}
最后还差一个判断两个元素是否在同一个集合的isSame函数,只要能通过find函数找到同一个根,那么它们就是在同一个集合:
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
路径压缩
在前面的find中,是通过递归,类似从n叉树叶子到根节点一层层往上搜索根节点(图片来自代码随想录):
当这个树很高的时候,find就需要递归多次,为了避免高度太高,其实只需要下面这样的构造即可:
这样的寻根速度就是O(1)的,那么如何构造这样的树形呢?
这就是路径压缩要做的:将非根节点的所有节点直接指向根节点,代码实现时,只需要让 father[u] 接住 递归函数 find(father[u]) 的返回结果。因为 find 函数向上寻找根节点,father[u] 表述 u 的父节点,那么让 father[u] 直接获取 find函数 返回的根节点,这样就让节点 u 的父节点 变成根节点。
所以有路径压缩的寻根代码如下:
// 并查集里寻根的过程
int find(int u) {
if (u == father[u]) return u; // 如果根就是自己,直接返回
else return father[u]=find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
精简如下:
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]);
}
模板
所以并查集通用java代码模板:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
int[] father = new int[n];
// 并查集初始化
void init() {
for (int i = 0; i < n; i++) {
father[i] = i;// 初始都指向自己
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
通过模板,我们可以知道,并查集主要有三个功能。
- 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
- 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
- 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
误区
以下内容来自代码随想录:
观察代码模板,可能会想isSame和join函数中间是不是有部分代码重复,可能可以合并成下面这样?
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
if (isSame) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
但是这是不行的,在正常的代码中,想要进行:
join(1, 2);
join(3, 2);
很明显这两个操作是是将1,2添加到同一集合,再将2,3添加同一集合,这样1,2,3会在同一个集合中。
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
先执行jion(1,2),执行到father[2]=1,2的根是1:
再执行join(3,2),会先通过find(3)寻找 3的根为3,通过find(2)寻找2的根为1(返回1,v=find(2)=1),最后通过father[v]=father[1] = 3=u,明确1的根是3:
这样进行isSame(1,3)判断,就会先通过find(1)找到1的根为3,通过find(3)找到3的根是3本身,返回true,可以正确判断出1,3是在同一个集合中。
而如果按前面的错误想法,将代码进行合并:
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
if (isSam(u,v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
这样进行join(1,2),需要先判断isSame(1,2),返回的是false,join进入father[2]=1;
再进行join(3,2),先判断isSame(3,2),在isSame(3,2)中,先find(3),返回3,再find(2),返回1,isSame返回fales,在join中进入father[2]=3:
此时问 1,3是否在同一个集合,调用 isSame(1, 3)的时候,find(1) 返回的是1,find(3)返回的是3。 return 1 == 3 返回的是false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期.
通过前后代码对比,可以发现:join 函数 一定要先通过find函数寻根再进行关联。
也就是必须是这样的模板:
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
int[] father = new int[n];
// 并查集初始化
void init() {
for (int i = 0; i < n; i++) {
father[i] = i;// 初始都指向自己
}
}
// 并查集里寻根的过程
int find(int u) {
return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
u = find(u);
v = find(v);
return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
u = find(u); // 寻找u的根
v = find(v); // 寻找v的根
if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
father[v] = u;
}
BFS专用题
LeetCode 994. 腐烂的橘子
2024.08.09 一刷
思路:
-
每分钟每个腐烂的橘子都会使上下左右相邻的新鲜橘子腐烂,每往后一分钟,腐烂的橘子都会向外圈扩散;
-
这和广度优先搜索的模式一致:从起点出发,每次都尝试访问同一层的节点,如果同一层都访问完了,再访问下一层;
-
为了确认是否所有新鲜橘子都被腐烂,可以记录一个变量 fresh 表示当前网格中的新鲜橘子数,广度优先搜索的时候如果有新鲜橘子被腐烂,则 fresh-1,最后搜索结束时如果 fresh 大于 0 ,说明有新鲜橘子没被腐烂,返回 −1 ,否则返回所有新鲜橘子被腐烂的时间的最大值即可;
广搜需要知道起点,而本题中可能初始是多个腐烂的橘子,也就是多个起点;因此可以考虑先遍历一遍,用队列记录所有腐烂橘子的位置,同时记录下路径长度为0(新鲜橘子被腐烂的时间),然后遍历队列,进行广搜(向外扩散):
- 对于扩散圈上的,符合在网格内并且是新鲜橘子的,入队并记录路径长度;
- 标记橘子为腐烂,并且新鲜橘子数量-1;
- 遍历完成后只需要判断fresh是否被减到0即可,如果减到0说明所有新鲜橘子都被腐烂,返回记录的最大路径长度即可;
代码如下:
// 多源广度优先搜索
class Solution {
public int orangesRotting(int[][] grid) {
// 作为原始坐标方向偏移,与原坐标相加,依次是上下左右
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
Queue<int[]> que = new ArrayDeque<>();// 队列,用于暂存访问节点与路径长度
int fresh = 0;// 统计新鲜橘子数量
// 先遍历一遍,统计新鲜橘子数量与广搜的几个源头
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j] == 2)que.offer(new int[]{i,j,0});
if(grid[i][j] == 1)++fresh;
}
}
int res = 0;
while(!que.isEmpty()){
int[] cur = que.poll();
int curI = cur[0];
int curJ = cur[1];
res = cur[2];
for(int d=0;d<4;d++){
int nextI = curI + dir[d][0];
int nextJ = curJ + dir[d][1];
// 在界限内,并且新鲜橘子
if(nextI >= 0 && nextJ >=0 && nextI < grid.length && nextJ < grid[0].length && grid[nextI][nextJ] == 1){
que.offer(new int[]{nextI,nextJ,cur[2]+1});
grid[nextI][nextJ] = 2;
fresh--;
}
}
}
return fresh == 0 ? res : -1;
}
}
BFS与DFS通用题
LeetCode 797. 所有可能的路径
2023.12.26 一刷
思路:
题目要求找出从0到n-1的所有路径,其实graph.length=n,n-1就是graph的下标;
graph[i]就是节点i可以到达的所有节点的列表;
可以采用图的深度优先搜索:
1.确认递归函数,参数
为了深度优先遍历,就需要把图作为参数;此外还需要知道当前遍历到了哪个节点,所以需要把节点也作为参数进行传递;
public void dfs(int[][] graph,int node)
2.确认终止条件
题目要求收集从0到n-1的路径,因此只要传进来参数节点是n-1(即graph.length-1)就视为路径完整,就将记录的路径收集进入res;
if(node == graph.length-1){
res.add(new ArrayList<>(path));
return;
}
3.处理目前搜索节点出发的路径
遍历到当前节点node后,需要知道节点node与谁相连,即graph[node][i],用i从0到graph[node].length-1遍历所有node可以到达的节点,依次加入path并进行邻接节点的dfs,然后进行回溯,将路径中的当前节点去除;:
for(int i=0;i<graph[node].length;i++){
// 记录邻接的节点
int nextNode = graph[node][i];
path.add(nextNode);
dfs(graph,nextNode);
path.remove(path.size()-1);
}
LeetCode 200. 岛屿数量
2023.12.26 一刷
一、深搜:
核心思想:
为了避免在递归过程重复计算之前已经遍历过的“陆地”,在dfs过程中需要将搜索过的“1”,置为“0”;
需要遍历grid的所有网格,一旦碰到为“1”的陆地,计数器就+1,然后进行dfs,将所有与之相连的“1”全部置为“0”。由于在深搜中会将邻接的“1”都置为“0”,因此不用担心重复计算岛屿的数量。
深搜代码如下:
// 解法1:深搜
class Solution {
public int numIslands(char[][] grid) {
int count=0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j] == '1'){
count++;
dfs(grid,i,j);
}
}
}
return count;
}
public void dfs(char[][] grid,int i,int j){
// 当超出网格界限或遍历到“0”,就可以返回了
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]=='0')return;
grid[i][j]='0';
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
}
二、广搜:
由于广搜需要搭配used数组标记已经访问过的陆地,因此遍历过程中不必像深搜一样将“1”的陆地置为“0”,只需对应网格标记为访问过即可;
遍历所有网格过程时,只有当前网格未访问过,且网格为“1”(陆地),才将岛屿数量+1,并进行bfs;
在bfs中,需要先将入队的网格标记为访问过,然后沿4个方向进行bfs,只有当要访问的网格未被访问,且为“1”,就将其入队(防止重复访问网格),并且立即将入队网格标记为访问过;
广搜代码如下:
// 解法2:广搜
class Solution {
boolean[][] used;// 标记访问过的网格
// 作为原始坐标方向偏移,与原坐标相加,依次是上下左右
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
public int numIslands(char[][] grid) {
int count=0;
used = new boolean[grid.length][grid[0].length];
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
// 只有当未访问过且为1的陆地才可以+1
if(!used[i][j] && grid[i][j] == '1'){
count++;
bfs(grid,i,j);
}
}
}
return count;
}
public void bfs(char[][] grid,int i,int j){
Queue<int[]> que = new ArrayDeque<>();// 队列,用于暂存访问节点
que.offer(new int[]{i,j});// 将当前网格入队
used[i][j] = true;//只要一入队就必须标记为访问过
while(!que.isEmpty()){
// 队头先出队,找出与之邻接的符合要求的网格入队
int[] cur = que.poll();
int curI = cur[0];
int curJ = cur[1];
// 依次遍历上下左右四个方向的网格
for(int d=0;d<4;d++){
int nextI = curI+dir[d][0];
int nextJ = curJ+dir[d][1];
// 当超出网格界限,就可以继续下一个方向了
if(nextI<0||nextI>=grid.length||nextJ<0||nextJ>=grid[0].length)continue;
// 只有当下一个网格未访问过,且为‘1’,才加入队列
if(!used[nextI][nextJ] && grid[nextI][nextJ]=='1'){
que.offer(new int[]{nextI,nextJ});
used[nextI][nextJ]=true;// 记得一加入队列就标记访问过
}
}
}
}
}
LeetCode 695. 岛屿的最大面积
2023.12.27 一刷
思路:
一、DFS(淹没法)
核心思想:
为了避免在递归过程重复计算之前已经遍历过的“陆地”,在dfs过程中需要将搜索过的“1”,置为“0”;
需要遍历grid的所有网格,一旦碰到为“1”的陆地,先将全局变量count初始为0,然后进入dfs进行“淹没”(把相邻为1的全置为0),每计入一块陆地就淹没一块,直到相邻的陆地都淹没完,此时count也就是相邻陆地面积,把所有岛屿面积都算出比较大小即可。
如果不采用淹没法,就需要额外的used数组去记录遍历过的网格,相应判断也会更复杂一些;
代码如下:
// 1.DFS(淹没遍历过的陆地)
class Solution {
int count;
public int maxAreaOfIsland(int[][] grid) {
int res=0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]==1){
count=0;
dfs(grid,i,j);
res = Math.max(res,count);
}
}
}
return res;
}
public void dfs(int[][] grid,int i,int j){
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==0)return;
count++;
grid[i][j]=0;
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
}
二、BFS
由于广搜需要搭配used数组标记已经访问过的陆地,因此遍历过程中不必像深搜一样将“1”的陆地置为“0”,只需对应网格标记为访问过即可;
在bfs中,需要先将入队的网格标记为访问过,然后沿4个方向进行bfs,只有当要访问的网格未被访问,且为“1”,就将其入队(防止重复访问网格),并且立即将入队网格标记为访问过,同时计数;
代码如下:
// 2.BFS(需要额外used数组)
class Solution {
boolean[][] used;
int count;
// 上下左右顺序偏移
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
public int maxAreaOfIsland(int[][] grid) {
int res=0;
used = new boolean[grid.length][grid[0].length];
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
// 只有未访问过且为1才可以进行计数
if(!used[i][j] && grid[i][j]==1){
count=0;//每次广搜前都将count置为0,重新计算每个岛屿面积
bfs(grid,i,j);
res = Math.max(res,count);
}
}
}
return res;
}
public void bfs(int[][] grid,int i,int j){
Queue<int[]> que = new ArrayDeque<>();
que.offer(new int[]{i,j});// 将第1个陆地入队
used[i][j]=true;// 只要一入队就标记
count++;// 标记同时也计数
while(!que.isEmpty()){
int[] cur = que.poll();
int curI = cur[0];
int curJ = cur[1];
// 遍历4个方向
for(int d=0;d<4;d++){
int nextI = curI+dir[d][0];
int nextJ = curJ+dir[d][1];
// 超出界限不用计数,跳过即可
if(nextI<0||nextI>=grid.length||nextJ<0||nextJ>=grid[0].length)continue;
// 只有当下一个网格未访问过,且为‘1’,才加入队列
if(!used[nextI][nextJ] && grid[nextI][nextJ]==1){
que.offer(new int[]{nextI,nextJ});
used[nextI][nextJ]=true;// 记得一加入队列就标记访问过
count++;// 同时计数
}
}
}
}
}
LeetCode 1020. 飞地的数量
2023.12.27 一刷
思路:
其实这题是695. 岛屿的最大面积的进阶版,只要去掉和边缘相邻的所有岛屿,就是求剩下的不与边缘相邻的岛屿的面积。
一、dfs(淹没法)
先遍历网格所有边缘,将与边缘相邻的为1的网格用dfs全部淹没(置为0),之后只要再进行dfs,计算剩下的为1的网格数量即可;
代码如下:
// 1.DFS(淹没法)
class Solution {
int count=0;
public int numEnclaves(int[][] grid) {
// 对网格左右边缘为1的进行“淹没”
for(int i=0;i<grid.length;i++){
if(grid[i][0]==1)dfs(grid,i,0);
if(grid[i][grid[0].length-1]==1)dfs(grid,i,grid[0].length-1);
}
// 对网格上下边缘为1 的进行淹没
for(int j=1;j<grid[0].length-1;j++){
if(grid[0][j]==1)dfs(grid,0,j);
if(grid[grid.length-1][j]==1)dfs(grid,grid.length-1,j);
}
count=0;// 前面对边缘进行淹没可能会改变count数值,接下来才是真的计数
// 对剩余部分进行真正的计数
for(int i=1;i<grid.length-1;i++){
for(int j=1;j<grid[0].length-1;j++){
if(grid[i][j]==1){
dfs(grid,i,j);
}
}
}
return count;
}
public void dfs(int[][] grid,int i,int j){
// 当超出边界,或者遇到为0的网格直接返回
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==0)return;
grid[i][j]=0;// 淹没
count++;
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
}
二、bfs
同样思路,不过之前的bfs模板题都是使用used数组,这次尝试不使用,同样进行淹没法;
代码如下:
// 2.BFS(淹没法)
class Solution {
int count=0;
int[][] dir ={{0, 1},{1, 0},{-1, 0},{0, -1}};
public int numEnclaves(int[][] grid) {
// 对网格左右边缘为1的进行“淹没”
for(int i=0;i<grid.length;i++){
if(grid[i][0]==1)bfs(grid,i,0);
if(grid[i][grid[0].length-1]==1)bfs(grid,i,grid[0].length-1);
}
// 对网格上下边缘为1 的进行淹没
for(int j=1;j<grid[0].length-1;j++){
if(grid[0][j]==1)bfs(grid,0,j);
if(grid[grid.length-1][j]==1)bfs(grid,grid.length-1,j);
}
count=0;// 前面对边缘进行淹没可能会改变count数值,接下来才是真的计数
// 对剩余部分进行真正的计数
for(int i=1;i<grid.length-1;i++){
for(int j=1;j<grid[0].length-1;j++){
if(grid[i][j]==1){
bfs(grid,i,j);
}
}
}
return count;
}
public void bfs(int[][] grid, int i, int j){
Queue<int[]> que = new ArrayDeque<>();
que.offer(new int[]{i,j});
count++;
grid[i][j] = 0;
while(!que.isEmpty()){
int[] cur = que.poll();
int curI = cur[0];
int curJ = cur[1];
// 遍历4个方向
for(int d = 0; d < 4; d++){
int nextI = curI + dir[d][0];
int nextJ = curJ + dir[d][1];
// 出界直接跳过
if(nextI<0||nextJ<0||nextI>=grid.length||nextJ>=grid[0].length)continue;
// 为1的才进行计数
if(grid[nextI][nextJ] == 1){
que.offer(new int[]{nextI,nextJ});
count++;// 一入队就计数
grid[nextI][nextJ] = 0;// 记得一加入队列就淹没
}
}
}
}
}
LeetCode 1254. 统计封闭岛屿的数目
2023.12.28 一刷
思路:
与【LeetCode 1020.飞地的数量】一样的思路,只不过‘1’与‘0’代表的含义完全相反;
由于只要和边缘接壤,就不算封闭,所以可以先进行处理;
都是先遍历网格边缘,将‘陆地(0)’淹没,再对剩下网格进行判断与淹没;
代码如下:
class Solution {
public int closedIsland(int[][] grid) {
int count=0;
// 对网格左右边缘为0的进行“淹没”
for(int i=0;i<grid.length;i++){
if(grid[i][0]==0)dfs(grid,i,0);
if(grid[i][grid[0].length-1]==0)dfs(grid,i,grid[0].length-1);
}
// 对网格上下边缘为0的进行淹没
for(int j=1;j<grid[0].length-1;j++){
if(grid[0][j]==0)dfs(grid,0,j);
if(grid[grid.length-1][j]==0)dfs(grid,grid.length-1,j);
}
// 对剩余部分进行真正的计数
for(int i=1;i<grid.length-1;i++){
for(int j=1;j<grid[0].length-1;j++){
// 统计的是岛屿数量,而不是面积,所以碰到就计数,并对相连接的进行淹没
if(grid[i][j]==0){
count++;
dfs(grid,i,j);
}
}
}
return count;
}
public void dfs(int[][] grid,int i,int j){
// 当超出边界,或者遇到为1的网格直接返回,不用淹没了
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j]==1)return;
grid[i][j]=1;// 淹没
dfs(grid,i-1,j);
dfs(grid,i+1,j);
dfs(grid,i,j-1);
dfs(grid,i,j+1);
}
}
LeetCode 130. 被围绕的区域
2023.12.29 一刷
思路:
- 示例中写:被围绕的区间不会存在于边界上,换句话说,任何边界上的 ‘O’ 都不会被填充为 ‘X’。
- 所以可以和前面做过的题一样,先遍历网格边缘,将边缘原来是O的标记为A,表示其原来是边缘O;
- 然后再重新遍历网格,先将还是O的(真正被X围绕的)置为X,再将为A的还原回O(边缘的O);
- 使用bfs或dfs均可。
dfs代码如下:
class Solution {
public void solve(char[][] board) {
// 对左右边缘的O进行染色,用dfs改成A作为标记
for(int i=0;i<board.length;i++){
if(board[i][0]=='O')dfs(board,i,0);
if(board[i][board[0].length-1]=='O')dfs(board,i,board[0].length-1);
}
// 对左右边缘的O进行染色,用dfs改成A作为标记
for(int j=1;j<board[0].length-1;j++){
if(board[0][j]=='O')dfs(board,0,j);
if(board[board.length-1][j]=='O')dfs(board,board.length-1,j);
}
// 遍历剩余部分,对为A的(原来是边缘的O)重新置为O,对为O的置为X
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
// 这两句判断不能颠倒顺序,否则A改回O之后
// 原来是A的紧接着就会被改成X
if(board[i][j]=='O')board[i][j]='X';
if(board[i][j]=='A')board[i][j]='O';
}
}
}
public void dfs(char[][] board,int i,int j){
if(i<0||i>=board.length||j<0||j>=board[0].length)return;
if(board[i][j]=='A'||board[i][j]=='X')return;
// 只剩下为O的情况,直接置为A
board[i][j]='A';
dfs(board,i-1,j);
dfs(board,i+1,j);
dfs(board,i,j-1);
dfs(board,i,j+1);
}
}
bfs代码如下:
// bfs(不用used数组)
class Solution {
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
; public void solve(char[][] board) {
// 对左右边缘的O进行染色,用dfs改成A作为标记
for(int i=0;i<board.length;i++){
if(board[i][0]=='O')bfs(board,i,0);
if(board[i][board[0].length-1]=='O')bfs(board,i,board[0].length-1);
}
// 对左右边缘的O进行染色,用dfs改成A作为标记
for(int j=1;j<board[0].length-1;j++){
if(board[0][j]=='O')bfs(board,0,j);
if(board[board.length-1][j]=='O')bfs(board,board.length-1,j);
}
// 遍历剩余部分,对为A的(原来是边缘的O)重新置为O,对为O的置为X
for(int i=0;i<board.length;i++){
for(int j=0;j<board[0].length;j++){
// 这两句判断不能颠倒顺序,否则A改回O之后
// 原来是A的紧接着就会被改成X
if(board[i][j]=='O')board[i][j]='X';
if(board[i][j]=='A')board[i][j]='O';
}
}
}
public void bfs(char[][] board,int i,int j){
Queue<int[]> que = new ArrayDeque<>();
que.offer(new int[]{i,j});
board[i][j]='A';
while(!que.isEmpty()){
int[] cur = que.poll();
int curI = cur[0];
int curJ = cur[1];
for(int d=0;d<4;d++){
int nextI = curI+dir[d][0];
int nextJ = curJ+dir[d][1];
if(nextI<0||nextI>=board.length||nextJ<0||nextJ>=board[0].length)continue;
if(board[nextI][nextJ]=='A'||board[nextI][nextJ]=='X')continue;
que.offer(new int[]{nextI,nextJ});
board[nextI][nextJ]='A';
}
}
}
}
LeetCode 417. 太平洋大西洋水流问题
2023.12.29 一刷
题意就是找到哪些点可以同时到达太平洋和大西洋,流动的方式只能从高往低流。
- 最容易想到的方式就是对每个点都进行dfs或bfs,看看能否到达两个大洋,但是这样会超时;
- 因为遍历每一个节点,是 m * n,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是:m*n
- 那么整体时间复杂度 就是 O(m2*n2) ,这是一个四次方的时间复杂度。
优化
-
可以反过来想,从太平洋边上的节点 逆流而上,将遍历过的节点都标记上。
-
从大西洋的边上节点 逆流而长,将遍历过的节点也标记上。
-
然后两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点。
-
可以设置一个全局变量visited数组:visited[i][j][0]=【0/1】表示height[i][j]【可以/不可以】到达太平洋(Pacific),visited[i][j][1]则表示能否到达大西洋(Atlantic),当visited[i][j][0]=visited[i][j][1]=1,则说明这个点既可到达太平洋,又可到达大西洋,就可以将对应坐标加入res;
1.dfs:
因为这题需要比较当前点与下一个要遍历的点之间大小关系来确定下一个点能不能从该方向走,
所以需要借助dir数组来记录四个方向的偏移;
一进入dfs就先将改点标记为对应的大洋路径,然后判断下一个要遍历的点是否在界限内、
是否符合逆流而上、是否已经标记过,如果是否直接返回上一层;剩下的就是逆流而上的情况,进入下一层dfs;
代码如下:
// dfs(dir数组用于遍历方向)
class Solution {
List<List<Integer>> res = new ArrayList<>();
boolean[][][] visited;
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
public List<List<Integer>> pacificAtlantic(int[][] heights) {
visited = new boolean[heights.length][heights[0].length][2];
// 遍历左右边缘,从边缘出发逆流而上,将所有可以的点都进行对应标记
for(int i=0;i<heights.length;i++){
dfs(heights,i,0,0);//左边缘,j=0;太平洋signal=0
dfs(heights,i,heights[0].length-1,1);// 右边缘,j=heights[0].length-1,大西洋
}
for(int j=0;j<heights[0].length;j++){
dfs(heights,0,j,0);// 上边缘,i=0,太平洋signal=0
dfs(heights,heights.length-1,j,1);// 下边缘,i=height.length-1,太平洋signal=1
}
// 标记好后重新遍历每个点,将可以到两个大洋的点下标加入res;
for(int i=0;i<heights.length;i++){
for(int j=0;j<heights[0].length;j++){
if(visited[i][j][0] && visited[i][j][1]){
List<Integer> list = new ArrayList<>();
list.add(i);
list.add(j);
res.add(list);
}
}
}
return res;
}
// signal用于指向当前遍历的路径是指向太平洋visited[i][j]【0】还是大西洋visited[i][j]【1】
public void dfs(int[][] heights,int i,int j,int signal){
visited[i][j][signal]=true;
for(int d=0;d<4;d++){
int nextI = i+dir[d][0];
int nextJ = j+dir[d][1];
// 当超出界限就换个方向
if(nextI<0||nextI>=heights.length||nextJ<0||nextJ>=heights[0].length)continue;
// 逆流而上,如果下一个比当前还大就不可以逆流
// 或者下一个点已经访问过了,也直接换方向
if(heights[i][j]>heights[nextI][nextJ] || visited[nextI][nextJ][signal])continue;
dfs(heights,nextI,nextJ,signal);
}
}
}
2.bfs:
大致思路和dfs一样,只是借助队列;
代码如下:
// bfs
class Solution {
List<List<Integer>> res = new ArrayList<>();
boolean[][][] visited;
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
public List<List<Integer>> pacificAtlantic(int[][] heights) {
visited = new boolean[heights.length][heights[0].length][2];
// 遍历左右边缘,从边缘出发逆流而上,将所有可以的点都进行对应标记
for(int i=0;i<heights.length;i++){
bfs(heights,i,0,0);//左边缘,j=0;太平洋signal=0
bfs(heights,i,heights[0].length-1,1);// 右边缘,j=heights[0].length-1,大西洋
}
for(int j=0;j<heights[0].length;j++){
bfs(heights,0,j,0);// 上边缘,i=0,太平洋signal=0
bfs(heights,heights.length-1,j,1);// 下边缘,i=height.length-1,太平洋signal=1
}
// 标记好后重新遍历每个点,将可以到两个大洋的点下标加入res;
for(int i=0;i<heights.length;i++){
for(int j=0;j<heights[0].length;j++){
if(visited[i][j][0] && visited[i][j][1]){
List<Integer> list = new ArrayList<>();
list.add(i);
list.add(j);
res.add(list);
}
}
}
return res;
}
// signal用于指向当前遍历的路径是指向太平洋visited[i][j]【0】还是大西洋visited[i][j]【1】
public void bfs(int[][] heights,int i,int j,int signal){
// 上来直接标记,终止条件可以在遍历四个方向的时候进行判断
visited[i][j][signal]=true;
Queue<int[]> que = new ArrayDeque<>();
que.offer(new int[]{i,j});//入队和标记一定要一起操作
visited[i][j][signal]=true;
while(!que.isEmpty()){
int[] cur = que.poll();
for(int d=0;d<4;d++){
int nextI = cur[0]+dir[d][0];
int nextJ = cur[1]+dir[d][1];
// 当超出界限就换个方向
if(nextI<0||nextI>=heights.length||nextJ<0||nextJ>=heights[0].length)continue;
// 逆流而上,如果下一个比当前还大就不可以逆流
// 或者下一个点已经访问过了,也直接换方向
if(heights[cur[0]][cur[1]]>heights[nextI][nextJ] || visited[nextI][nextJ][signal])continue;
// 能走到这就是下一个点符合逆流而上,且没访问过
que.offer(new int[]{nextI,nextJ});
visited[nextI][nextJ][signal]=true;
}
}
}
}
LeetCode 827.最大人工岛
2023.12.31 一刷
思路:
最容易想到的就是遍历全部网格,尝试将每一个 0 改成1,然后对每次修改都搜索地图中的最大的岛屿面积,也就是每改变一个0的方格,都重新计算一个地图的最大面积;这样遍历每个位置需要n×n时间复杂度,对每个位置计算最大岛屿,也需要n×n的时间复杂度,最终需要n4时间复杂度,n最大为500,4次方会超时;
优化:
前面的思路中有重复计算部分:每个位置都计算由0改1后的最大岛屿面积,但是实际上只要先遍历一遍网格,计算好各个岛屿的面积,并且记录下来就可以了,这样每个位置由0改1之后,只要加上这个位置相邻4个方向的岛屿面积,就可以进行面积统计了;
第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录(编号从2开始)。可以使用map记录,key为岛屿编号,value为岛屿面积,如下,图片来自代码随想录:
第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积;
代码如下:
class Solution {
int count;// 用于第一轮遍历网格中的dfs计算岛屿面积
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
public int largestIsland(int[][] grid) {
int res = Integer.MIN_VALUE,n=grid.length;
int mark =2;//用于给每一块岛屿编号,从2开始(1已经被占用)
// 存储岛屿编号与对应面积(key-value-->编号-面积)
Map<Integer,Integer> map = new HashMap<>();
// 第一轮给岛屿编号,并用map记录对应编号岛屿的面积
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(grid[i][j]==1){
count = 0;//每次计算岛屿面积都需要重置count
int area = dfs(grid,i,j,mark);
map.put(mark++,area);
}
}
}
// 第二轮尝试将‘0’处改为1
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
// 只能将0改1,不是0只能跳过
if(grid[i][j]==0){
int curArea=1;// 当前位置由0转1,面积累加
// 存储每个位置四周的岛屿编号,用于去重,防止同一个编号面积被重复添加进总面积
Set<Integer> hashSet = new HashSet<>();
// 开始统计grid[i][j]转为1后,在四个方向上可以扩展多少面积
for(int d=0;d<4;d++){
int nextI = i+dir[d][0],nextJ = j+dir[d][1];
// 如果该方向超出界限就换方向
if(nextI<0||nextI>=n||nextJ<0||nextJ>=n)continue;
int nextMark = grid[nextI][nextJ];//记录该方向对应编号
// 当编号存在于hashSet中,说明四个方向中已经统计过这个编号面积,不用重复
// 或者如果这个方向编号是0,也可以跳过
if(hashSet.contains(nextMark)||grid[nextI][nextJ]==0)continue;
// 将该方向编号对应的面积添加上
curArea += map.get(nextMark);
// 同时记录这个位置当前方向的编号,使用hashset避免重复添加
hashSet.add(nextMark);
}
res = Math.max(res,curArea);
}
}
}
// 如果为Integer.MIN_VALUE说明res没被修改过,即初始全部都是1,无法进入if
// 那么n*n就是最终面积
return res==Integer.MIN_VALUE ? n*n : res;
}
// 用于计算岛屿面积
public int dfs(int[][] grid,int i,int j,int mark){
// 当超出边界,或者不是1时返回0
// 不是1:为0或其他编号,0表示海水,不计算面积,其他编号表示遍历过,不用重复计算
if(i<0||i>=grid.length||j<0||j>=grid[0].length||grid[i][j] != 1)return 0;
grid[i][j]=mark;// 给这块岛屿这个位置标记编号
// 记录当前面积1与4个方向的面积
count = 1+dfs(grid,i-1,j,mark)+dfs(grid,i+1,j,mark)+dfs(grid,i,j-1,mark)+dfs(grid,i,j+1,mark);
return count;
}
}
LeetCode 127. 单词接龙
2024.01.02 一刷
思路:
题目其实就是从beginWord开始,每次只改一个字母,找出最少的修改次数,能从beginWord变成endWord的一个方案,并且每次修改后的单词都必须在wordList中,这样就有点像无向图的最短路径问题。
可以使用BFS方法,广搜只要搜到了终点,那么一定是最短的路径,因为广搜就是以起点中心向四周扩散的搜索。
注意:
- wordList可以转成set结构,查找更快一些;
- 需要用标记map,标记着节点是否走过,否则就会死循环,key-value对应单词-该单词路径长度;
- 在广搜过程中,尝试对当前单词的每个位置都尝试一遍修改为a-z,当修改后与endWord一致就是找到了最短路径;不一致就要看看修改后的新单词是否在set中,在set中且没访问过的就加入队列,并加入map;
代码如下:
class Solution {
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
// 转为Set查找更快
Set<String> wordSet = new HashSet<>(wordList);
// 从示例2中可以看出,wordList是可能不存在endWord的,这种情况返回0
if(!wordSet.contains(endWord))return 0;
Queue<String> que = new LinkedList<>();// BFS队列
// 记录访问过的单词(避免重复访问造成循环),以及其对应的路径长度
Map<String,Integer> map = new HashMap<>();
que.offer(beginWord);
map.put(beginWord,1);// 一入队就记录访问位与对应路径长度
while(!que.isEmpty()){
String word =que.poll();// 队头出队
int path = map.get(word);// 获取该单词路径长度
// 对该单词每个位置都尝试'a'-'z'
for(int i=0;i<word.length();i++){
// 每个位置的改变都是新单词,用char[]方便修改
char[] charWord = word.toCharArray();
for(char c='a';c<='z';c++){
charWord[i] = c;// 尝试第i位修改为a-z中的一个
String newWord = new String(charWord);// 得到新单词
// 如果新单词和目标结果一致,那么最短路径就是单词路径长度+1
if(newWord.equals(endWord)){
return path+1;
}
// 只有当新单词在wordSet中,并且还没访问过,才加入队列并置添加map
if(wordSet.contains(newWord) && !map.containsKey(newWord)){
que.offer(newWord);
map.put(newWord,path+1);
}
}
}
}
// 遍历结束都没有走到endWord,就不存在
return 0;
}
}
该题还有双向BFS解法,不过自我感觉面试的时候很难写出这种解法,所以只是了解一下(代码来自代码随想录):
一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一层节点,当发现某一时刻两边都访问过同一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而提高代码运行效率。
// 双向BFS
class Solution {
// 判断单词之间是否之差了一个字母
public boolean isValid(String currentWord, String chooseWord) {
int count = 0;
for (int i = 0; i < currentWord.length(); i++)
if (currentWord.charAt(i) != chooseWord.charAt(i)) ++count;
return count == 1;
}
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
if (!wordList.contains(endWord)) return 0; // 如果 endWord 不在 wordList 中,那么无法成功转换,返回 0
// ansLeft 记录从 beginWord 开始 BFS 时能组成的单词数目
// ansRight 记录从 endWord 开始 BFS 时能组成的单词数目
int ansLeft = 0, ansRight = 0;
// queueLeft 表示从 beginWord 开始 BFS 时使用的队列
// queueRight 表示从 endWord 开始 BFS 时使用的队列
Queue<String> queueLeft = new ArrayDeque<>(), queueRight = new ArrayDeque<>();
queueLeft.add(beginWord);
queueRight.add(endWord);
// 从 beginWord 开始 BFS 时把遍历到的节点存入 hashSetLeft 中
// 从 endWord 开始 BFS 时把遍历到的节点存入 hashSetRight 中
Set<String> hashSetLeft = new HashSet<>(), hashSetRight = new HashSet<>();
hashSetLeft.add(beginWord);
hashSetRight.add(endWord);
// 只要有一个队列为空,说明 beginWord 无法转换到 endWord
while (!queueLeft.isEmpty() && !queueRight.isEmpty()) {
++ansLeft;
int size = queueLeft.size();
for (int i = 0; i < size; i++) {
String currentWord = queueLeft.poll();
// 只要 hashSetRight 中存在 currentWord,说明从 currentWord 可以转换到 endWord
if (hashSetRight.contains(currentWord)) return ansRight + ansLeft;
for (String chooseWord : wordList) {
if (hashSetLeft.contains(chooseWord) || !isValid(currentWord, chooseWord)) continue;
hashSetLeft.add(chooseWord);
queueLeft.add(chooseWord);
}
}
++ansRight;
size = queueRight.size();
for (int i = 0; i < size; i++) {
String currentWord = queueRight.poll();
// 只要 hashSetLeft 中存在 currentWord,说明从 currentWord 可以转换到 beginWord
if (hashSetLeft.contains(currentWord)) return ansLeft + ansRight;
for (String chooseWord : wordList) {
if (hashSetRight.contains(chooseWord) || !isValid(currentWord, chooseWord)) continue;
hashSetRight.add(chooseWord);
queueRight.add(chooseWord);
}
}
}
return 0;
}
}
LeetCode 841. 钥匙和房间
2024.01.03 一刷
思路:
注意这题不需要我们从0号房间开始【找出一条路径】遍历完所有房间,只需要从0开始遍历完所有房间。这二者区别就是找路径需要有回溯操作,当发现一条路径不行的时候,需要进行撤销;而只需要遍历完的话,只需要设置一个count,当前节点符合要求的时候,就count+1,不需要撤销count的值;
本题是一个有向图搜索全路径的问题,可以用深搜(DFS)或者广搜(BFS)来搜
1.dfs:
为了统计走过多少房间,需要设置一个全局变量count,如果最后count=n,说明可以全走完;
需要一个全局变量visited数组,用来记录走过了哪些房间,每走过一个房间,就标记,并且count+1;
①确认递归函数,参数:
需要传入rooms来进行遍历,还需要知道当前拿到的key,这样才知道下一个房间是几号。
②确认终止条件
在本层递归中处理节点,并且在进入下一层递归时进行判断,不符合条件的不进入下一层递归,这样就不需要终止条件;
③处理目前搜索节点
给当前房间标记访问,count+1,并且遍历该房间所有钥匙,当钥匙对应的房间是没有访问过的才进入下一层递归
代码如下:
// dfs
class Solution {
boolean[] visited;
int count;
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
int n = rooms.size();
visited = new boolean[n];
count =0;
dfs(rooms,0);
return count == n;
}
public void dfs(List<List<Integer>> rooms,int key){
visited[key]=true;
count++;
for(int x : rooms.get(key)){
if(!visited[x]){
dfs(rooms,x);
}
}
}
}
2.bfs:
同样是借助visited数组标记访问过的房间,count记录可以到达的房间数;
每次入队,都需要标记,并且计数+1;
代码如下:
// bfs
class Solution {
public boolean canVisitAllRooms(List<List<Integer>> rooms) {
int n = rooms.size();
int count=0;//表示可以到达的房间数
boolean[] visited = new boolean[n];
Queue<Integer> que = new LinkedList<>();
que.offer(0);
visited[0]=true;// 一入队就必须标记
count=1;// 标记的同时可以到达的房间数+1
while(!que.isEmpty()){
int x = que.poll();
for(int key:rooms.get(x)){
if(!visited[key]){
que.offer(key);
visited[key]=true;
count++;
}
}
}
return count == n;
}
}
LeetCode 463. 岛屿的周长
2024.01.04 一刷
思路:
遍历每一格,找出陆地,判断这个陆地格子四周的情况,只要四个方向其中之一碰到边界,或者水域,这个方向就计入一个单位周长;
代码如下:
class Solution {
public int islandPerimeter(int[][] grid) {
int count = 0;
int[][] dir = {{-1,0},{1,0},{0,-1},{0,1}};
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]==1){
for(int d =0;d<4;d++){
int nextI = i+dir[d][0];
int nextJ = j+dir[d][1];
if(nextI<0||nextI>=grid.length||nextJ<0||nextJ>=grid[0].length||grid[nextI][nextJ]==0){
count++;
}
}
}
}
}
return count;
}
}
并查集相关题
LeetCode 1971. 寻找图中是否存在路径
2024.01.10 一刷
思路:
这道题目是并查集基础题目,题目中各个点是双向图连接,判断一个顶点到另一个顶点有没有有效路径(是否连通),其实就是看这两个顶点是否在同一个集合里,使用join(int u, int v)将每条边加入到并查集。最后 isSame(int u, int v) 判断是否是同一个根,就可以了。
代码如下:
class Solution {
int[] father;
public boolean validPath(int n, int[][] edges, int source, int destination) {
father = new int[n];
init();
// 遍历所有的边,都加入并查集
for(int i=0;i<edges.length;i++){
join(edges[i][0],edges[i][1]);
}
// 如果source和destination在同一个集合,说明是连通的,存在有效路径
if(isSame(source,destination))return true;
return false;
}
// 初始化,都指向自己
public void init(){
for(int i=0;i<father.length;i++){
father[i]=i;
}
}
// 返回输入元素最终的根
public int find(int u){
if(u==father[u])return u;
else return father[u] = find(father[u]);
}
// 判断两个元素最终的根是否相同
public boolean isSame(int u,int v){
u=find(u);
v=find(v);
return u==v;
}
// 将v->u 这条边加入并查集
public void join(int u,int v){
u = find(u);
v = find(v);
// 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
if(u==v)return ;
father[v]=u;
}
}