DFS深度优先搜索算法初探

前言

今天我继续和诸君分享我的算法心得。(dalao请轻喷)

DFS(深度优先搜索算法)可以说是最具代表性的初级算法之一。其旨在树的结构下进行试探性遍历,从而更新节点信息。个人认为,要想学好DFS,首先对代码的模拟能力一定要有一定基础,即可以根据实际问题编写出合适的代码,把自己的意图较为精确地传达给计算机。

实际上,DFS是基于树和图的数据结构上的算法,但我在最初学习DFS时并没有学过图论(所以说到底我是个蒟蒻),因此很多理解都是非官方的。本篇文章分享对象主要是启蒙一下还未经历过树图毒打的萌新们,希望读者可以借此初步入门DFS这个大坑。

首先,我们需要了解以下预备芝士:

  1. DFS是一种暴力解法,裸深搜在问题有一定复杂度限制的时候往往难以满足要求。即使我们可以用剪枝、预处理等方法尝试优化代码的执行,一般来说,复杂度不过关的问题你再怎么优化也AC不了(也许你把读优输优都做到极致还是会有一丝丝机会?)
  2. DFS一般是以递归方式呈现,我们知道,递归的质量一般来说比递推要差,这也就是DFS复杂度高的原因。

我们可以通过简单的DFS模板来分析算法的原理:

void DFS(需要传递的信息[, 搜索深度])
{
    if(到达边界||满足要求)
    {
        进行预期操作
        return;
    }
    //else
    for(每一种可能)
    {
        标记, 更新信息
        DFS(传递新信息[, 搜索深度 + 1]);
        把本层信息恢复成更新前的状态
    }    
}

可以看到,深度有限搜索的核心思想就是一条路走到底,不撞南墙不回头。首先,对于当前层次的节点,只要可以,DFS会尽可能找到更深的一层进行探索;否则,就回到上一层节点进行此操作。直到整个图的所有节点都被访问过,即问题的所有可能性都被我们尝试过后,算法结束。

探索

没有理解的童鞋可以通过下面一段故事情景,与小A童鞋一起尝试领悟DFS:

  • 小A今天去游乐园玩。游乐园里面的游玩设施有很多,但是由于年代较久远,任意两个游玩设施之间只有一条道路。小A看着地图一时也决定不了怎么走,于是他决定先沿着一条路走走。 
  • 沿着小A最初选择的道路,不一会儿,小A就游玩了摩天轮、过山车、旋转木马。可是好景不长,慢慢的,因为小A走得比较深入,他发现刚刚玩的旋转木马周围没有任何其他的游玩项目了。
  • “只能往回走了。”小A想着,于是回到了刚才玩过的过山车旁,这时候他发现还有另一条路,通往的是丛林探险。于是小A高兴地走去。
  • 等到实在找不到可以玩的地方,再往回走吧。小A这样想道。
  • 不知不觉,小A把整个游乐园都逛了一遍。

小A今天在游乐场游玩的顺序如下: fd95ccdcbbea47c8843b7d48ae187bb8.png

 回到家,小A总结了一下今天的游玩历程,发现自己严格贯彻沿着一条路走,直到没有路了就回退,重复操作直到遍历完整个地图这一原则,这不就是刚刚学的深度优先搜索算法吗?

小A轻松写出了以下伪代码,用来表示今天他游玩游乐场的规则:

void 游玩游乐场(游玩了xx项目)
{
    if(没路可走了)
    {
        回上一个玩的项目周围看看吧
    }
    //周围还有可以游玩的项目
    for(选择其中最近的一个)
    {
        这个项目是丛林探险
        游玩游乐场(游玩丛林探险)
        从丛林探险走回来
    }
}

嗯嗯,确实可以按照DFS的模板写出来,那就是你没跑了!

这样想的话,原来DFS也没那么难。于是小A试着用这样的体会完成老师布置的编程作业。

cd88c4ae519a412d8e68b90ae9729ad1.png

怎么把数字具体化成实际问题呢?小A试着这样翻译:假设今天去游乐园玩,但是自己体力有限,最多只有N个体力值。游乐园的项目有无限多个,每个游玩项目的体力值为1~N之间的一个数字,且任意两个游玩项目之间都是联通的。由于自己比较懒,所以在玩过轻松的项目后就不想再尝试困难项目了,也就是说,当自己游玩了体力值为3的项目,就不会想去玩体力值超过3的项目,而只愿意去体验体力值小于等于3的项目了。

最后求问:小A有哪些方案可以把体力刚好消耗完?请输出具体方案。

小A大手一挥,写出了如下伪代码:

void 游玩游乐园(还有N个体力, 已经游玩的方案)
{
    if(体力消耗完了)
    {
        输出具体的游玩方案
    }
    //else
    for(找一找是否有满足体力要求的游戏可供选择)
    {
        选择这个游戏加入到游玩方案中
        游玩游乐园(还有N-x个体力, 新的游玩方案)
        不玩这个游戏, 把游玩方案复原
    }
}

将伪代码进一步翻译成实际代码:

