leetcode 经典宽搜BFS深搜DFS题目(思路、方法、code)

leetcode 经典宽搜BFS深搜DFS题目(思路、方法、code)

BFS和DFS的经典应用就是在树和图中的遍历。

地图的搜索问题通常都可以用BFS和DFS解决,下面题目非常典型,并且很多题目类似,自己如果能够独立完成基本就掌握了BFS和DFS以及地图的遍历问题。

文章目录

      • leetcode 经典宽搜BFS深搜DFS题目(思路、方法、code)
        • [200. 岛屿数量](https://leetcode-cn.com/problems/number-of-islands/)
        • [127. 单词接龙](https://leetcode-cn.com/problems/word-ladder/)
        • [473. 火柴拼正方形](https://leetcode-cn.com/problems/matchsticks-to-square/)
        • [103. 二叉树的锯齿形层次遍历](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)
        • [130. 被围绕的区域](https://leetcode-cn.com/problems/surrounded-regions/)
        • [994. 腐烂的橘子](https://leetcode-cn.com/problems/rotting-oranges/)
        • [695. 岛屿的最大面积](https://leetcode-cn.com/problems/max-area-of-island/)
        • [542. 01 矩阵](https://leetcode-cn.com/problems/01-matrix/)

200. 岛屿数量

给你一个由 ‘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;
    }

};

127. 单词接龙

给定两个单词(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)

将单词之间的转换,理解为一张图,即如果两个单词之间可以转换(相差一个字母),就说明这两个单词之间存在一条边。因此,问题就变成了从无向无权图中起始单词到终止单词的最短路径

leetcode 经典宽搜BFS深搜DFS题目(思路、方法、code)_第1张图片
因此,如果有了这个图,则直接采用BFS即可得到解。故该问题的难点就在于如何构建该图?可以根据字典构建一个邻接表,将所有可以转换的单词之间都联系起来,为了方便期间,可以用MAP进行构建图,map的key是单词的string序列,map的value是 vector ,即当前key可以转换到的单词序列。

在此没有太好的方法,双重循环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也是可以解决该题,在时间复杂度上会有所改善。

473. 火柴拼正方形

还记得童话《卖火柴的小女孩》吗?现在,你知道小女孩有多少根火柴,请找出一种能使用所有火柴拼成一个正方形的方法。不能折断火柴,可以把火柴连接起来,并且每根火柴都要用到。

输入为小女孩拥有火柴的数目,每根火柴用其长度表示。输出即为是否能用所有的火柴拼成正方形。

示例 1:
输入: [1,1,2,2,2]
输出: true
解释: 能拼成一个边长为2的正方形,每边两根火柴。

示例 2:
输入: [3,3,3,3,4]
输出: false
解释: 不能用所有火柴拼成一个正方形。

注意:
给定的火柴长度和在 010^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;
	}
};

103. 二叉树的锯齿形层次遍历

给定一个二叉树,返回其节点值的锯齿形层次遍历。(即先从左往右,再从右往左进行下一层遍历,以此类推,层与层之间交替进行)。

例如:
给定二叉树 [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;
    }
};

130. 被围绕的区域

给定一个二维的矩阵,包含 '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 ;	
    }
};

994. 腐烂的橘子

在给定的网格中,每个单元格可以有以下三个值之一:

值 0 代表空单元格; 值 1 代表新鲜橘子; 值 2 代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。

返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1。

leetcode 经典宽搜BFS深搜DFS题目(思路、方法、code)_第2张图片

分析:该题和上个题:被围绕的区域 非常相似,都可以采用多源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;   
    }
};

695. 岛屿的最大面积

给定一个包含了一些 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;	
    }
};

542. 01 矩阵

给定一个由 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;
	}
    
};

你可能感兴趣的:(数据结构与算法)