算法初探系列2 - 深度优先搜索之计算思维

这篇博客探讨了计算思维的概念,通过两个实例展示了如何将其应用于图论问题和迷宫问题的解决。首先,通过逻辑推理题介绍了布尔代数在解决实际问题中的应用。接着,详细阐述了如何使用DFS解决饲料分配问题,展示了递归和回溯的方法。最后,以八皇后问题为例,解释了如何利用DFS避免暴力枚举,提高算法效率。此外,还提及了过河卒问题和幻想迷宫问题,强调了方向数组在路径规划中的作用。
摘要由CSDN通过智能技术生成

最近有点忙,没来得及写博客,让大家久等啦。
上节课蒟蒻君给大家讲了dfs如何解决枚举类型的题,这节课咱们将会讲到它的另一种用途——图论上的用途(针对图和树的算法)。
大家看到标题肯定会想:什么叫计算思维?
计算思维就是将不好表示的东西用数来表示,比如以下几道题:
铺垫1

  • 题目

有四位同学其中对一位多次扶老奶奶过马路 (老奶奶们很善良,不会碰瓷) ,不留名,表扬信下来后,老师问是哪位同学做的好事。

  • A说:不是我。
  • B说:是C。
  • C说:是D。
  • D说:他胡说。
    经过老奶奶们的确认,知道了三个同学中有一个说的是假话,其余都是真话。现在请你根据以上信息,写出程序找到做好事的人。
  • 分析

我们如果不从数学对角度去考虑 (因为数学的太简单了 ) ,要完成本任务,我们首先要将这四个人说的自然语言改变成计算机能看懂的可计算的式子
在本题里,这个式子用的就是所谓的“布尔代数”。
我们先假设每个人是做好事的那位同学,并且带入,判断是否矛盾即可。

  • 关系表达式

先假设做好事的人为thisman。

  • 代码
#include <bits/stdc++.h>
using namespace std;
int main() {
	for (int k = 0; k < 4; ++k) {
		char thisman = 'A' + k;
		// 如果3句话为真,则输出当前可能性所假定的人为做好事者
		if ((thisman != 'A') + (thisman == 'C') + (thisman == 'D') + (thisman != 'D') == 3) {
			cout << thisman << "做了好事\n";
			return 0;
		}
		
	} 
	cout << "无人做了好事\n"; 
	return 0;
}

铺垫2

  • 题目
    现在农场里有编号为0,1,2,3,4的五袋饲料,和A、B、C、D、E五只小猪猪。请你帮助农场主写一个程序,输出使得所有猪都能吃到喜欢的饲料的方案。
    假设五只猪喜欢吃的饲料如下表:
  • 解题思路
    step1:吃食兴趣用一个二维数组描述。
const int like[5][5] = {{0, 0, 1, 1, 0},
				      	{1, 1, 0, 0, 1},
				        {0, 1, 1, 0, 1},
				        {0, 0, 0, 1, 0},
				        {0, 1, 0, 0, 1}};

step2:饲料状态用一个一维数组描述。

int assigned[5];

数组元素存储的是分配到下标对应饲料的小猪猪编号。若assigned[book_id] == -1 则表示book_id这袋饲料没有分配。
注意:数组下标是的编号。
开始,所有饲料都未分配,我们要做出以下的预处理:

memset(assigned, -1, sizeof(assigned));

step3:递归(与或图)。
在这里插入图片描述
在这里插入图片描述

  • 代码
#include <bits/stdc++.h>
using namespace std;
const int like[5][5] = {{0, 0, 1, 1, 0},
				        {1, 1, 0, 0, 1},
				        {0, 1, 1, 0, 1},
				        {0, 0, 0, 1, 0},
				        {0, 1, 0, 0, 1}};
int sum;	// 总方案数
int assigned[5];
void Try(int pig) {	// 也就是所谓的dfs函数
	// 递归终止条件:所有猪都分配到了合适的饲料
	if (pig == 5) {
		cout << "第" << ++sum << "个方案:";
		for (int i = 0; i < 5; ++i) {
			cout << assigned[i] << ' ';
		} 
		cout << '\n';
		return ;
	} 
	// 为每袋饲料找到合适的猪
	for (int feed = 0; feed < 5; ++feed) {
		// 判断是否满足分饲料的条件
		if (like[pig][feed] != 1 || assigned[feed] != -1) {
			continue;
		} 
		// 记录这袋饲料的分配情况 
		assigned[feed] = pig;
		// 为下一只猪找到合适的饲料 
		Try(pig + 1);
		// 回溯,尝试另一袋饲料
		assigned[feed] = -1; 
	}
} 
int main() {
	memset(assigned, -1, sizeof(assigned));
	Try(0);	// 从编号为0的猪开始寻找方案 
	return 0;
} 
  • 拓展延伸:是否可以不使用回溯