int n;
int a[100];
//四个参数作用:存储具体方案 保存当前剩余体力 方案长度 上一次游玩项目消耗的体力
void dfs(int a[100], int n, int len, int last)
{
    //如果体力消耗完了 行程结束 输出方案
	if (n <= 0)
	{
		if (n < 0)return;
		for (int i = 0; i < len - 1; ++i)
		{
			cout << a[i] << "+";
		}
		cout << a[len - 1];
		cout << endl;
	}
    //如果体力还有剩余 以上次消耗的体力作为基准找项目
	for (int i = last; i; --i)
	{
        //假设小A游玩了i项目 消耗了i体力
		a[len] = i;
		dfs(a, n - i, len + 1, i);
        //由于数组元素值的覆盖 无需撤销操作
	}
}
int main()
{
	cin >> n;
    //小A开始形成 方案数组为空 体力为n 方案长度为0 可以选择的最大项目为n
	dfs(a, n, 0, n);
	return 0;
}

问题就这么完美地解决了!小A趁热打铁,找出了洛谷上的几道简单的DFS题目练练手。

洛谷P1036 选数

已知 n 个整数 1,2,⋯ ,x1​,x2​,⋯,xn​,以及 11 个整数 k(k<n)。从 n 个整数中任选 k 个整数相加,可分别得到一系列的和。例如当 n=4,k=3,44 个整数分别为 3,7,12,193,7,12,19 时,可得全部的组合与它们的和为:

3+7+12=22

3+7+19=29

7+12+19=38

3+12+19=34

现在,要求你计算出和为素数共有多少种。例如上例,只有一种的和为素数:93+7+19=29。

这道题和小A刚才老师布置的作业做法很相似,我们只需要在输入的 n 个整数中进行搜索 k 个数字,并检查最终得到的结果是否是素数即可。

具体代码如下:

int n, k;
int a[30];
long long ans;
//试除法 判断一个数字是否是素数
bool is_prime(int n)
{
	for (int i = 2; i <= sqrt(n); i++)
	{
		if (n % i == 0)return 0;
	}
	return 1;
}
void dfs(int now, int sum, int st)
{
	if (now == k)
	{
		if (is_prime(sum))++ans;
		return;
	}
	for (int i = st; i < n; i++)
	{
		dfs(now + 1, sum + a[i], i + 1);
	}
	return;
}

int main()
{
	cin >> n >> k;
	for (int i = 0; i < n; i++)
        cin >> a[i];
	dfs(0, 0, 0);
	cout << ans;
	return 0;
}

当然,我们也可以对算法做很多优化,比如在本题中要判断很多数是否是素数,我们可以提前用素数筛法预处理,在数据比较大时,这可以省下一些时间。当然,本题只需要最朴素的方法足矣。

洛谷P1596 Lake Counting S

题目描述:由于近期的降雨,雨水汇集在农民约翰的田地不同的地方。我们用一个 N×M (1≤N≤100, 1≤M≤100) 的网格图表示。每个网格中有水(W) 或是旱地(.)。一个网格与其周围的八个网格相连,而一组相连的网格视为一个水坑。约翰想弄清楚他的田地已经形成了多少水坑。给出约翰田地的示意图,确定当中有多少水坑。

根据我们之前的分析,要想把DFS模板套入问题,首先要分辨出以下三个条件:

  1. 搜索边界,即搜索到满足什么条件时返回上一个节点。
  2. 遍历方法,即如何选择下一个搜索节点。
  3. 保存答案的方式。你可以选择全局变量、参数或者返回值的方式保存搜索得到的信息。

首先我们分析本题的要点,可以知道,任意两个 'W' 网格如果联通,只会被算作一个水坑,因此我们可以对任一个 'W' 网格进行连通性搜索,把和他联通的所有 'W' 都视为 '.' 网格,这样就可以保证不会重复计算。我们可以用二维数组模拟约翰的田地,同时对每一个数组元素为 'W' 的进行一次搜索。在搜索过程中,前往周围元素也为 'W' 的网格,继续搜索的同时将其元素更改为 '.' ,这样就能保证在后续过程中不会再次搜索到这个节点,实现了一个水坑搜索一次,那么最后的搜索总次数就是答案了。

具体代码如下:

int n, m;
//在本题中,我直接使用了数组来保存方向向量,读者可以理解并学习使用
int dx[] = { 1,0,-1,0,1,1,-1,-1 };
int dy[] = { 0,1,0,-1,1,-1,1,-1 };
int num;
//模拟田地的数组
char pool[102][102];
//边界处理 防止搜索越界造成无穷递归
bool check(int x, int y)
{
	return x >= 0 && y >= 0 && x < n&& y < m;
}
//代码的内容和刚才的分析一样
void dfs(int x, int y)
{
	pool[x][y] = '.';
	for (int i = 0; i < 8; i++)
	{
		int nx = x + dx[i];
		int ny = y + dy[i];
		if (check(nx, ny) && pool[nx][ny] == 'W')
		{
			dfs(nx, ny);
		}
	}
}

int main()
{
	cin >> n >> m;
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
		{
			cin >> pool[i][j];
		}
	}
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
			if (pool[i][j] == 'W')
			{
                //每进行一次搜索就计数一次
				num++;
				dfs(i, j);
			}
	cout << num;
	return 0;
}

接下来的几道比较难的题目就比较让小A摸不着头脑了,也许他还需要更多的学习和练习。于是他把题目分享给你,也许聪明的你可以尝试将它们解决?

洛谷P4799 世界冰球锦标赛

洛谷P1219 八皇后

洛谷P4017 最大食物链计数

后记

希望读者可以通过我今天的分享初步了解到DFS的原理,并能解决相关的一些简单问题。在后续过程中,我们需要广泛学习并反复练习才能熟练掌握。作为基础算法之一,DFS更多的是和其他算法结合起来形成一个复杂问题,这就需要读者自行探索了。学习编程的所有人,我们共勉。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

没啥基础的小白

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值