BFS适合找到一个全局最优解,而DFS更适合找到一个适合解,
举个例子,如果说BFS是一群无限多的老鼠走迷宫,那么DFS就是一只老鼠走迷宫,所以在空间上利用的要比BFS小点
一只老鼠走迷宫,它在每个路口都选择最左边的(先走右边也行),能走做远就走多远,知道碰到墙壁没有办法再继续往前走了,然后回退一个路口走另外一边,就用这个办法就可以走遍所有的路,而且不会走过重复的路(注:不代表不会进行重复的计算),这个思路就是DFS
定义:
DFS 全称是Depth First Search ,中文名是深度优先搜索,是一种用于遍历或搜索树或图的算法。所谓深度优先,就是说每次都尝试向更深的节点走。
深搜是主要基于递归的(也可以引入栈这一数据结构),原理是一条路走到黑,直到将所有答案搜索完之后,再来判断哪一个是正确的解。从定义中可以看出既然是将所有的都搜索,因为不是所有的点都有用,而且甚至会有好多的点会进行重复的搜索,所以会导致做了很多的无用功即时间复杂度一般都是指数型的,很容易TLE即超时。一般这个时候就要用到剪枝的操作和记忆化搜索的操作来进行降低时间复杂度了。
dfs的运算过程就是利用递归来运算,虽然代码看起来简单,但是由于是递归所以小白在逻辑理解上会有些难以理解
dfs的简易的模板
void dfs(层数,其他参数)
{
if (出局的判断(递归出口)) {
更新答案
return;//返回上一层
}
添加剪枝条件//减少部分运算
for (枚举下一层的可能情况) {
if (used[i] == 0) {//如果第i层没有走过就可以走
used[i] = 1;//将该位置标记说明已经走过
dfs(层数 + 1, 其他参数);
used[i] = 0;//回溯以免影响上一层的状态的使用
}
}
return;//返回上一层
}
- dfs的参数
- 1.全局变量--arr
- 2.形参---x,start
- 1.全局变量--arr
典型例题:洛谷P1706全排列问题
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 10;
int n;
int arr[N];
bool st[N];//true表示选false表示不选
void dfs(int x) {
if (x > n) {
for (int i = 1;i <= n;i++) {
printf("%5d", arr[i]);
}
printf("\n");
return;
}
for (int i = 1;i <= n;i++) {
if (!st[i]) {
st[i] = true;
arr[x] = i;
dfs(x + 1);
st[i] = false;
arr[x] = 0;
}
}
}
int main() {
scanf("%d", &n);
dfs(1);
return 0;
}
洛谷P2089烤鸡
#include<iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20;
int n;
int arr[N];
int mem[59055][N];
int res = 0;
void dfs(int x, int sum) {
if (sum > n) return;//剪枝
if (x > 10) {
if (sum == n) {
res++;
for (int i = 1;i <= 10;i++) {
mem[res][i] = arr[i];
}
}
return;
}
for (int i = 1;i <= 3;i++) {
arr[x] = i;
dfs(x + 1, sum + i);
arr[x] = 0;//回溯
}
}
int main()
{
scanf("%d", &n);
dfs(1, 0);
printf("%d\n", res);
for (int i = 1;i <= res;i++) {
for (int j = 1;j <= 10;j++) {
printf("%d ", mem[i][j]);
}
printf("\n");
}
return 0;
}
再次代码中用到一个剪枝,一般来说在dfs算法中剪枝和不剪枝的时间最多可以相差一倍
剪枝
定义:剪枝是搜索的必要的优化手段,通常可以将指数级的枚举优化到近似于多项式的复杂度,打个比喻:就是把不会产生的答案的枝条,或者不必要的纸条将其剪掉。
DFS的剪枝有可行性剪枝,记忆化搜索等
1.可行性剪枝:对当前状态进行检查,如果当前条件不合法就不再继续,直接返回,例如最大的也放不满,最小的也放不下。
2.记忆化搜索:在递归过程中,有许多分支被反复计算,会大大降低算法的执法效率。用记忆化搜索,将已经计算出来的结果保存,以后需要用到的时候直接取出结果即可,避免重复计算从而提高的算法效率。---------主要用于动态规划
记忆化搜索=暴力dfs+记录答案
递推的过程=dfs向下递归的公式
递推数组的初始值=递归的边界
例题:acwing完全背包问题
第一个dfs写法是正常的dfs写法,因为会进行大量重复的计算所以时间复杂度很高很容易TLE
第二个是加了记忆化数组的dfs写法,大大减少的计算量
#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];//价值数组,重量数组
int n, m;
int mem[N][N];//记忆化数组前存多少物品,后面存背包当前的重量
//正常的dfs写法(会超时
/*int dfs(int x,int sum){
if(x>n) return 0;
if(sum<v[x]) return dfs(x + 1, sum);
//
else return max( dfs(x + 1, sum),dfs(x, sum - v[x]) + w[x]);
//拿过一次之后话能再拿
}*/
//加了记忆化数组的dfs写法几乎等价于dp
int dfs(int x, int ans) {
if (mem[x][ans])return mem[x][ans];
int sum = 0;
if (x > n) {
return 0;
}
if (ans < v[x]) sum = dfs(x + 1, ans);
else sum = max(dfs(x + 1, ans), dfs(x, ans - v[x]) + w[x]);
mem[x][ans] = sum;
return sum;
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1;i <= n;i++) {
scanf("%d %d", &v[i], &w[i]);
}
int res=dfs(1,m);
printf("%d\n",res);
return 0;
}
洛谷P1048采药
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
int n, t;
int tcost[103], mget[103];
int mem[103][1003];
int dfs(int pos, int tleft) {
if (mem[pos][tleft] != -1)
return mem[pos][tleft]; // 已经访问过的状态,直接返回之前记录的值
if (pos == n + 1) return mem[pos][tleft] = 0;
int dfs1, dfs2 = -1;
dfs1 = dfs(pos + 1, tleft);
if (tleft >= tcost[pos])
dfs2 = dfs(pos + 1, tleft - tcost[pos]) + mget[pos]; // 状态转移
return mem[pos][tleft] = max(dfs1, dfs2); // 最后将当前状态的值存下来
}
int main() {
memset(mem, -1, sizeof(mem));
cin >> t >> n;
for (int i = 1; i <= n; i++) cin >> tcost[i] >> mget[i];
cout << dfs(1, t) << endl;
return 0;
}
蓝桥2023c++b组省赛题第F题
首先的思路是
1.用dfs来搜索出由1组成的连通块
void dfs_road(int x, int y) {
for (int i = 0; i < 4; i++) {
int a = x + dxx[i], b = y + dyy[i];
if (a > n || a < 1 || b > m || b < 1) continue;
if (vis[a][b] || g[a][b] == '0') continue;
vis[a][b] = true;//标记搜过了
dfs_road(a, b);
}
}
解释下这里为什么不回溯,因为vis数组用于标记访问过的单元格,确保每个单元格最多只访问一次。因为每个单元格只会被访问一次,所以没有回溯的需要。
2.在遍历1之前将每个连通块的起点入队方便去判断是否是属于内部的子岛屿
//开搜
vector<PII> vec;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (g[i][j] == '1' && !vis[i][j]) {
vis[i][j] = 1;
vec.push_back({ i,j });//记录每个岛屿的起点即可,如果从起点可以搜到最外面的一层海,那么一定可以
dfs_road(i, j);
}
}
}
3.判断方法就是通过起点去遍历八个方向看能否走到最外层x=0,x=n+1,y=0,y=m+1,简述来说就是假如说搜不到这里就说明这个岛屿被另一个岛屿围起来了
这样通过两个dfs就可以来判断出有多少个大岛屿了
void dfs_sea(int x, int y) {
// 如果搜到最外层的海
if (x == 0 || x == n + 1 || y == 0 || y == m + 1) {
flag = 1;
return;
}
for (int i = 0; i < 8; i++) {
int a = x + dx[i], b = dy[i] + y;
if (a > n + 1 || a < 0 || b > m + 1 || b < 0) continue; // 超过外海了
if (vis[a][b] || g[a][b] == '1') continue;
vis[a][b] = true;
dfs_sea(a, b);
}
}
话不多说我们来看代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <vector>
using namespace std;
#define x first
#define y second
const int N = 100;
typedef pair<int, int> PII;
char g[N][N];
bool vis[N][N];
int dx[] = { 0,0,1,-1,1,1,-1,-1 };
int dy[] = { 1,-1,0,0,1,-1,1,-1 }; // 方向数组
int dxx[] = { 0,1,0,-1 }, dyy[] = { 1,0,-1,0 }; // 四个方向的
int t, m, n, ans = 0, flag;
void dfs_road(int x, int y) {
for (int i = 0; i < 4; i++) {
int a = x + dxx[i], b = y + dyy[i];
if (a > n || a < 1 || b > m || b < 1) continue;
if (vis[a][b] || g[a][b] == '0') continue;
vis[a][b] = true;
dfs_road(a, b);
}
}
void dfs_sea(int x, int y) {
// 如果搜到最外层的海
if (x == 0 || x == n + 1 || y == 0 || y == m + 1) {
flag = 1;
return;
}
for (int i = 0; i < 8; i++) {
int a = x + dx[i], b = dy[i] + y;
if (a > n + 1 || a < 0 || b > m + 1 || b < 0) continue; // 超过外海了
if (vis[a][b] || g[a][b] == '1') continue;
vis[a][b] = true;
dfs_sea(a, b);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
cin >> t;
while (t--) {
ans = 0;
cin >> m >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
cin >> g[i][j];
memset(vis, 0, sizeof vis);//每次dfs都要初始化一下标记数组
vector<PII> vec;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (g[i][j] == '1' && !vis[i][j]) {
vis[i][j] = 1;
vec.push_back({ i,j });//记录每个岛屿的起点即可,如果从起点可以搜到最外面的一层海,那么一定可以
dfs_road(i, j);
}
}
}
for (auto it : vec) {
memset(vis, 0, sizeof vis);
flag = 0;//用来标记是否找到外海
dfs_sea(it.x, it.y);
if (flag == 1) ans++;
}
cout << ans << '\n';
}
return 0;
}