#include <bits/stdc++.h>
using namespace std;
const int like[5][5] = {{0, 0, 1, 1, 0},
				        {1, 1, 0, 0, 1},
				        {0, 1, 1, 0, 1},
				        {0, 0, 0, 1, 0},
				        {0, 1, 0, 0, 1}};
int sum;	// 总方案数
void Try(int pig, int assigned[]) {
	// 递归终止条件:所有猪都分配到了合适的饲料
	if (pig == 5) {
		cout << "第" << ++sum << "个方案:";
		for (int i = 0; i < 5; ++i) {
			cout << assigned[i] << ' ';
		} 
		cout << '\n';
		return ;
	} 
	// 为每袋饲料找到合适的猪
	for (int feed = 0; feed < 5; ++feed) {
		// 判断是否满足分饲料的条件
		if (like[pig][feed] != 1 || assigned[feed] != -1) {
			continue;
		} 
		// 记录这袋饲料的分配情况 
		int nxt_assigned[5];
		for (int i = 0; i < 5; ++i) {
			nxt_assigned[i] = assigned[i];
		} 
		nxt_assigned[feed] = pig;
		// 为下一位猪猪找饲料
		Try(pig + 1, nxt_assigned); 
	}
} 
int main() {
	int assigned[5]; // 为了不与Try函数的参数重名,在main函数内部定义 
	memset(assigned, -1, sizeof(assigned));
	Try(0, assigned);	// 从编号为0的猪开始寻找方案 
	return 0;
}  

无关紧要
想必大家已经掌握“计算思维”的精髓啦,我们看看如何把这个思维用到dfs中~
知识概述
dfs里有一个经典问题,叫“迷宫问题”。在迷宫里,要做出很多操作。但无论在什么样的迷宫中,目前的位置都是必须要记录哒!
题目中的人物可以在迷宫里像很多方向走(最常见的就是上下左右)。我们要记录的就是目前的坐标。而所谓的方向数组就是位置改变的原则。
比如:

const int dir_arr[4][2] = {{0, 1}, {0, -1}, {-1, 0}, {1, 0}}; // 上下左右

例题1:八皇后问题
在一个8*8的棋盘里,放置8个皇后,使得两两互不攻击。
在这里插入图片描述

  • 思路1:for循环枚举
#include <bits/stdc++.h>
using namespace std;
#define f(n) for (q[(n) = 1]; q[(n)] <= 8; ++q[(n)])
bool is_safe(int q[]) {
	......
}
int main() {
	int q[9];
	int sum = 0;
	f(1) f(2) f(3) f(4) f(5) f(6) f(7) f(8)
	if (is_safe(q)) {
		cout << "第" << ++sum << "种方法:";
		for (int i = 1; i <= 8; ++i) {
			cout << q[i] << ' ';
		}
		cout << '\n';
	}
	return 0;
}

太暴力啦!!!
明显,这个算法的时间复杂度确实有点太高了。有没有其他方法呢?

  • 思路2:枚举+递归 = 深度优先搜索(与或图)
    在这里插入图片描述
    在这里插入图片描述
#include <bits/stdc++.h>
using namespace std;
const int N = 9, M = 17;
int sum;	// 方案数
int Q[N];	// 8个皇后所占用的行号
bool S[N];	// 当前行是否安全
bool L[M];	// 右上到左下的对角线是否安全
bool R[M];	// 左上到右下的对角线是否安全
void dfs(int col) {
	// 递归终止条件:所有列都有皇后了
	if (col == N) {
		cout << "第" << ++sum << "种方案:";
		for (int i = 1; i <= 8; ++i) {
			cout << Q[i] << ' ';
		}
		cout << '\n';
		return ;
	} 
	// 尝试当前列8行的位置
	for (int row = 1; row < N; ++row) {
		// 判断是否安全
		if (!S[row] || !L[col - row + N] || !R[col + row]) {
			continue;
		}
		// 记录当前行号
		Q[col] = row;
		// 修改是否安全的标记 
		S[row] = false;
		L[col - row + N] = false;
		R[col + row] = false;
		// 继续尝试下一列
		dfs(col + 1);
		// 回溯
		S[row] = true;
		L[col - row + N] = true;
		R[col + row] = true;
	}
}
int main() {
	for (int i = 0; i < N; ++i) {
		S[i] = true;
	}
	for (int i = 0; i < M; ++i) {
		L[i] = R[i] = true;
	}
	// 从第一行开始判断
	dfs(1);
	return 0;
}
  • 拓展延伸:能否不使用回溯
