一.最短路径——深度优先搜索
(1)寻找最短路径时,DFS所有路径都要走一遍(缺点),BFS只需要走一条路径。
(2)如果只是找到一条路并非最短路,那么BFS和DFS复杂度是一样的。
(3)以DFS寻找最短路径为例,当搜索图为一张图时(非树,树的话每个点只经过一次,回溯的时候再次回到某个点不代表是第二次哦),并且可走的路径很多,DFS选择不同路径时会在某些点(如右下图A点)重复走,所用时间是极大的(而BFS一个点只走一次)。
如果每个点只走一次,那么广度优先搜索能找到最短路径,深度优先搜索只能得到一条路径,不一定是最短路径。
(4)深度优先搜索有模板,只需稍微改动
(5)特点:回溯
(6)要标记一下哪些点已经走过了,防止进入下面这样的死循环
1.题目描述:
输入样例:
3 3
S*#
**#
#*E
输出样例:
4
问题解决(下面的代码可看做模板):
套模板时要注意改动的地方:
(1)寻找路径的时候,一个点被践踏多次,所以回溯的时候要把这个点标记为0(没走过)
(2)根据题目,有上、下、左、右四个方向
主函数
int main{
ans=99999999是为了第一次min的时候,让ans=step
注意:
(1)mpt和vis都需要初始化成0,mpt初始化为0的目的是,待会赋值完后图以外的地方都为0,就形成了一个边界
(2)输入图时的for循环,i从1开始的,scanf的时候从mpt[i]+1开始的,也就是说不去用图的第0行和第0列。这样的好处是,在dfs函数中,不需要判断dx,dy减1后是否超出边界(变成-1)。
(3)在输入的while循环那里不要加!=EOF,根本没用!加了反而出错!不嫌弃的话可以看一下!=EOF的解释
#include<bits/stdc++.h>
using namespace std;
char grid[101][101];
int vis[101][101];
//大括号内含小括号!!!
int dir[4][2] = { {-1, 0}, {0, -1}, {0, 1}, {1, 0}};
int ans=9999999;
void DFS(int x, int y,int step)
{
if(step>ans) //剪枝 (可以不考虑)
return;
if(grid[x][y]=='E'){
ans=min(ans,step);
return;//递归出口--终点
}
for(int i = 0; i<4; i++)
{
int xx = x + dir[i][0];
int yy = y + dir[i][1];
//if(xx<0 || xx>=n || yy<0 || yy>=m) continue; //越界终止本次循环
if((grid[xx][yy] == '*'||grid[xx][yy]=='E')&&vis[xx][yy]==0) {
vis[xx][yy]=1; //关键代码 !!
DFS(xx, yy,step+1); //关键代码!!
vis[xx][yy]=0; //关键代码!!回溯!!!
}
}
}
int main()
{
int h,w;
int i, j;
while(scanf("%d%d", &h, &w))
{
if(h == 0 && w == 0) break;
memset(grid, 0, sizeof(grid));
memset(vis, 0, sizeof(vis));
for(i = 1; i <=h; i++) //避免从第0行开始
scanf("%s", grid[i]+1); //避免从第0列开始
for(i = 1; i <=h; i++){ //从第1行开始搜索
for(j = 1; j <=w; j++){ //从第1行开始搜索
if(grid[i][j] == 'S')
{
vis[i][j]=1; //关键代码!!
DFS(i, j,0); //关键代码!!
}
}
}
printf("%d\n",ans);
}
return 0;
}
如果只是找一条路
下面只给出改动之处的代码。要改两处,
(1)在找到‘E’时,不需要min函数取最小值了。直接记录step并返回
(2)回溯的时候不需要将点标记没走过
(3)不会再有第二条路了,因为第一次找到E的时候,把E标记为1已走过了。
(4)虽然顺着一条路找到了E,但整个DFS过程还是会把所有点都走一遍,然后在return回main函数。
void DFS(int x, int y,int step)
{
if(grid[x][y]=='E'){
// ans=min(ans,step);
ans=step;
return;//递归出口--终点
}
for(int i = 0; i<4; i++)
{
int xx = x + dir[i][0];
int yy = y + dir[i][1];
//if(xx<0 || xx>=n || yy<0 || yy>=m) continue; //越界终止本次循环
if((grid[xx][yy] == '*'||grid[xx][yy]=='E')&&vis[xx][yy]==0) {
vis[xx][yy]=1; //关键代码 !!
DFS(xx, yy,step+1); //关键代码!!
//vis[xx][yy]=0; //关键代码!!回溯!!!
}
}
}
实际所有点都走了一遍,顺序是这样的1~9。
2.题目描述
有@表示石油,@(水平、垂直、对角线)连在一起是一个石油田。求有多少个石油田?
注意:
(1)根据题目,有8个方向,dir数组需要改
(2)回溯的时候不需要标记为0,因为一个点只走一次
#include<bits/stdc++.h>
using namespace std;
char grid[101][101];
int vis[101][101];
//大括号内含小括号!!!
int dir[8][2] = { {-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1} };
void DFS(int x, int y)
{
for(int i = 0; i < 8; i++)
{
int xx = x + dir[i][0];
int yy = y + dir[i][1];
//if(xx<0 || xx>=n || yy<0 || yy>=m) continue; //越界终止本次循环
if(grid[xx][yy] == '@'&&vis[xx][yy]==0) {
vis[xx][yy]=1; //关键代码 !!
DFS(xx, yy); //关键代码!!
}
}
}
int main()
{
int h,w;
int i, j; //循环变量
int count; //油田数量计数
while(scanf("%d%d", &h, &w))
{
if(h == 0 && w == 0) break;
count = 0;
memset(grid, 0, sizeof(grid)); //char数组也可以用0初始化
memset(vis, 0, sizeof(vis));
for(i = 1; i <=h; i++) //避免从第0行开始
scanf("%s", grid[i]+1); //避免从第0列开始
for(i = 1; i <=h; i++){ //从第1行开始搜索
for(j = 1; j <=w; j++){ //从第1行开始搜索
if(grid[i][j] == '@'&&vis[i][j]==0)
{
count++;
vis[i][j]=1; //关键代码!!
DFS(i, j); //关键代码!!
} //还有未走过的油田
}
}
printf("%d\n", count);
}
return 0;
}
模板总结一下:
全局变量有:图、记录图、方向
main函数负责:memset初始化,输入,甚至搜索。
DFS函数负责:不需要递归出口?!;然后就是各个方向的找寻。
在main函数和DFS函数中都有关键代码,干两件事,一件是标记已走,一件是继续递归(可看做堆栈,对比着BFS的队列来看)
(3)如果不去掉第0行和第0列的话,需要在dfs里判断是否超出上、下、左、右边界
void DFS(int x, int y)
{
......
for(int i = 0; i < 8; i++)
{
xx = x + dir[i][0];
yy = y + dir[i][1];
if(xx<0 || xx>=n || yy<0 || yy>=m) continue; //越界终止本次循环!!!!
if(grid[xx][yy] == '@'&&vis[xx][yy]==0) {
DFS(xx, yy);
}
}
}
int main()
{
while(scanf("%d%d", &h, &w))
{
......
for(i = 0; i <h; i++) //从第0行开始!!!!!
scanf("%s", grid[i]); //从第0列开始 !!!!!
for(i = 0; i <h; i++){ //从第0行开始 !!!!
for(j = 0; j <w; j++){ //从第0列开始 !!!!
......
}
}
}
如果不另外用一个二位数组标记是否已走过的话,思路会更简单,而且不容易出错。
# include<bits/stdc++.h>
using namespace std;
/*
一个点只走一次,所以DFS和BFS的复杂度都符合要求。
思路:走过的油田改成'*' ,这样就不需要另外一个二维数组记录哪里走过没有了。
*/
int h,w;
char oil[100][100];
int dir[8][2]={{1,0},{0,1},{-1,0},{0,-1},{1,1},{-1,-1},{1,-1},{-1,1}};
int ct=0;
/*作用:将发现的那片油田抹除*/
void dfs(int x,int y){
for(int i=0;i<8;i++){ //8个方向都来一遍
int dx=x+dir[i][0];
int dy=y+dir[i][1];
if(oil[dx][dy]=='@'){
oil[dx][dy]='*';
dfs(dx,dy);
}
}
}
/*作用:寻找是否还有油田*/
int main(){
memset(oil,0,sizeof(oil)); //地图初始化
while(scanf("%d%d",&h,&w)){
ct=0;
if(h==0&&w==0) break;
for(int i=1;i<=h;i++){ //一行一行的输入
scanf("%s",oil[i]+1);
}
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
if(oil[i][j]=='@') { //一旦发现有石油
ct++;
oil[i][j]='*'; //关键代码
dfs(i,j); //关键代码
}
}
}
printf("%d",ct);
}
return 0;
}
4.螺旋数字
给定矩阵的大小,输出下面这样的螺旋数字。
思路:
很明显每一个结点对于方向的选择是有个优先级排序的,先往下,下面没有了再往右,右边没有了,再往上,上面没有了,再往左,左边没有了,又回到最初的往右。
一开始,我想的是设计“下,右,上,左”的优先级,
int dir[4][2] = { {1, 0}, {0, 1}, {-1, 0}, {0, -1} }; //下,右,上,左的顺序
for(int i = 0; i<4; i++)
{
int xx = x + dir[i][0];
int yy = y + dir[i][1];
}
但这样的思路有一个破绽,看上图中走到13时,按照我们的“下,右,上,左”的优先级,13应该往下走,而我们想让他往右走。
所以优先级应该是“下,右,上,左,右”,注意,左和下同时可以走时,左的优先级是大于下的。所以这是个循环优先级!(怎么跑到微机的概念上去了)
从左到下的循环我用了如下代码来实现:
if(grid[x+1][y]==333&&grid[x][y-1]==333){
xx=x;
yy=y-1;
}
翻译过来就是,当左边和下边都可以走时,选择走左边。
下面给出完整代码。
#include<bits/stdc++.h>
using namespace std;
int grid[101][101];
int dir[4][2] = { {1, 0}, {0, 1}, {-1, 0}, {0, -1} }; //下,右,上,左的顺序
int cnt=1;
int h,w;
void DFS(int x, int y)
{
for(int i = 0; i<4; i++)
{
int xx = x + dir[i][0];
int yy = y + dir[i][1];
if(grid[x+1][y]==333&&grid[x][y-1]==333){
xx=x;
yy=y-1;
}
if(grid[xx][yy] == 333) {
cnt++;
grid[xx][yy]=cnt; //关键代码!!
// for(int m=1;m<=h;m++){
// for(int n=1;n<=w;n++){
// cout<<grid[m][n]<<" ";
// }
// cout<<endl;
// }
// cout<<"--------------------"<<endl;
DFS(xx, yy); //关键代码!!
}
}
}
int main()
{
int i, j;
while(scanf("%d%d", &h, &w))
{
if(h == 0 && w == 0) break;
memset(grid, 0, sizeof(grid));
for(i = 1; i <=h; i++) //避免从第0行开始
for(int j=1;j<=w;j++)
grid[i][j]=333;
grid[1][1]=1; //关键代码!!
DFS(1,1); //从grid[1][1]开始 //关键代码!!
for(i = 1; i <=h; i++) {
for(int j=1;j<=w;j++)
printf("%d ",grid[i][j]);
cout<<endl;
}
}
return 0;
}
二、广度优先搜索
(1)广度优先特点在于,从一个点出发层层扩散出去。所以一打眼看到墨水染色这个题的时候就应该想到用BFS。要实现这个层层扩散就要借用队列先进先出的思想。所以BFS一般都用队列来实现。
(2)有模板哦!
1.题目描述
还是上面那个迷宫
模板如下:
#include<bits/stdc++.h>
using namespace std;
struct node{
int x;
int y;
int step;
};
const int MAX_N = 100+5;
char grid[MAX_N][MAX_N];
int vis[MAX_N][MAX_N];
int dir[4][2]={{0,1},{0,-1},{-1,0},{1,0}}; //上、下、左、右四个方向
int ans = -1;
int BFS(int sx,int sy){
//声明队列
queue<node> q;
//起点入队
q.push(node{sx,sy,0});
/*--------BFS---------*/
while(!q.empty()){
int size = q.size();
for(int i=1; i<=size; i++){
node now = q.front();
q.pop();
if(grid[now.x][now.y]=='E'){ //找到终点了!
ans = now.step;
break;
}
for(int j=0; j<4; j++){
int nx = now.x+dir[j][0];
int ny = now.y+dir[j][1];
if((grid[nx][ny]=='*'||grid[nx][ny]=='E')&&vis[nx][ny]==0){
q.push({nx,ny,now.step+1}); //关键代码!!!
vis[nx][ny]=1; //关键代码 !!!
}
}
}
}
/*-----结果输出----*/
printf("至少需要%d步\n", ans);
}
int main(){
int h,w;
int i,j;
while(scanf("%d%d",&h,&w)){
if(h==0&&w==0) break;
memset(grid, 0 ,sizeof(grid));
memset(vis, 0, sizeof(vis));
/*-----绘制地图-----*/
for(i=1; i<=h; i++){ //避免从第0行开始
scanf("%s",grid[i]+1); //避免从第0列开始
}
/*-------Action!----*/
for(i=1; i<=h; i++){
for(j=1; j<=w; j++){
if(grid[i][j]=='S'){
vis[i][j]=1; //关键代码!!!
BFS(i,j); //关键代码(不太算!!!
}
}
}
}
return 0;
}
模板总结:
全局变量:多了一个struct node
main函数:一样
BFS函数:创建队列;起点入队;标记起点已走过;while(!q.empty()),先出队,判断出队的这个是最终结果吗?(对应于DFS的递归出口),再各个方向入队
关键代码也是干两件事
2.题目描述
输入一个m*n的矩阵,矩阵中的元素包含0、1、2(其中0表示什么都没有,1表示白纸,2表示墨水),每秒墨水会向四周(上下左右)进行染色,被染色的纸也会对周围的纸进行染色。试问,对于输入的矩阵,多久才能完全染色?如果最终不能完全染色,输出-1.
解析:自己手动进行染色,并模拟队列,会发现如果完全染色,队列左后一个元素所带的时间step就是总用时
# include<bits/stdc++.h>
using namespace std;
/*不用标记走没走过,因为染过了的就是2*/
char grid[100][100];
int h, w;
int dir[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
struct node{
int x;
int y;
int step;
};
int BFS(){
int ans=0;
queue<node> q; //创建队列
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
if(grid[i][j]=='2')
q.push({i,j,0}); //把一开始就是绿色的入队
}
}
while(!q.empty()){
int size=q.size(); //当前队列有多少个
for(int i=1;i<=size;i++){
node now=q.front();
q.pop();
ans=now.step;
for(int k=0;k<4;k++){
int x=now.x+dir[k][0];
int y=now.y+dir[k][1];
if(grid[x][y]=='1'){
grid[x][y]='2'; //关键代码!!染色
q.push({x,y,now.step+1}); //关键代码 !!
}
}
}
}
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
if(grid[i][j]=='1')
ans=-1; //未完全染色
}
}
return ans;
}
int main(){
/*-------初始化-------*/
while(scanf("%d%d",&h,&w)){
if(h==0&&w==0) break;
memset(grid,0,sizeof(grid));
for(int i=1;i<=h;i++){
scanf("%s",grid[i]+1);
}
/*染色*/
int result=BFS();
printf("%d\n",result);
}
return 0;
}
三、例题们
1.被围绕的区域
解析:
思路是从四个边界出发,找到O后开始深度搜索,和这个O连着的所有O都属于在边界上,先用#标记。四个边界搜索完后,图中剩下的O是真正不挨着边界的O,把他们变成X。最后把#变成O。
(1)用DFS求解:
注意:
①由于LeetCode上给定了图board,行和列都是从0开始的,所以必须进行越界处理
class Solution {
public:
void solve(vector<vector<char>>& board) {
int h=board.size();
int w=board[0].size();
/*---沿着4个边界扫描,看有没有“O”了?----*/
for(int k=0;k<h;k++){
if(board[k][0]=='O')
dfs(k,0,board);
if(board[k][w-1]=='O')
dfs(k,w-1,board);
}
for(int k=0;k<w;k++){
if(board[0][k]=='O')
dfs(0,k,board);
if(board[h-1][k]=='O')
dfs(h-1,k,board);
}
/*---------0先变X------------*/
for(int i=0;i<h;i++){
for(int j=0;j<w;j++){
if(board[i][j]=='O')
board[i][j]='X';
}
}
/*-------#再变回O-----------*/
for(int i=0;i<h;i++){
for(int j=0;j<w;j++){
if(board[i][j]=='#')
board[i][j]='O';
}
}
}
void dfs(int x,int y, vector<vector<char>>& board){
int dir[4][2]={{1,0},{0,1},{-1,0},{0,-1}};
board[x][y]='#'; //走过的用#标记,关键代码!!!!!
for(int i=0;i<4;i++){
int dx=x+dir[i][0];
int dy=y+dir[i][1];
if(dx<0||dx>=board.size()||dy<0||dy>=board[0].size()) //越界处理
continue;
if(board[dx][dy]=='O'){
dfs(dx,dy,board);
}
}
}
};
DevC++版本:
#include<bits/stdc++.h>
using namespace std;
char grid[100][100];
int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
void DFS(int x,int y){
for(int i=0;i<4;i++){
int xx=x+dir[i][0];
int yy=y+dir[i][1];
if(grid[xx][yy]=='O'){
grid[xx][yy]='#';
DFS(xx,yy);
}
}
}
int main(){
int h,w;
cin>>h>>w;
memset(grid,0,sizeof(grid)); //初始化
for(int i=1;i<=h;i++){ //输入
scanf("%s",grid[i]+1);
}
/*-----寻找边界上的O点,改为#-----*/
for(int i=1;i<=h;i++){ //第一列
if(grid[i][1]=='O'){
grid[i][1]='#';
DFS(i,1);
}
}
for(int i=1;i<=h;i++){ //最后一列
if(grid[i][w]=='O'){
grid[i][w]='#';
DFS(i,w);
}
}
for(int i=1;i<=w;i++){ //第一行
if(grid[1][i]=='O'){
grid[1][i]='#';
DFS(1,i);
}
}
for(int i=1;i<=w;i++){ //最后一行
if(grid[h][i]=='O'){
grid[h][i]='#';
DFS(h,i);
}
}
/*------寻找内部的O点,改为X-----*/
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
if(grid[i][j]=='O')
grid[i][j]='X';
}
}
/*-----把边界的#改回O点------*/
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
if(grid[i][j]=='#')
grid[i][j]='O';
}
}
/*--------输出-------*/
for(int i=1;i<=h;i++){
for(int j=1;j<=w;j++){
cout<<grid[i][j]<<' ';
}
cout<<endl;
}
return 0;
}
2.扫雷游戏
这里用的DFS,但报出了栈溢出的错误。思路是正确的,以后再回来看看哪里错了吧
class Solution {
public:
/*8个方向*/
vector<vector<char>> updateBoard(vector<vector<char>>& board, vector<int>& click) {
int dir[8][2]={{1,0},{0,1},{-1,0},{0,-1},{1,1},{-1,-1},{1,-1},{-1,1}};
if(board[click[0]][click[1]]=='M'){
board[click[0]][click[1]]='X';
return board;
}
//if(board[click[0]][click[1]]=='E')
int count=0;
for(int i=0;i<8;i++){
int x=click[0]+dir[i][0];
int y=click[1]+dir[i][1];
if(x<0||x>=board.size()||y<0||y>=board[0].size())
continue;
if(board[x][y]=='M'){
count++;
}
}
if(count>0){//与地雷相邻呀
board[click[0]][click[1]]=count+48; //????
return board;
}else{
board[click[0]][click[1]]='B';
for(int i=0;i<8;i++){
vector<int> c={0,0};
c[0]=click[0]+dir[i][0];
c[1]=click[1]+dir[i][1];
if(c[0]<0||c[0]>=board.size()||c[1]<0||c[1]>=board[0].size())
continue;
updateBoard(board,c);
}
}
return board;
}
};