在之前https://blog.csdn.net/K08e_824?spm=1000.2115.3001.5343中,大致讲解了dfs搜索,
这里还可以再来几道习题
比如:https://www.luogu.com.cn/problem/P1605
这个迷宫其实是比较经典的dfs,(如果将其再包装一下,要求最短路径,就成了bfs,这都是后话...)
可以设想一下,如下的状态空间:
红色代表起点,绿色代表终点,从起点往下搜索,搜索方式是上下左右,上下左右...直到遇到终点,其实这是比较靠谱的搜索方式。
当然,这一题里面还有障碍物等等附加条件,这都是后话
对于一个题,我们确定了它是dfs,就需要确定以下问题:
1、搜索方式是什么?
这一题很明显就是上下左右;所以可以定义两个数组
const int dx[]={1,-1,0,0};
const int dy[]={0,0,-1,1};
用
for(int i = 0;i < 4;++i)
遍历dx和dy,得到的dx[i] 和 dy[i]恰好可以组成上/下/左/右,并且不重复
2、递归出口是什么?
如果对于单独的迷宫问题,递归出口就是
if(map[x][y] == 终点)
路径数++;
return;
但这一题还有障碍物,所以需要再加一个
if(map[x][y] == 障碍)
return;
所以这一题大致就解决了;
#include<bits/stdc++.h>
using namespace std;
const int dx[] = {1,-1,0,0};
const int dy[] = {0,0,1,-1};
const int N = 5;//题目中有 1<= N , M <= 5
bool vis[N][N];//标记数组
int n,m,t;//设置为全局变量,方便函数调用,减少参数
bool in(const int& x,const int& y)
{
return x >= 1 && y >= 1 && x <= n && y <= m;
}
void dfs(const vector<vector<int>>& maze,const int& x,const int& y,const int& fx,const int& fy,int& count)
{
vis[x][y] = true;
//遍历到的地方先打上标记
if(maze[x][y] == 1)//遇到障碍就回溯
return;
if(x== fx && y == fy && maze[x][y] != 1)//到达终点,路径数++,回溯,但不能直接退出,因为还不能确定是不是所有路径都搜完了
{
count++;
vis[x][y] = false;//必须要有这一步,不然找到一条路径终点一直是true就没法搜了
return;
}
for(int i = 0;i < 4;++i)
{
int tx = x + dx[i];
int ty = y + dy[i];
//实现上下左右
if(vis[tx][ty] != true && in(tx,ty))
{
dfs(maze,tx,ty,fx,fy,count);
}
}
vis[x][y] = false;//回溯
}
int main()
{
cin>>n>>m>>t;
//n行 m列 t个障碍
int sx,sy,fx,fy;
cin>>sx>>sy>>fx>>fy;
//起点终点坐标
vector<vector<int>> maze(n + 1,vector<int>(m + 1));//地图,且默认初始化为0
//这里根据题意可知,都是从第一行开始计数而不是第零行,所以可以多开一行;
for(int i = 0;i < t;++i)
{
int x,y;
cin>>x>>y;
//障碍坐标
maze[x][y] = 1;//将障碍处标记为1
}
int count = 0;
dfs(maze,sx,sy,fx,fy,count);
cout<<count;
return 0;
}
切记不要忘记回溯操作,,初学的时候因为忘记回数操作导致很多题都没做对!
二、八皇后
这是一道非常经典的dfs,我认为这题写十遍都不为过;
https://www.luogu.com.cn/problem/P1219
这题其实难点比较多,我们之前见到的dfs题大多设置一个 判断是否遍历过的数组 都是看这一题有多少状态空间来设置的,但是这一题不一样,
之前是到哪一个点,那么那一个点就打个标记,但是这个涉及到很多点,同一行的,同一列的,同一对角线的...
这就需要多个数组来判断,如何更简单的设置这些数组也是一门学问。比如我可以再设置一个vis[N][N],然后将同一行同一列,同一对角线都打上标记,这确实是可行的,但是会有很多冗余量;
其实这一题的状态空间大概如下:
这样一个长宽都为n的正方形,
为了避免打标记的数组成为二维数组,我们可以将列/行看成一个单独变量,具体的意思就是我们可以从上往下依次放,这样是十分高效而且不会出错的,
比如我是用行做单独变量,我们在一行放完之后,立马到下一行寻找到可以放的地方,
因为一行中只能出现一个,所以我放完之后立马去下一行寻找是没有问题的,
这就解决了二维数组的产生的问题;
那么对角线该如何解决??
从这一条 走向为右上的对角线 来说(下标从0开始)
我们可以看到对角线上的点 横纵坐标相加最大为 2(n-1),最小为0,而且是在[0,2n-2]这个区间中,每个值都会出现的,所以我们可以将这个点的横纵坐标的和 作为 判断有没有在对角线上出现过的标准
所以我们可以开一个大小为 2n的数组,每次搜索到一个点都将这个点的横纵坐标加起来,从将数组中下标为这个值的点 置为true 代表被遍历过了 这条对角线不可用了;
那再以这条 走向为右下的 对角线为例:
这些对角线上的点都有一个很明显的特征:它们的差值为固定值
在一个长宽都为n的正方形中,x坐标的取值范围是[0,n-1],y也是[0,n-1];
所以 x-y的取值范围是[1-n,n-1];
那这个怎么用数组实现呢?
其实仔细看它的 最大值减去最小值 还是 2(n-1),我们不妨再开一个大小为2n的数组
然后我们把每次 横纵坐标的差值 + n 的结果 当作是否被搜索过的标准;
这里觉得绕是很正常的,但其实也很好理解,既然每条对角线上的点 x-y都是相同的 而且 独一无二的 那么将这个x-y加上n之后还是相同的而且独一无二的,这些特性都没有改变过,所以这是一个比较巧妙的技巧;
那么,题前准备做好了,接下来应该就是那两个问题了;
1、搜索策略是什么?
很明显,我在上面说过,这么搜索是按照行数从上往下,一行一行搜,搜到一个可以放的,放完立刻下一行;
2、递归出口是什么?
我们既然决定了搜索策略是 从第0行 一行一行的来,那么如果n-1行我们已经放过了,再次进入递归,这个时候 行数应该是 == n的 所以这个时候直接return
那么这一题也就变得十分容易:
#include<bits/stdc++.h>
using namespace std;
int n;
void dfs(const vector<vector<int>>& maze,vector<bool>& vis1,vector<bool>& vis2,vector<bool>& vis3,const int& x,int& count,vector<int>& res,int& num)
{
if(x == n)
{
if(num)
{
--num;
for(int i = 0;i < res.size();++i)
{
cout<<res[i] <<" ";
}
cout<<endl;
}
count++;
return;
}//如果 x可以进行到 n-1行 下一次递归进来就是 x== n 那么代表 n-1行已经放进元素了,这就是一种成功的策略
else
{
for(int y = 0;y < n;++y)
{
if(vis1[y] != true && vis2[x + y] != true && vis3[x - y + n] != true)
{
res[x] = y + 1;
vis1[y] = true;
vis2[x + y] = true;
vis3[x - y + n] = true;
//如果这些都符合条件,代表未被搜索过,我们现在给他打上标记,然后往下一行搜索
dfs(maze,vis1,vis2,vis3,x + 1,count,res,num);
//回溯 千万不能忘记
vis1[y] = false;
vis2[x + y] = false;
vis3[x - y + n] = false;
}
}
}
}
int main()
{
cin >> n;
vector<int> res(n);//答案数组
vector<vector<int>> maze(n,vector<int>(n));//地图
vector<bool> vis1(n);//标记列
vector<bool> vis2(2 * n);//标记对角线1
vector<bool> vis3(2 * n);//标记对角线2
int count = 0;
int num = 3;
//输出前三个答案
//这里num和count都必须传 引用,因为递归中如果传值,会一直重置值;
dfs(maze,vis1,vis2,vis3,0,count,res,num);
cout<<count;
return 0;
}
希望这两个题对大家有所帮助!敬礼~