struct state {
	int Q[N];	// 8个皇后所占用的行号
	bool S[N];	// 当前行是否安全
	bool L[M];	// 右上到左下的对角线是否安全
	bool R[M];	// 左上到右下的对角线是否安全
} s;
void dfs(int col, state S) {
	// 递归终止条件:所有列都有皇后了
	if (col == N) {
		cout << "第" << ++sum << "种方案:";
		for (int i = 1; i <= 8; ++i) {
			cout << s.Q[i] << ' ';
		}
		cout << '\n';
		return ;
	} 
	// 尝试当前列8行的位置
	for (int row = 1; row < N; ++row) {
		// 判断是否安全
		if (!s.S[row] || !s.L[col - row + N] || !s.R[col + row]) {
			continue;
		}
		// 记录当前行号
		state nxt = s;
		nxt.Q[col] = row;
		// 修改是否安全的标记 
		nxt.S[row] = false;
		nxt.L[col - row + N] = false;
		nxt.R[col + row] = false;
		// 继续尝试下一列
		dfs(col + 1, nxt);
	}
}

例题2:过河卒

  • 题目
    在这里插入图片描述
    在这里插入图片描述
  • 思路1:暴力搜索
    先算出所有“马的控制点”,然后用深度优先搜索尝试每一条不途径这些点的路径。
    代码
#include <bits/stdc++.h>
using namespace std;
const int dir_zu[2][2] = {{0, 1}, {1, 0}}; // 卒的方向数组,只能向右和向下走 
const int dir_ma[2][9] = {{0, -2, -1, 1, 2, 2, 1, -1, -2},
					  	  {0, 1, 2, 2, 1, -1, -2, -2, -1}}; // 马的方向数组 
int n, m;	// B点坐标 
int x, y;	// 马的坐标 
int sum;	// 总方案数 
bool is_danger[30][30];	// (i, j)是否危险,false代表安全 
inline bool judge_in(int x, int y) {	// 判断点(x, y)是否在棋盘里 
	return x >= 0 && x <= n && y >= 0 && y <= m;
}
void init() {	// 初始化
	is_danger[x][y] = true;	// 马现在的点肯定是马的控制点 
	for (int i = 0; i < 9; ++i) {
		int pre_x = x + dir_ma[0][i];	// 目前点的x坐标 
		int pre_y = y + dir_ma[1][i];
		if (judge_in(pre_x, pre_y)) {	// 判断是否在棋盘里 
			is_danger[pre_x][pre_y] = true;	// 标记为不安全 
		}
	}
}
void dfs(int x, int y) {
	// 递归终止条件:卒已到达B点(坐标相等) 
	if (x == n && y == m) {
		++sum;
		return ;
	}
	// 尝试每一种方向 
	for (int i = 0; i < 2; ++i) {
		int nxt_x = x + dir_zu[i][0];	// 下一个点的x坐标 
		int nxt_y = y + dir_zu[i][1];	// 下一个点的y坐标
		if (!judge_in(nxt_x, nxt_y) || is_danger[nxt_x][nxt_y]) {	// 判断是否可以继续走 
			continue;
		} 
		dfs(nxt_x, nxt_y);	// 可以走就继续走 
	}
}
int main() {
	cin >> n >> m;
	cin >> x >> y;
	init();
	dfs(0, 0);
	cout << sum << '\n';
	return 0;
}

但是,在luogu上,这个代码的最后三个数据都TLE啦!
在这里插入图片描述
所以,我们要想一个新的思路…
这个思路在以后蒟蒻君给大家讲dp(动态规划)的时候会讲到。现在就是让大家熟悉以下方向数组的使用。
例题3:幻想迷宫
传送
今天的讲解就到这里,希望大家看完有所收获~~

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

蒟蒻一枚

谢谢鸭~

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

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

打赏作者

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

抵扣说明:

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

余额充值