DFS深度优先搜索
写在前面
古早笔记,从csdn移植过来
是大一算法课的结课作业,写的非常浅显,还请见谅,仅作为自己的第一篇博客纪念。
DFS(深度优先搜索)
思想
用走迷宫举例,我们在走迷宫时遇到岔道就固定沿着一个方向走,如果发现没有路,则退回到上一次选择岔道的地方,改为另一条路,按照同样的法则走,这个方法执行的前提是要记住每次分叉时走过哪些岔路,不要重复走。
这是最简单的走法,简单来讲就是穷举暴力,将所有的路都试一遍。因此也非常费时,时间复杂度比较高,当然DFS也有很多优化技巧,比如剪枝。
DFS有递归和非递归两种实现方法,DFS就是回溯法,首先想到递归是很自然的。
例:Counting Sheep http://acm.hdu.edu.cn/showproblem.php?pid=2952
Description:
A while ago I had trouble sleeping. I used to lie awake, staring at the ceiling, for hours and hours. Then one day my grandmother suggested I tried counting sheep after I'd gone to bed. As always when my grandmother suggests things, I decided to try it out. The only problem was, there were no sheep around to be counted when I went to bed.
Creative as I am, that wasn't going to stop me. I sat down and wrote a computer program that made a grid of characters, where # represents a sheep, while . is grass (or whatever you like, just not sheep). To make the counting a little more interesting, I also decided I wanted to count flocks of sheep instead of single sheep. Two sheep are in the same flock if they share a common side (up, down, right or left). Also, if sheep A is in the same flock as sheep B, and sheep B is in the same flock as sheep C, then sheeps A and C are in the same flock.
Now, I've got a new problem. Though counting these sheep actually helps me fall asleep, I find that it is extremely boring. To solve this, I've decided I need another computer program that does the counting for me. Then I'll be able to just start both these programs before I go to bed, and I'll sleep tight until the morning without any disturbances. I need you to write this program for me.
Input
The first line of input contains a single number T, the number of test cases to follow.
Each test case begins with a line containing two numbers, H and W, the height and width of the sheep grid. Then follows H lines, each containing W characters (either # or .), describing that part of the grid.
Output
For each test case, output a line containing a single number, the amount of sheep flock son that grid according to the rules stated in the problem description.
Notes and Constraints
0 < T <= 100
0 < H,W <= 100
Sample Input
2
4 4
#.#.
.#.#
#.##
.#.#
3 5
###.#
..#..
#.###
Sample Output
6
3
题解
简单的DFS求连通块,每个结点都有上下左右四个方向可以进行搜索,我们只需要按一个顺序(比如向上->向下->向左->向右)进行递归,并且用一个标记数组idx[]记录当前结点是否走过。如果走过该点或者走到了边界外,则返回。
注意,退出条件(图的边界或已走过该点),递归必须有退出条件,否则会进入死循环。
代码(递归实现)
#include<cstdio>
#include<cstring>
#define maxn 105
char pic[maxn][maxn];
int t, idx[maxn][maxn];
int h, w, cnt;
void DFS(int r, int c, int id)
{
if (r < 0 || r >= h || c < 0 || c >= w)
return;
if (idx[r][c] > 0 || pic[r][c] != '#')
return;
idx[r][c] = id;
DFS(r - 1, c, id);
DFS(r + 1, c, id);
DFS(r, c - 1, id);
DFS(r, c + 1, id);
}
int main()
{
scanf("%d", &t);
while (t--)
{
scanf("%d%d", &h, &w);
for (int i = 0; i < h; i++)
scanf("%s", pic[i]);
memset(idx, 0, sizeof(idx));
cnt = 0;
for (int i = 0; i < h; i++)
for (int j = 0; j < w; j++)
if (idx[i][j] == 0 && pic[i][j] == '#')
DFS(i, j, ++cnt);
printf("%d\n", cnt);
}
return 0;
}
递归虽然可以将问题简化,但是递归是非常耗时的,一旦数据非常大,运算时间也是可想而知的,因此在某些时候我们把递归转化为非递归。学习了数据结构之后就可以了解到,栈具有后进先出的特点并且可以保存遍历过的结点,递归实际上是使用了系统调用栈,而如果不用系统调用栈,我们依然需要一个堆栈来保存状态,因此我们可以自己维护一个栈来保存所有未被访问的结点,将所有“符合条件”的“邻居”压入栈中备用,然后从周围结点继续深入,每个结点在访问之后被弹出。
代码(非递归实现)
#include<cstdio>
#include<cstring>
#include<stack>
#define maxn 255
using namespace std;
char pic[maxn][maxn];
int idx[maxn][maxn], t;
int h, w, cnt;
int dx[4] = { 1,-1,0,0 };
int dy[4] = { 0,0,1,-1 };
typedef struct
{
int x;
int y;
}Point;
void DFS_stack(Point start, int& id)
{
stack<Point>s;
s.push(start);
idx[start.x][start.y] = 1;
while (!s.empty())
{
Point now = s.top();
s.pop();
idx[now.x][now.y] = 1;
for (int i = 0; i < 4; i++)
{
if (pic[now.x + dx[i]][now.y + dy[i]] == '#' && !idx[now.x + dx[i]][now.y + dy[i]])
{
Point t;
t.x = now.x + dx[i];
t.y = now.y + dy[i];
s.push(t);
}
}
}
}
int main()
{
Point start;
int t;
scanf("%d", &t);
while (t--)
{
scanf("%d%d", &h, &w);
memset(pic, 0, sizeof(pic));
for (int i = 0; i < h; i++)
scanf("%s", pic[i]);
memset(idx, 0, sizeof(idx));
cnt = 0;
for (int i = 0; i < h; i++)
{
for (int j = 0; j < w; j++)
{
if (idx[i][j] == 0 && pic[i][j] == '#')
{
start.x = i;
start.y = j;
DFS_stack(start, ++cnt);
}
}
}
printf("%d\n", cnt);
}
return 0;
}
优化方法
当然需要使用非递归写法的情况还是比较少的(我个人觉得),由于DFS就是暴力枚举,所以他的时间复杂度相应的也会较高O(n^x),而空间复杂度会相对较小。为了降低DFS的时间复杂度,也有很多的优化方法,最简单的就是剪枝。
常用的剪枝有:可行性剪枝、最优性剪枝、记忆化搜索、搜索顺序剪枝。
可行性剪枝
这是最简单的一种剪枝了,就是把不符合题意的部分剪掉,如果当前条件不合法就不再继续搜索,直接return。
最优性剪枝
如果当前状态得到的答案必定比之前的答案大,那么就没有必要再继续搜索,可以剪掉。
我们利用某个函数估计出此时条件下答案的‘下界’,将它与已经推出的答案相比,如果不比当前答案小,就可以剪掉。
一般实现:在搜索取和最大值时,如果后面的全部取最大仍然不比当前答案大就可以返回。
在搜和最小时同理,可以预处理后缀最大/最小和进行快速查询。
最优性剪枝的重点在于估计‘下界’,但注意,我们把‘下界’估计得越大,减去的状态就越多,如果估算得过大,超过了它的真的代价,就会把真正的解剪掉。
if(check2(x)>=ans)
return ...;
记忆化搜索
记录每个状态的搜索结果,当重复遍历到这个状态的时候直接使用之前记录的结果即可。我们在对图进行DFS的时候,用一个标记数组idx[]记录一个结点是否被访问过,其实就是一种记忆化。
搜索顺序剪枝
搜索顺序剪枝是一种类似于贪心思想的优化方法(个人认为),在一些迷宫、网络类题目中,搜索顺序的选择十分重要。
在上面的例题中,我们也会优先选择从羊(#)的地方开始搜索,而不是整个图里面随意乱搜,从已知信息多的地方开始搜,自然更好。
比如我们要从左上向右下搜索,很自然能想到遍历邻接点顺序右下左上更好一些。
再比如在一些题目中,先搜某个值大的,再搜某个值小的,速度自然会更快。
最后的碎碎念
其实搜索应该是DFS和BFS一起写比较好,但是第一次写博客有点无从下手最后只写了DFS,写得不好还请多多包涵orz
一直以来我都是在不断地接受知识,嘴比较笨也很少给同学讲东西,但是写博客的过程却感觉到自己对算法的思路逐渐变得有条理,突然感觉到了输出学习法的好处Σ(☉▽☉"a憋了一晚上最后能憋出点东西还是挺有成就感的。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!