BFS和DFS的经典应用就是在树和图中的遍历。
地图的搜索问题通常都可以用BFS和DFS解决,下面题目非常典型,并且很多题目类似,自己如果能够独立完成基本就掌握了BFS和DFS以及地图的遍历问题。
给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
分析:求解该问题,实际上就是要找多少个陆地区域,因此采用BFS、DFS两种方法均可遍历。核心思路在于,当发现一个陆地,需要将其上下左右四个方向所有可以延展到的陆地都要加入其中(可以直接将这些陆地变为海洋即可),根据此,遍历所有区域还要注意的是,这个题目的输入是 char 不是 int
BFS方法
通过一个队列,每次找到一个陆地后,进行BFS搜索,将该陆地所在的整体陆地地区全部更新为海洋。(需要注意的是,这里每次push进队列时,就直接将该位置赋值为海洋,否则会push进很多重复节点)
class Solution {
public:
int numIslands(vector< vector<char> >& grid)
{
if(grid.size()==0) return 0;
int result=0;
for(int i=0;i<grid.size();i++)
for(int j=0;j<grid[0].size();j++)
{
if(grid[i][j]=='1') //找到一个陆地就要把相连的全部变为水
{ result++;
queue< pair<int,int> > Q;
Q.push(make_pair(i,j)); //调用make_pair使(i,j)转化为pair
grid[i][j]='0'; //将该地区置为0
while(!Q.empty())
{
pair<int,int> top=Q.front();
Q.pop();
//接着把上下左右四个方向的陆地加进去
//注意push时候就要将其改为'0',要不然重复太多
if(top.first-1>=0&&grid[top.first-1][top.second]=='1')
{ grid[top.first-1][top.second]='0';
Q.push(make_pair(top.first-1,top.second));
}
if(top.second-1>=0&&grid[top.first][top.second-1]=='1')
{ grid[top.first][top.second-1]='0';
Q.push(make_pair(top.first,top.second-1));
}
if(top.first+1<grid.size()&&grid[top.first+1][top.second]=='1')
{
grid[top.first+1][top.second]='0';
Q.push(make_pair(top.first+1,top.second));
}
if(top.second+1<grid[0].size()&&grid[top.first][top.second+1]=='1')
{
grid[top.first][top.second+1]='0';
Q.push(make_pair(top.first,top.second+1));
}
}
}
}
return result;
}
};
DFS方法:
采用DFS递归深搜即可,思路与BFS一致,遍历方法不同而已。
class Solution {
void DFS(vector<vector<char>> &grid,int cur_i,int cur_j)
{
if(cur_i<0||cur_i==grid.size()||cur_j<0||cur_j==grid[0].size()||grid[cur_i][cur_j]=='0') return;
grid[cur_i][cur_j]='0'; //将其标记为海洋
int di[4]={0,0,1,-1};
int dj[4]={1,-1,0,0}; //方向数组
for(int index=0;index<4;index++) //四个方向遍历
{
DFS(grid,cur_i+di[index],cur_j+dj[index]); //找到一个陆地就直接继续深入搜索
}
return;
}
public:
int numIslands(vector<vector<char>>& grid) {
int ans=0;
for(int i=0;i<grid.size();i++)
{
for(int j=0;j<grid[0].size();j++)
{
if(grid[i][j]=='1')
{
ans++;
DFS(grid,i,j);
}
}
}
return ans;
}
};
给定两个单词(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" 不在字典中,所以无法进行转换。
分析:(这个mid难度有点hard)
将单词之间的转换,理解为一张图,即如果两个单词之间可以转换(相差一个字母),就说明这两个单词之间存在一条边。因此,问题就变成了从无向无权图中起始单词到终止单词的最短路径。
因此,如果有了这个图,则直接采用BFS即可得到解。故该问题的难点就在于如何构建该图?可以根据字典构建一个邻接表,将所有可以转换的单词之间都联系起来,为了方便期间,可以用MAP进行构建图,map的key是单词的string序列,map的value是 vector
在此没有太好的方法,双重循环wordlist建立该邻接表。
在宽搜时,需要标记已经访问过的元素,但是由于元素是string类型而非int类型,没办法通过一个visit数组来标记,故在此只能采用一个set存储已经访问过的元素。
class Solution {
public:
bool connect(const string& word1,const string &word2)//判断两个单词是否仅一个字母不同
{
int dif=0; //不同的字母数量
for(int i=0;i<word1.length();i++)
if(word1[i]!=word2[i])
dif++;
return dif==1;
}
//构建图,graph必须为引用
void construct_graph(string &beginWord,vector<string> &wordList,unordered_map<string,vector<string> > &graph)
{
wordList.push_back(beginWord);
for(int i=0;i<wordList.size();i++)
for(int j=i+1;j<wordList.size();j++)//避免重复
{
if(connect(wordList[i],wordList[j])==true)
{
graph[wordList[i]].push_back(wordList[j]);
graph[wordList[j]].push_back(wordList[i]);
}
}
}
int ladderLength(string beginWord, string endWord, vector<string>& wordList)
{
unordered_map<string,vector<string> > graph;
construct_graph(beginWord,wordList,graph);
queue<string> Q;
unordered_set<string> visit;//记录已经访问过的元素
Q.push(beginWord);
visit.insert(beginWord);
int result=1;
while(!Q.empty())
{
int size=Q.size();
for(int i=0;i<size;i++) //每一次循环代表宽度增1
{
string tmp=Q.front();
Q.pop();
vector<string> neighbors=graph[tmp]; //邻居们
for(int i=0;i<neighbors.size();i++)
{
if(visit.find(neighbors[i])==visit.end()) //还没加入
{
Q.push(neighbors[i]);
visit.insert(neighbors[i]);
}
if(neighbors[i]==endWord) return result+1;
}
}
result++; //遍历一层则+1
}
return 0;
}
};
可能在一些技巧上可以优化一些时间,但是算法复杂度基本不容易改变。
双向BFS也是可以解决该题,在时间复杂度上会有所改善。
还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。
输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。
示例 1:
输入: [1,1,2,2,2]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。
示例 2:
输入: [3,3,3,3,4]
输出: false
解释: 不能用所有火柴拼成一个正方形。
注意:
给定的火柴长度和在 0 到 10^9之间。
火柴数组的长度不超过15。
分析:要想组成正方形,需要四条边都等于总和的1/4,因此需要将火柴分成四组,使得每一个火柴都属于其中的一组。因此可以考虑回溯,将四条边想象成四个桶,如果将当前的边加入到一个桶中没有违背条件,则在此基础上继续回溯。如果回溯到最后也无法满足,或者如果某个边加入后就超出理应的边长了,就剪枝。
一些优化的策略:
如果火柴杆的总和不是4的倍数,直接返回false即可。
将火柴杆从大到小排序,优先选用大的边可以令不成功的情况更快的返回。
class Solution {
public:
bool makesquare(vector<int>& nums)
{
if(nums.size()<4)
return false; //边数小于4
int sum=accumulate(nums.begin(), nums.end(), 0);
if(sum%4) return false; //和不是4的倍数
sort(nums.begin(),nums.end(),greater<int>()); //降序排列
vector<int> bucket(4); //4个边的值
return dfs(0,nums,sum/4,bucket);
}
bool dfs(int index,vector<int>& nums,int target,vector<int> &bucket)
{ //index为当前遍历到的下标,nums为边长数组,target为目标边长,bucket表示当前每条边的长度
if(index>=nums.size()) //每条边都用了
return bucket[0]==target&&bucket[1]==target&&bucket[2]==target;
for(int i=0;i<4;i++) //将当前边放在四个桶中分别尝试
{
if(bucket[i]+nums[index]>target) //说明不可以放在这一个边
continue;
bucket[i]+=nums[index]; //放入该边后继续DFS
if(dfs(index+1,nums,target,bucket))
return true;
bucket[i]-=nums[index]; //注意回溯的恢复状态
}
return false;
}
};
给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回锯齿形层次遍历如下:
[
[3],
[20,9],
[15,7]
]
分析:该题可以理解为二叉树的层次遍历,不同的是,对于不同层数的遍历方向不一致。总体来说层次遍历采用BFS是比较直观的,因此起始可以考虑用一个flag标记,不需要考虑如何入队,只需要考虑如何将每次遍历的节点如何插入即可。也可以用双端队列,根据需求从头入队或者从尾入队。
class Solution {
public:
vector< vector<int> > zigzagLevelOrder(TreeNode* root)
{
vector< vector<int> > result;
queue<TreeNode*> Q;
if(root==NULL) return result;
bool flag=true; //true表示正向,false表示负向
Q.push(root);
while(!Q.empty())
{
vector<int> floor;//记录该层答案
int len=Q.size();
while(len>0)
{
TreeNode* tmp=Q.front();
Q.pop();
if(flag==true)
floor.push_back(tmp->val); //正向插入值
else
floor.insert(floor.begin(),tmp->val);//反向插入值
if(tmp->left!=NULL) Q.push(tmp->left);
if(tmp->right!=NULL) Q.push(tmp->right);
len--;
}
result.push_back(floor);
flag=!flag;
}
return result;
}
};
给定一个二维的矩阵,包含 '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'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。
分析:在经过替换后,除了边界的O和与边界O相同的O,其他地方应该都会被X替代。因此,可以考虑找到中间区域中与边界O相连的地方,可以将这些地区标记,在最后替换时,将标记区域不进行更改仍然保存为O,未标记区域改为X即可。
因此可以考虑遍历整个边界,采用BFS将其连接的O全部进行标记,然后进行替换即可。故与岛屿问题类似。
class Solution {
public:
void solve(vector< vector<char> >& board)
{
if(board.size()==0||board.size()==1) return;
int row=board.size(),col=board[0].size();
queue< pair<int,int> > Q;
//首先将所有的边界上的O坐标入队列
for(int i=0;i<col;i++)
{
if(board[0][i]=='O')
{board[0][i]='N';Q.push(make_pair(0,i));}
if(board[row-1][i]=='O')
{board[row-1][i]='N';Q.push(make_pair(row-1,i));}
}
for(int i=1;i<row-1;i++)
{
if(board[i][0]=='O')
{board[i][0]='N';Q.push(make_pair(i,0));}
if(board[i][col-1]=='O')
{board[i][col-1]='N';Q.push(make_pair(i,col-1));}
}
//然后将队列进行扩展,
while(!Q.empty())
{
pair<int,int> top=Q.front();
Q.pop();
//遍历上下左右,遍历找到则直接修改其值,避免大量重复
if(top.first-1>0&&board[top.first-1][top.second]=='O')
{
board[top.first-1][top.second]='N';
Q.push(make_pair(top.first-1,top.second));
}
if(top.second-1>0&&board[top.first][top.second-1]=='O')
{
board[top.first][top.second-1]='N';
Q.push(make_pair(top.first,top.second-1));
}
if(top.first+1<row-1&&board[top.first+1][top.second]=='O')
{
board[top.first+1][top.second]='N';
Q.push(make_pair(top.first+1,top.second));
}
if(top.second+1<col-1&&board[top.first][top.second+1]=='O')
{
board[top.first][top.second+1]='N';
Q.push(make_pair(top.first,top.second+1));
}
}
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
board[i][j]=(board[i][j]=='N')?'O':'X';
return ;
}
};
在给定的网格中,每个单元格可以有以下三个值之一:
值 0 代表空单元格; 值 1 代表新鲜橘子; 值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。
分析:该题和上个题:被围绕的区域 非常相似,都可以采用多源BFS,即最初将所有的腐败水果加入队列,类似于树的层次遍历,每次把当前时间的所有腐烂水果进行BFS扩散,记录即可。每次入队列时即将其值改为2,也就是一旦入队列以为着已经腐烂,避免多次入队列。
class Solution {
public:
int orangesRotting(vector< vector<int> >& grid)
{
if(grid.size()==0) return 0;
int row=grid.size(),col=grid[0].size();
queue< pair<int,int> > Q;
//将所有的烂橘子入队列
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
{
if(grid[i][j]==2) //烂橘子
{
Q.push(make_pair(i,j));//将其入队列
}
}
int result=-1;
while(!Q.empty())
{
int size=Q.size(); //记录当前队列的size
while(size>0)
{
pair<int,int> top=Q.front();
Q.pop();
//将上下左右的好橘子腐烂并且入队列
if(top.first-1>=0&&grid[top.first-1][top.second]==1)
{
grid[top.first-1][top.second]=2;
Q.push(make_pair(top.first-1,top.second));
}
if(top.second-1>=0&&grid[top.first][top.second-1]==1)
{
grid[top.first][top.second-1]=2;
Q.push(make_pair(top.first,top.second-1));
}
if(top.first+1<row&&grid[top.first+1][top.second]==1)
{
grid[top.first+1][top.second]=2;
Q.push(make_pair(top.first+1,top.second));
}
if(top.second+1<col&&grid[top.first][top.second+1]==1)
{
grid[top.first][top.second+1]=2;
Q.push(make_pair(top.first,top.second+1));
}
size--;
}
result++;
}
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
if(grid[i][j]==1) return -1; //说明不能全部腐烂
if(result==-1) return 0;//说明队列是空的,
else return result;
}
};
给定一个包含了一些 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。
分析:该题还是一个典型的地图遍历题目,遍历所有位置,如果该位置是土地,则利用BFS或者DFS将该土地连接的地方全部置为海洋并记录土地的面积。期间动态记录最大值即可。
class Solution {
public:
int BFS(vector< vector<int> >& grid,int i,int j)
{
int ans=0;
queue< pair<int,int> > Q;
int row=grid.size(),col=grid[0].size();
Q.push(make_pair(i,j));
ans++;
grid[i][j]=0; //将其加入队列后即将其置为0
while(!Q.empty())
{
pair<int,int> top=Q.front();
Q.pop();
//遍历上下左右
if(top.first-1>=0&&grid[top.first-1][top.second]==1)
{
grid[top.first-1][top.second]=0;
Q.push(make_pair(top.first-1,top.second));
ans++;
}
if(top.second-1>=0&&grid[top.first][top.second-1]==1)
{
grid[top.first][top.second-1]=0;
Q.push(make_pair(top.first,top.second-1));
ans++;
}
if(top.first+1<row&&grid[top.first+1][top.second]==1)
{
grid[top.first+1][top.second]=0;
Q.push(make_pair(top.first+1,top.second));
ans++;
}
if(top.second+1<col&&grid[top.first][top.second+1]==1)
{
grid[top.first][top.second+1]=0;
Q.push(make_pair(top.first,top.second+1));
ans++;
}
}
return ans;
}
int maxAreaOfIsland(vector< vector<int> >& grid)
{
if(grid.size()==0) return 0;
int row=grid.size(),col=grid[0].size();
//遍历所有节点
int result=0;
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
{
if(grid[i][j]==1) //说明找到陆地
result=max(result,BFS(grid,i,j));
}
return result;
}
};
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例 1:
输入:
0 0 0
0 1 0
0 0 0
输出:
0 0 0
0 1 0
0 0 0
示例 2:
输入:
0 0 0
0 1 0
1 1 1
输出:
0 0 0
0 1 0
1 2 1
分析:这个题目和腐烂的橘子是一样的! 采用多源BFS,将所有的0加入队列,记录队列的尺寸,就像遍历树一样,每遍历一层就将该层可以到达的所有点都置入队列且修改其值,直至队列为空。在这里我采用了方向数组的方法表示上下左右
class Solution {
public:
vector< vector<int> > updateMatrix(vector< vector<int> >& matrix)
{
vector< vector<int> > result=matrix;
if(matrix.size()==0) return result;
int row=matrix.size(),col=matrix[0].size();
queue< pair<int,int> > Q;
for(int i=0;i<row;i++)
for(int j=0;j<col;j++) //将所有的0入队列
{
if(matrix[i][j]==0)
{
Q.push(make_pair(i,j));
}
}
int dx[4]={0,0,-1,1};
int dy[4]={-1,1,0,0}; //上下左右
int floor=1;
while(!Q.empty())
{
int size=Q.size();
while(size>0)
{
pair<int,int> top=Q.front();
Q.pop();
for(int i=0;i<4;i++) //4个方向
{
int new_y=top.first+dx[i],new_x=top.second+dy[i]; //x横坐标,y纵坐标
if(new_y>=0&&new_x>=0&&new_y<row&&new_x<col&&matrix[new_y][new_x]==1)
{
matrix[new_y][new_x]=0;
result[new_y][new_x]=floor; //更新该点的result
Q.push(make_pair(new_y,new_x));
}
}
size--;
}
floor++;
}
return result;
}
};