深度优先搜索(depth-first seach,DFS)在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。 考虑如下一颗简单的树。我们从 1 号节点开始遍历,假如遍历顺序是从左子节点到右子节点,那么按照优先向着“深”的方向前进的策略,假如我们使用递归实现,我们的遍历过程为 1(起始节点)->2(遍历更深一层的左子节点)->4(遍历更深一层的左子节点)->2(无子节点,返回父结点)->1(子节点均已完成遍历,返回父结点)->3(遍历更深一层的右子节点)->1(无子节点,返回父结点)-> 结束程序(子节点均已完成遍历)。如果我们使用栈实现,我们的栈顶元素的变化过程为 1->2->4->3。
深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。 有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化(memoization)。
(1)695. Max Area of Island (Easy)
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目描述
给定一个二维的 0-1 矩阵,其中 0 表示海洋,1 表示陆地。单独的或相邻的陆地可以形成岛屿,每个格子只与其上下左右四个格子相邻。求最大的岛屿面积。
个人思路
很久没做深搜了,看到的时候有点懵,有点大概的思路,知道应该一步步去搜索然后递归返回,但不知道如何转化成代码形式去解决,最终看了题解后才恍然大悟。注意这种上下左右搜索的题目,如果加一个方向标识会简单很多。并且此题是标准的搜索题,一定要牢记。
题解思路
一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。当然,我们也可以使用栈(stack)实现深度优先搜索,但因为栈与递归的调用原理相同,而递归相对便于实现,因此刷题时笔者推荐使用递归式写法,同时也方便进行回溯(见下节)。不过在实际工程上,直接使用栈可能才是最好的选择,一是因为便于理解,二是更不易出现递归栈满的情况。
展示代码
class Solution {
public:
vector<int> direct={-1,0,1,0,-1};
int dfs(vector<vector<int>>& grid,int a,int b){
if(grid[a][b]==0)
return 0;
grid[a][b]=0;
int x,y,area=1;
for(int i=0;i<4;i++){
x=a+direct[i];
y=b+direct[i+1];
if(x>=0&&x<grid.size()&&y>=0&&y<grid[0].size())
area+=dfs(grid,x,y);
}
return area;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if (grid.empty() || grid[0].empty()) return 0;
int maxs=0;
for(int i=0;i<grid.size();i++){
for(int j=0;j<grid[0].size();j++){
if(grid[i][j]==1)
maxs=max(maxs,dfs(grid,i,j));
}
}
return maxs;
}
};
(2)省份问题 (Medium)
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目描述
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
个人思路
自己做的时候有点绕进去了,没想明白,看了题解后理解了。直接上题解吧。
题解思路
对于题目 695,图的表示方法是,每个位置代表一个节点,每个节点与上下左右四个节点相邻。而在这一道题里面,每一行(列)表示一个节点,它的每列(行)表示是否存在一个相邻节点。因此题目 695 拥有 m × n 个节点,每个节点有 4 条边;而本题拥有 n 个节点,每个节点最多有 n 条边,表示和所有人都是相邻城市,最少可以有 1 条边,表示自己与自己相连。当清楚了图的表示方法后,这道题与题目 695 本质上是同一道题:搜索省份(岛屿)的个数(最大面积)。
代码展示
class Solution {
public:
void dfs(vector<vector<int>>& isConnected,vector<bool>&visited,int i){
visited[i]=true;
for(int j=0;j<visited.size();j++){
if(isConnected[i][j]==1&&!visited[j])
dfs(isConnected,visited,j);
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
int num=0;
vector<bool> visited(isConnected.size(),false);
for(int i=0;i<isConnected.size();i++){
if(!visited[i]){
dfs(isConnected,visited,i);
num++;
}
}
return num;
}
};
(3)417. Pacific Atlantic Water Flow (Medium)
力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台
题目描述
给定一个二维的非负整数矩阵,每个位置的值表示海拔高度。假设左边和上边是太平洋,右
边和下边是大西洋,求从哪些位置向下流水,可以流到太平洋和大西洋。水只能从海拔高的位置
流到海拔低或相同的位置。
个人思路
看到这个题目的第一反应就是一二题的叠加态,而且是个非常明显的搜索题。但是如果直接遍历的话,递归的时间会非常长,会有非常多重复的判断。结合第二题的思路,我们利用剪枝来解决这个问题。 其实这道题递归到最后,由于所有水流都是要流入海洋的,因此我们可以先解决四边的问题。
代码展示
class Solution {
public:
vector<int> direction{-1, 0, 1, 0, -1};
// 辅函数 - 深度优先搜索
void dfs(const vector<vector<int>>& matrix, vector<vector<bool>>& can_reach,
int r, int c) {
if (can_reach[r][c]) {
return;
}
can_reach[r][c] = true;
int x, y;
for (int i = 0; i < 4; ++i) {
x = r + direction[i], y = c + direction[i+1];
if (x >= 0 && x < matrix.size()
&& y >= 0 && y < matrix[0].size() &&
matrix[r][c] <= matrix[x][y]) {
dfs(matrix, can_reach, x, y);
}
}
}
// 主函数
vector<vector<int>> pacificAtlantic(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) {
return {};
}
vector<vector<int>> ans;
int m = matrix.size(), n = matrix[0].size();
vector<vector<bool>> can_reach_p(m, vector<bool>(n, false));
vector<vector<bool>> can_reach_a(m, vector<bool>(n, false));
// 从太平洋边缘出发,标记可以到达的位置
for (int i = 0; i < m; ++i) {
dfs(matrix, can_reach_p, i, 0);
}
for (int i = 0; i < n; ++i) {
dfs(matrix, can_reach_p, 0, i);
}
// 从大西洋边缘出发,标记可以到达的位置
for (int i = 0; i < m; ++i) {
dfs(matrix, can_reach_a, i, n - 1);
}
for (int i = 0; i < n; ++i) {
dfs(matrix, can_reach_a, m - 1, i);
}
// 寻找既可以到达太平洋又可以到达大西洋的位置
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; ++j) {
if (can_reach_p[i][j] && can_reach_a[i][j]) {
ans.push_back(vector<int>{i, j});
}
}
}
return ans;
}
};
总结
在学算法的时候,学深搜和广搜的时候有点偷懒,导致在做题的时候有种似懂非懂的感觉。学习的时候还是要更加认真一点才行。