DFS,即深度优先搜索(Depth-First-Search),是一种用于遍历或搜索树或图的算法。其思想主要可以归纳为以下几点:
一、基本思想
- 从起始节点(或源节点)开始,沿着一条路径尽可能深地访问,直到到达最深处(即无法再继续扩展的节点)。
- 当到达最深处或无法继续扩展时,回溯到前一个节点,并尝试探索其他未访问过的路径。
- 这个过程会递归进行,直到所有的节点都被访问过。
二、节点状态
在DFS中,每个节点都有三种可能的状态:
- 未访问:节点尚未被访问过。
- 已访问但未探索完所有相邻节点:节点已被访问,但其所有相邻节点尚未都被访问。
- 已访问且已探索完所有相邻节点:节点及其所有相邻节点都已被访问。
三、核心要素
- 回溯:当探索到某一步时,发现无法继续或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术称为回溯法。满足回溯条件的某个状态的点称为“回溯点”。
- 剪枝:减小搜索树规模、尽早排除搜索树中不必要的分支的一种手段。
- 数据结构:DFS通常使用栈(Stack)这种数据结构来存储待扩展的节点。栈的特点是后进先出(LIFO),这符合DFS先扩展最新产生的节点的原则。
题目
A - 蘑菇
你身处充满着蘑菇与花朵的峡谷中,花是无害的,但蘑菇是有毒的,你现在站在一朵花上,只能向附近上下左右的花朵移动。
编写一个程序,计算你总共能够接触到多少朵花(每朵花只能计数一次,计数包括初始的花)。
Input
包括多个数据集合。
第一行是两个整数 m 和 n,分别n行m列的峡谷区域。n 和 m 都不超过 20。
在接下来的 n行中,每行包括 m 个字符。每个字符表示一个物体,规则如下 1)‘.’:花 2)‘#’:蘑菇 3)‘@’:花,并且你站在这朵花上。这片峡谷上只有一个'@’
当在一行中读入的是两个零时,表示输入结束。
Output
对每个数据集合,分别输出一行
输出总共能够接触到的花朵数目
Sample
Input | Output |
---|---|
6 9 ....#. .....# ...... ...... ...... ...... ...... #@...# .#..#. 0 0 |
4 |
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
// 全局变量,用于存储起始点的坐标
int x1 = -1, y1 = -1;
// 网格的行数和列数
int n, m;
// 存储网格信息的二维字符数组
char a[24][24];
// 标记网格中每个点是否已访问的二维布尔数组
bool b[24][24];
// 存储可达空白单元格数量的全局计数器
int count;
// 存储四个方向(右、下、左、上)的偏移量
int Next[4][2] = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
// 深度优先搜索函数
void dfs(int x, int y) {
// 如果当前点是障碍物或已访问,则返回
if (a[x][y] == '#' || b[x][y]) return;
// 可达空白单元格数量加一
count++;
// 标记当前点为已访问
b[x][y] = true;
// 遍历四个方向
for (int i = 0; i < 4; i++) {
int tx = x + Next[i][0]; // 计算新点的x坐标
int ty = y + Next[i][1]; // 计算新点的y坐标
// 检查新点是否在网格内、未访问且是空白单元格
if (tx >= 1 && tx <= n && ty >= 1 && ty <= m && !b[tx][ty] && a[tx][ty] == '.') {
dfs(tx, ty); // 递归调用dfs
}
}
}
// 查找起始点'@'的函数
void find() {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (a[i][j] == '@') {
x1 = i; // 存储起始点的x坐标
y1 = j; // 存储起始点的y坐标
return; // 找到后直接返回
}
}
}
}
int main() {
// 循环读取输入,直到遇到m=0, n=0为止
while (~scanf("%d %d", &m, &n)) {
if (m == 0 && n == 0) break; // 遇到结束标志,退出循环
memset(a, 0, sizeof(a)); // 清空网格数组
memset(b, 0, sizeof(b)); // 清空访问标记数组
// 读取网格信息
for (int i = 1; i <= n; i++) {
scanf("%s", a[i] + 1); // 注意:数组下标从1开始,因此+1来跳过数组的第一个元素(未使用)
}
find(); // 查找起始点
count = 0; // 重置计数器
dfs(x1, y1); // 从起始点开始进行深度优先搜索
printf("%d\n", count); // 输出可达空白单元格的数量
}
return 0;
}
B - 油田
某公司负责探测地下油层,每次处理一个大的矩形区域。先创建一个网格,将土地划分为许多方形块,然后用传感设备分别探测每个地块,以确定该地块是否含有石油。一块含有石油的土地叫做pocket。如果两个pocket边相邻或对角相邻,则它们属于同一油层的一部分。你的工作是确定在一个网格有多少不同的油层。
Input
输入包含多组数据。每组数据都以包含m和n的一行开始,m和n是网格中行和列的数量(1 <= m <= 100,1 <= n <= 100),由一个空格分隔。如果m = 0,则表示输入结束。下面是m行,每行有n个字符(不包括行尾字符)。每个字符对应一块土地,要么是“*”,代表没有油,要么是“@”,代表一个pocket。
Output
输出网格有多少不同的油层。
Sample Input
1 1
*
3 5
*@*@*
**@**
*@*@*
1 8
@@****@*
5 5
****@
*@@*@
*@**@
@@@*@
@@**@
0 0
Sample Output
0
1
2
2
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int m, n; // 地图的行数和列数
int step[8][2] = { // 八个方向的移动数组,用于DFS遍历
{1, 0}, {0, 1}, {1, 1}, {-1, 0},
{0, -1}, {-1, -1}, {-1, 1}, {1, -1}
};
char a[105][105]; // 存储地图数据的二维字符数组
bool b[105][105]; // 标记数组,记录位置是否被访问过
int sum; // 记录岛屿(或相反,水域的连通区域)的数量
// 深度优先搜索函数
void dfs(int x, int y) {
for (int i = 0; i < 8; i++) { // 遍历八个方向
int tx = x + step[i][0]; // 计算新位置的x坐标
int ty = y + step[i][1]; // 计算新位置的y坐标
if (a[tx][ty] == '@' && tx > 0 && tx <= m && ty > 0 && ty <= n && !b[tx][ty]) {
// 检查边界条件并标记位置为已访问
b[tx][ty] = true;
dfs(tx, ty); // 递归调用DFS
}
}
}
int main() {
while (~scanf("%d %d", &m, &n)) { // 读取地图尺寸,直到输入为0 0时停止
if (m == 0) break;
sum = 0; // 初始化岛屿(或水域连通区域)数量计数器
memset(a, 0, sizeof(a)); // 初始化地图数组
memset(b, 0, sizeof(b)); // 初始化标记数组
for (int i = 1; i <= m; i++) scanf("%s", a[i] + 1); // 读取地图数据,注意从1开始索引
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (a[i][j] == '@' && !b[i][j]) {
// 标记位置为已访问,并启动DFS来遍历整个连通区域
b[i][j] = true;
dfs(i, j);
sum++; // 增加连通区域计数器
}
}
}
printf("%d\n", sum); // 输出连通区域的数量
}
return 0;
}
C - 迷宫
烤乐滋不小心走入了一个迷宫,迷宫可以看成是由n * n的格点组成,每个格点只有2种状态,.和#,前者表示可以通行后者表示不能通行。
迷宫的规则是只能移动到上下左右的相邻格点上,现在烤乐滋想要从点A走到点B,问在不走出迷宫的情况下能不能办到。
如果起点或者终点有一个不能通行(为#),则看成无法办到。
Input
第1行是测试数据的组数k,后面跟着k组输入。
每组测试数据的第1行是一个正整数n (1 <= n <= 100),表示迷宫的规模是n * n的。接下来是一个n * n的矩阵,矩阵中的元素为.或者#。再接下来一行是4个整数x1,y1,x2,y2,描述A处在第x1行, 第y1列,B处在第x2行, 第y2列。注意,x1,y1,x2,y2全部是从0开始计数的。
output
k行,每行输出对应一个输入。能办到则输出“YES”,否则输出“NO”。
Sample
Input | Output |
---|---|
2 3 .## ..# #.. 0 0 2 2 5 ..... ###.# ..#.. ###.. ...#. 0 0 4 0 |
YES NO |
代码
#include <iostream>
#include <cstring>
using namespace std;
int n, k, ha, la, hb, lb;
char a[105][105];
bool b[105][105];
bool flag;
int step[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
void dfs(int x, int y) {
if (flag) return;
if (x == hb && y == lb) {
flag = true;
return;
}
for (int i = 0; i < 4; i++) {
int tx = x + step[i][0];
int ty = y + step[i][1];
if (a[tx][ty] == '.' && tx >= 0 && tx < n && ty >= 0 && ty < n && !b[tx][ty]) {
b[tx][ty] = true;
dfs(tx, ty);
}
}
}
int main() {
cin >> k;
while (k--) {
cin >> n;
flag = false;
memset(a,0, sizeof(a));
memset(b, 0, sizeof(b));
for (int i = 0; i < n; i++) {
scanf("%s", a[i]);
}
scanf("%d%d%d%d", &ha, &la, &hb, &lb);
b[ha][la] = true;
if(a[ha][la]!='#'&&a[hb][lb]!='#') dfs(ha, la);
if (flag) printf("YES\n");
else printf("NO\n");
}
return 0;
}
D - 跳马问题
马在中国象棋以日字形规则移动。
请编写一段程序,给定n*m大小的棋盘,以及马的初始位置(x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
Input
第一行为整数T(T < 10),表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标n,m,x,y。(0<=x<=n-1,0<=y<=m-1, m < 10, n < 10)
Output
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,0为无法遍历一次。
Sample
Input | Output |
---|---|
1 5 4 0 0 |
32 |
代码
#include <bits/stdc++.h>
using namespace std;
int T;
int d[8][2]={{1,2},{1,-2},{-1,2},{-1,-2},{2,1},{2,-1},{-2,1},{-2,-1}};
int a[15][15];
bool b[15][15];
int sum,ans,sy,sx,n,m;
void dfs(int x,int y){
if(sum>=n*m-1){
ans++;
return ;
}
for(int i=0;i<8;i++){
int tx=x+d[i][0];
int ty=y+d[i][1];
if(ty<0||tx<0||tx>=n||ty>=m) continue;
if(b[tx][ty]) continue;
b[tx][ty]=1;
sum++;
dfs(tx,ty);
b[tx][ty]=0;
sum--;
}
}
int main(){
scanf("%d",&T);
while(T--){
scanf("%d%d%d%d",&n,&m,&sx,&sy);
sum=ans=0;
memset(b,0,sizeof(b));
b[sx][sy]=1;
dfs(sx,sy);
printf("%d\n",ans);
}
return 0;
}
E - 游戏关卡
烤烤做梦中
他进入了一个沉浸式游戏:站在一个2行m列的区域,他必须从左上角(1,1)出发,最终到达右下角(2,m)才算完成这一关卡。
作为一个熟练的步行者,他可以从一个格子走到相邻的八个格子 (当然,他只能在游戏区域移动)
有些格子有陷阱,一旦走到这些格子,游戏失败。
请你帮助烤烤确定是否有可能通过关卡。
烤烤梦中一共有t个关卡要通过。
Input
第一行包含一个整数t(1≤t≤100)-关卡的数量。
对于每组数据,首先输入整数m(3≤m≤100)—关卡区域的列数。
接下来的两行描述关卡。所有格子由字符“0”和“1”组成。字符’0’对应一个安全单元格,字符’1’对应一个陷阱单元格。
(数据输入保证单元格(1,1)和(2,m)是安全的。)
Output
对于每组数据,如果能够完成关卡,则输出YES,否则输出NO。
Example
Input
4 3 000 000 4 0011 1100 4 0111 1110 6 010101 101010
Output
YES YES NO YES
代码
#include <bits/stdc++.h>
using namespace std;
char a[5][105];
int d[5][2]={{0,1},{1,0},{-1,0},{1,1},{-1,1}};
bool vis[5][105];
int t,m;
bool flag;
void dfs(int x,int y){
//printf("%d %d\n",x,y);
if(flag) return;
if(x==2&&y==m) {
flag=1;
return;
}
if(a[x][y+1]=='1'){
if(a[x-1][y+1]=='1'||a[x+1][y+1]=='1') {
return;
}
}
int tx,ty;
for(int i=0;i<5;i++){
tx=x+d[i][0];
ty=y+d[i][1];
if(!vis[tx][ty]&&a[tx][ty]=='0'&&tx>0&&ty>0&&ty<=m&&tx<=2) {
vis[tx][ty]=1;
dfs(tx,ty);
}
}
}
int main(){
scanf("%d",&t);
while(t--){
flag=0;
memset(a,0,sizeof(a));
memset(vis,0,sizeof(vis));
scanf("%d",&m);
for(int i=1;i<=2;i++) scanf("%s",a[i]+1);
vis[1][1]=1;
dfs(1,1);
if(flag==1) printf("YES\n");
else printf("NO\n");
}
return 0;
}