文章目录
算法解释
深度优先搜索和广度优先搜索是两种常见的优先搜索算法,广泛应用于图和树等结构中搜索。
深度优先搜索
深度优先搜索(depth-first seach,DFS)在搜索到一个新的节点时,立即对该新节点进行遍历;因此遍历需要用先入后出的栈来实现,也可以通过与栈等价的递归来实现。对于树结构而言,由于总是对新节点调用遍历,因此看起来是向着“深”的方向前进。
深度优先搜索也可以用来检测环路:记录每个遍历过的节点的父节点,若一个节点被再次遍历且父节点不同,则说明有环。我们也可以用之后会讲到的拓扑排序判断是否有环路,若最后存在入度不为零的点,则说明有环。
有时我们可能会需要对已经搜索过的节点进行标记,以防止在遍历时重复搜索某个节点,这种做法叫做状态记录或记忆化(memoization)。
695.岛屿的最大面积(medium)
题目描述
给定一个二维的 0-1 矩阵,其中 0 表示海洋,1 表示陆地。单独的或相邻的陆地可以形成岛屿,每个格子只与其上下左右四个格子相邻。求最大的岛屿面积
输入输出样例
输入:grid = [[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 。
题解
一般来说,深度优先搜索类型的题可以分为主函数和辅函数,主函数用于遍历所有的搜索位置,判断是否可以开始搜索,如果可以即在辅函数进行搜索。辅函数则负责深度优先搜索的递归调用。当然,我们也可以使用栈(stack)实现深度优先搜索,但因为栈与递归的调用原理相同,而递归相对便于实现,因此推荐使用递归式写法,同时也方便进行回溯(见下节)。不过在实际工程上,直接使用栈可能才是最好的选择,一是因为便于理解,二是更不易出现递归栈满的情况。
代码
class Solution {
public:
int dfs(vector<vector<int>>& grid,int r,int c){
if(grid[r][c]==0) return 0;
grid[r][c]=0;
int area=1;
if(r+1>=0&&r+1<grid.size()&&c>=0&&c<grid[0].size()){
area+=dfs(grid,r+1,c);
}
if(r-1>=0&&r-1<grid.size()&&c>=0&&c<grid[0].size()){
area+=dfs(grid,r-1,c);
}
if(r>=0&&r<grid.size()&&c+1>=0&&c+1<grid[0].size()){
area+=dfs(grid,r,c+1);
}
if(r>=0&&r<grid.size()&&c-1>=0&&c-1<grid[0].size()){
area+=dfs(grid,r,c-1);
}
return area;
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(grid.empty()||grid[0].empty()) return 0;
int maxarea=0;
for(int i=0;i<grid.size();i++){
for(int j=0;j<grid[i].size();j++){
maxarea=max(maxarea,dfs(grid,i,j));
}
}
return maxarea;
}
};
因为没有用方向数组显得代码比较乱,下面介绍第二种的递归写法
class Solution {
public:
int dfs(vector<vector<int>>& grid,int r,int c){
if(r<0||r>=grid.size()||c<0||c>=grid[0].size()||grid[r][c]==0) return 0;
grid[r][c]=0;
return 1+dfs(grid,r+1,c)+dfs(grid,r-1,c)+dfs(grid,r,c+1)+dfs(grid,r,c-1);
}
int maxAreaOfIsland(vector<vector<int>>& grid) {
if(grid.empty()||grid[0].empty()) return 0;
int maxarea=0;
for(int i=0;i<grid.size();i++){
for(int j=0;j<grid[i].size();j++){
maxarea=max(maxarea,dfs(grid,i,j));
}
}
return maxarea;
}
};
547.省份数量(Medium)
题目描述
有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
输入输出描述
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
题解
这题需要一个额外数组来记录遍历状态
代码
class Solution {
public:
void dfs(vector<vector<int>>& isConnected,vector<bool>& isTrave,int i){
if(isTrave[i]) return;
isTrave[i]=true;
for(int j=0;j<isConnected.size();j++){
if(isConnected[i][j]==1&&!isTrave[j]){
dfs(isConnected,isTrave,j);
}
}
}
int findCircleNum(vector<vector<int>>& isConnected) {
vector<bool> isTrave(isConnected.size(),false);
int ret=0;
for(int i=0;i<isConnected.size();i++){
if(!isTrave[i]){
dfs(isConnected,isTrave,i);
ret++;
}
}
return ret;
}
};
417.太平洋大西洋水流问题
题目描述
给定一个二维的非负整数矩阵,每个位置的值表示海拔高度。假设左边和上边是太平洋,右边和下边是大西洋,求从哪些位置向下流水,可以流到太平洋和大西洋。水只能从海拔高的位置流到海拔低或相同的位置。
输入输出样例
给定下面的 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]] (上图中带括号的单元).
题解
虽然题目要求的是满足向下流能到达两个大洋的位置,如果我们对所有的位置进行搜索,那么在不剪枝的情况下复杂度会很高。因此我们可以反过来想,从两个大洋开始向上流,这样我们只需要对矩形四条边进行搜索。搜索完成后,只需遍历一遍矩阵,满足条件的位置即为两个大洋向上流都能到达的位置。
代码
class Solution {
public:
vector<int> direction{-1,0,1,0,-1};
void dfs(vector<vector<int>>& heights,vector<vector<bool>>& trace,int r,int c){
if(trace[r][c]) return;
trace[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<heights.size()&&y>=0&&y<heights[0].size()&&heights[r][c]<=heights[x][y]){
dfs(heights,trace,x,y);
}
}
}
vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
if(heights.empty()||heights[0].empty()) return {};
vector<vector<int>> res;
int m=heights.size(),n=heights[0].size();
vector<vector<bool>> can_trace_p(m,vector<bool>(n,false));
vector<vector<bool>> can_trace_a(m,vector<bool>(n,false));
for(int i=0;i<m;i++){
dfs(heights,can_trace_p,i,0);
dfs(heights,can_trace_a,i,n-1);
}
for(int i=0;i<n;i++){
dfs(heights,can_trace_p,0,i);
dfs(heights,can_trace_a,m-1,i);
}
for(int i=0;i<m;i++){
for(int j=0;j<n;j++){
if(can_trace_a[i][j]&&can_trace_p[i][j]) res.push_back({i,j});
}
}
return res;
}
};
130.被围绕的区域
题目描述
给你一个 m x n 的矩阵 board ,由若干字符 ‘X’ 和 ‘O’ ,找到所有被 ‘X’ 围绕的区域,并将这些区域里所有的 ‘O’ 用 ‘X’ 填充。
输入输出样例
输入:board = [["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'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
题解
任何边界上的 O 都不会被填充为 X。 我们可以想到,所有的不被包围的 O 都直接或间接与边界上的 O 相连。
代码
class Solution {
public:
void dfs(vector<vector<char>>& board,int r,int c){
if(r<0||r>=board.size()||c<0||c>=board[0].size()||board[r][c]!='O') return;
board[r][c]='A';
dfs(board,r+1,c);
dfs(board,r-1,c);
dfs(board,r,c+1);
dfs(board,r,c-1);
}
void solve(vector<vector<char>>& board) {
//通过dfs,将与边界相连或者在边界上的O置为A
for(int i=0;i<board.size();i++){
dfs(board,i,0);
dfs(board,i,board[0].size()-1);
}
for(int j=1;j<board[0].size()-1;j++){
dfs(board,0,j);
dfs(board,board.size()-1,j);
}
//遍历,完成题目要求
for(int i=0;i<board.size();i++){
for(int j=0;j<board[0].size();j++){
if(board[i][j]=='A'){
board[i][j]='O';
}else{
board[i][j]='X';
}
}
}
}
};
257.二叉树的所有路径
题目描述
给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。
叶子节点 是指没有子节点的节点。
输入输出样例
输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]
题解
直接使用深度优先搜索,从root开始遍历。当root为nullptr时,就结束。
- 如果当前结点不是叶子结点,继续遍历左右结点。
- 如果是叶子结点,则将该路径加入到答案中。
代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<string> res;
void dfs(TreeNode* root,string s){
if(root==nullptr) return;
s+=to_string(root->val);
if(root->left==nullptr&&root->right==nullptr) {
res.push_back(s);
}
else{
s+="->";
dfs(root->left,s);
dfs(root->right,s);
}
}
vector<string> binaryTreePaths(TreeNode* root) {
string s;
dfs(root,s);
return res;
}
};
回溯法
具体详见另一笔记(正在完成,稍后更新):
广度优先搜索
广度优先搜索(breadth-first search,BFS)不同与深度优先搜索,它是一层层进行遍历的,因此需要用先入先出的队列而非先入后出的栈进行遍历。由于是按层次进行遍历,广度优先搜索时按照“广”的方向进行遍历的,也常常用来处理最短路径等问题。
934.最短的桥
题目描述
给定一个二维 0-1 矩阵,其中 1 表示陆地,0 表示海洋,每个位置与上下左右相连。已知矩阵中有且只有两个岛屿,求最少要填海造陆多少个位置才可以将两个岛屿相连。
输入输出样例
输入:A = [[0,1],[1,0]]
输出:1
题解
这题我认为是比较难的,需要先确定两个岛屿,然后将其中一座岛屿不断延申,直到到达另一座的岛屿。
在寻找岛屿时,我们使用深度优先搜索。向外延伸时,使用广度优先搜索。
其中,最小步数就是广度优先搜索的层数
详解看代码备注
代码
class Solution {
public:
vector<int> direction{-1,0,1,0,-1};
int shortestBridge(vector<vector<int>>& grid) {
int m=grid.size(),n=grid[0].size();
queue<pair<int,int>> points;
//dfs寻找岛屿A,并把1全部赋值为2;设置标志位,找到第一个1后立即停止
bool flag=false;
for(int i=0;i<m;i++){
if(flag) break;
for(int j=0;j<n;j++){
if(grid[i][j]==1){
dfs(points,grid,i,j);
flag=true;
break;
}
}
}
//通过广度优先搜索找到岛屿B,并把寻找过程中0变为2,延伸岛屿A
//延伸一圈,步数+1,直到与另一座岛屿B接触
int x,y;
int level=0;
while(!points.empty()){
++level;
int n_points=points.size();
while(n_points--){
//取出一个岛屿A周围的0,并遍历。
auto [r,c]=points.front();
points.pop();
//如果遇到1,则说明两岛屿相遇,如果没有,则继续延伸
for(int k=0;k<4;k++){
x=r+direction[k],y=c+direction[k+1];
//如果没有越界
if(x>=0&&y>=0&&x<m&&y<n){
if(grid[x][y]==2) continue;
if(grid[x][y]==1) return level;
points.push({x,y});
grid[x][y]=2;
}
}
}
}
return 0;
}
//深度优先搜索,将一座岛屿A变成2
void dfs(queue<pair<int,int>>& points,vector<vector<int>>& grid,int i,int j){
int m=grid.size(),n=grid[0].size();
//如果越界或者已经遍历到的,直接返回
if(i<0||j<0||i>=m||j>=n||grid[i][j]==2) return;
//将这座岛屿A周围为0的区域存储起来,并返回
if(grid[i][j]==0){
points.push({i,j});
return;
}
//将岛屿A设为2,并遍历四周
grid[i][j]=2;
dfs(points,grid,i+1,j);
dfs(points,grid,i-1,j);
dfs(points,grid,i,j+1);
dfs(points,grid,i,j-1);
}
};
126.单词接龙Ⅱ(Hard)
题目描述
给定一个起始字符串和一个终止字符串,以及一个单词表,求是否可以将起始字符串每次改一个字符,直到改成终止字符串,且所有中间的修改过程表示的字符串都可以在单词表里找到。若存在,输出需要修改次数最少的所有更改方式。
输入输出样例
输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"]
输出:[["hit","hot","dot","dog","cog"],["hit","hot","lot","log","cog"]]
解释:存在 2 种最短的转换序列:
"hit" -> "hot" -> "dot" -> "dog" -> "cog"
"hit" -> "hot" -> "lot" -> "log" -> "cog"
题解
我们可以把起始字符串、终止字符串、以及单词表里所有的字符串想象成节点。若两个字符串只有一个字符不同,那么它们相连。因为题目需要输出修改次数最少的所有修改方式,因此我们可以使用广度优先搜索,求得起始节点到终止节点的最短距离。
在搜索结束后,我们还需要通过回溯法来重建所有可能的路径。
代码
在这里插入代码片