【DFS和BFS习题集+分类总结】(更新至2023.1.1)(17788字)

2023-11-16

目录

第一题:八皇后(dfs+路径输出(前驱版))

第一题的补充练习:N皇后(dfs + 打表)

第二题:自然数的拆分

第三题:图的遍历(BFS和DFS)

第四题: fire net(dfs)

第五题:nightmare(可以走回头路的DFS)

第六题:滑雪(求矩阵里的最长连续下降序列 ,记忆化dfs)

第七题:连连看(dfs求拐数)

第八题:逃离迷宫(dfs求拐数(相对简单))

第九题:sticks(抽象的DFS)

第十题:变形课(隐藏的dfs)

第十一题:求拐数(地图方向限定,dfs)

第十二题:记忆化搜索(基础练习)

第十三题:用DFS求组合(注意特判)

第十四题:利用父节点的DFS(树上DFS)


第一题:八皇后(dfs+路径输出(前驱版))

#include <bits/stdc++.h>
using namespace std;
int r[15], c[15], rc[32], cr[32];
/*r记录不能放的row,c记录不能放的的列,rc记录不能放的主对角线,cr记录不能放的副对角线*/
int cnt = 3, pre[15];//pre是前驱数组,用于打印路径,cnt用于记录前三条
int n, ans = 0;//n是方阵大小,ans是总数
void print(int x) {
	if (pre[x] == 0) {//前驱为零
		cout << x;//打印
		return;
	}
	print(pre[x]);
	cout << ' '  << x;//递归打印
}
void dfs(int x, int y) {
	if (x == n) {//x是行数,当到了最后一行,说明通过检验进入了下一层,此刻就是结束的时候
		ans++;
		if (cnt > 0) {//这里博主是蒟蒻,居然写cnt--,结果只是跳过了一条,其他的路径都输出了
			cnt--;
			print(y);
			cout << '\n';
		}
		return;
	}
	for (int i = 1; i <= n; i++) {
		int nx = x + 1, ny = i;
		if (r[nx] || c[ny] || rc[nx + ny] || cr[nx - ny + n]) {
			continue;//非法排除
		}
		r[nx] = 1;
		c[ny] = 1;
		rc[nx + ny] = 1;//对角线的性质,和是定值坐标系的思维
		cr[nx - ny + n] = 1;//标记,不一定要加n只要是正的在范围内就可以
		pre[ny] = y; 
		dfs(nx, ny);
		r[nx] = 0;
		c[ny] = 0;//清除标记
		rc[nx + ny] = 0;
		cr[nx - ny + n] = 0;
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	dfs(0, 0);
	cout << ans << '\n';
	return 0;
}

第一题的补充练习:N皇后(dfs + 打表)

闲聊:这里非要你打表,不然要超时。题目类似,权当巩固了。

#include <bits/stdc++.h>
using namespace std;
const int N = 15;
int n, ans, r[N], rc[2 * N], cr[2 * N], a[N];
void dfs(int y) {
	for (int i = 1; i <= n; i++) {
		if (r[i] || rc[i + y] || cr[i - y + n]) {//行,行列和,行列差(+n保证是正的)
			continue;
		}
		if (y == n) {//注意这个不可以写在开头,这样会带来过量可能性,我们要保证第列已经放入棋盘
			ans++;//不过改成n+1放开头就可以了,加个return,不加问题不大,棋盘已满
		}
		r[i] = 1;
		rc[i + y] = 1;
		cr[i - y + n] = 1;
		dfs(y + 1);//y是列
		r[i] = 0;
		rc[i + y] = 0;
		cr[i - y + n] = 0;
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	for (int i = 1; i <= 10; i++) {
		n = i;
		ans = 0;
		memset(r, 0, sizeof(r));
		memset(rc, 0, sizeof(rc));
		memset(cr, 0, sizeof(cr));
		dfs(1);
		a[i] = ans; 
	}
	while (cin >> n, n) {
		cout << a[n] << '\n';
	}
	return 0; 
} 

第二题:自然数的拆分

这里我们用前驱打印不太好,以为可能会陷入死循环,pre[1] = 1会一直递归可就尬了和前面的八皇后的问题不一样,八皇后问题的每一个序号都是独立这里不是。

另外,还要注意一点的是,这里要求升序输出,所以用了三个量的DFS,这样写起来清楚一些。

还有一个小点,就是单个数不算。

PS:这里博主又犯病了,在main函数里面再次定义了一遍n,导致,n在dfs内部为零,在主函数内为n,在函数体里面,定义变量的作用域我们优先考虑较近的,如果没有则考虑全局变量,而且这个是不会报错的。

PS:其实用DFS去拆分数效率很低,这里给的范围也很小,实际上,拆分自然数的最好方法,我个人认为的最好方法是【母函数】,许多人说时间复杂度有(n^3)但是实际上通过一般的剪枝复杂度只有(n^2)左右,再加上FFT傅里叶变换,可以更低,不过博主也不会,现在还是一个蒟蒻的ACM预备队员,零基础学起来,相当痛苦,各种OI爷,这是题外话了。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
int n;//全局变量
int ans[10];//记录答案的静态数组
void dfs(int x, int l,int sum) {//分别要选第几个数(数组下标),上一次选用的数,当前总和
	if (sum == n) {//数满了后回溯
		if (x >= 2) {//单个数不算
			cout << ans[0];//先打一个
		for (int i = 1; i < x; i++) {
			cout << '+' << ans[i]; 
		}
		}
		cout << '\n';
		return;
	}
	for (int i = 1; i <= n; i++) {
		int nsum = sum + i;//新的和
		if (nsum > n || i < l) {
			continue;//如果超出,或者减小了排除
		}
		ans[x] = i;//记录答案
		dfs(x + 1, i, nsum);//更新
	}
} 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
//  int n//这里博主的蒟蒻体现出来了-_-
	cin >> n;
	dfs(0, 0, 0);
	return 0;
} 

第三题:图的遍历(BFS和DFS)

闲聊:本题主要有两个步骤,一个是存图,存图一般有两个方法,一个是vector<> v[]存图,这比单纯用二维数组要好一些,另一个是链式前向星(博主现在还太菜,不熟悉,在dijkstra里的堆优化算法里有用过)。

存图的目的是,把输入的有向关系,按照相同起点整合到一起,并且将每一个同起点的集合按终点从小到大,有序的整理起来,这样就可以进行搜索了。

这里还想说一点的是,博主在做这道题的时候,没有标记1点结果就错了3个测试点,虽然样例和题目的暗示里面都说从一开始,没想到测试数据里面真的有指向1的点,这个告诉我们一定要标记完整。

bool数组似乎可以更省空间,直接赋值常数即可,不必写false和true,浪费笔墨(虽然没有笔墨)。

下面是代码:(必有注释, 因为自己也要复习看看)

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> a[N];//用于存图
bool book[N];//用于标记这个点是否已经输出
int n;
void dfs(int x) {//这个dfs有点特殊,不用写出口条件,因为图是有限的
	for (int i = 0; i < a[x].size(); i++) {
		if (book[a[x][i]]) {//如果已经输出过了就跳过
			continue;
		}
		book[a[x][i]] = 1;//标记
		cout << ' ' << a[x][i];
		dfs(a[x][i]);//这里不必复原,清除标记,因为这里就是遍历所有的节点而非寻路
	}
}
queue<int> q;
void bfs() {
	memset(book, 0, sizeof(book));
	book[1] = 1;
	q.push(1);
	while (!q.empty()) {
		int c = q.front();//和priority_queue不一样,是没有.top()的
		q.pop();
		for (int i = 0; i < a[c].size(); i++) {
			if (book[a[c][i]]) {
				continue;
			}
			q.push(a[c][i]);/*这是原来的节点‘生长’出来的新的节点,是‘一度关系’,似乎是有点层次遍历的味道,不过博主还是不太懂(菜)*/
			book[a[c][i]] = 1;
			cout << ' ' << a[c][i];
		}
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int m;
	cin >> n >> m;
	int u, v;
	for (int i = 0; i < m; i++) {
		cin >> u >> v;
		a[u].push_back(v);//存图,u表示起点,a[u]是一个vector用于存储起点为u的有向边
	}
	for (int i = 1; i <= n; i++) {
		sort(a[i].begin(), a[i].end());//整理以每一个节点的为起点的终点的顺序
	}
	book[1] = 1;//就是这里wa了三个的痛
	cout << 1;//一般oj平台有special judge,故先打一个,然后其他的打空格 + 数即可
	dfs(1);/*dfs一般都有变量,有几个看情况,有时候有返回值,比如记忆化搜索,需要返回dp值,有时候没有*/
	cout << '\n';
	cout << 1;
	bfs();//没有变量就行
	return 0;
}

第四题: fire net(dfs)

聒絮:这题类似八皇后问题,这题比较简单的蒟蒻博主一遍过。

这题主要是continue的条件写起来啰嗦一些,其他没有社么。

主要是这题给的范围很小盲目暴力就能过。

再复习一下基本的格式:

int /void dfs(int a, int b, int c ...) {

      出口条件

      一个for循环枚举所有的行动路径. {

            for循环内部,剔除一些非法的情况

             1.计算出下一个状态

            2.标记这个点

             dfs(a1 , b1, c1 ...)

           如果要遍历所有的话要回溯

           3.我们清除标记一定要清除本函数里标记的点。

}

}

代码:

#include <bits/stdc++.h>
using namespace std;
char mp[5][5];
bool book[5][5];
int maxn, n;//maxn是用来记录最大的碉堡的数目
void dfs(int step) {//step是当前的碉堡数目
	maxn = max(maxn, step);
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			if (book[i][j] || mp[i][j] == 'X') {
				continue;//如果这个位置有碉堡或者墙的话跳过
			}
			int flag = 1;//假定可以放的话
			for (int k = i; k < n; k++) {
				if (mp[k][j] == 'X') {//上下左右找,再到边界和墙之前如果有碉堡就判0
					break;
				}
				if (book[k][j]) {
					flag = 0;
				}
			}
			if (flag) {
				for (int k = i; k >= 0; k--) {
				if (mp[k][j] == 'X') {
					break;
				}
				if (book[k][j]) {
					flag = 0;
				}
			}
		}
			if (flag) {
				for (int k = j; k >= 0; k--) {
				if (mp[i][k] == 'X') {
					break;
				}
				if (book[i][k]) {
					flag = 0;
				}
			}
		}
			if (flag) {
				for (int k = j; k < n; k++) {
				if (mp[i][k] == 'X') {
					break;
				}
				if (book[i][k]) {
					flag = 0;
				}
			}
		}
			if (!flag) {
				continue;
			}
			book[i][j] = 1;
			dfs(step + 1);
			book[i][j] = 0;
			}
		}
} 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);//提高读取速度罢了
	while (cin >> n, n) {
		maxn = 0;
		memset(book, 0, sizeof(book));
		for (int i = 0; i < n; i++) {
				cin >> mp[i];//按行输入
		}
		dfs(0);
		cout << maxn << '\n';
	}
} 

第五题:nightmare(可以走回头路的DFS)

这里博主先用了DFS发现两个问题,这里是从起点出发的,但是这样写一方面会导致不能重复走,也就是不能走回头路,而且,当到达一个炸弹时间更新点之后,我们这次的更新会导致其他步的时间也被影响。

下面是第三个样例的坐标变换,行,列,时间,步数。

1
5 8
1 2 1 1 1 1 1 4
1 0 0 0 1 0 0 1
1 4 1 0 1 1 0 1
1 0 0 0 0 3 0 1
1 1 4 1 1 1 1 1
0 2 6 0
0 3 5 1
0 4 4 2
0 5 3 3
0 6 2 4
0 7 1 5
1 4 3 3
2 4 2 4
2 5 1 5
0 1 5 1
0 0 4 2
1 0 3 3
2 0 2 4
2 1 1 5
3 0 1 5
0 0 6 0
0 1 5 1
0 2 4 2
0 3 3 3//本来时间是这个
0 4 2 4
0 5 1 5
1 4 1 5
1 0 5 1
2 0 4 2
2 1 7 3
2 2 6 4//但是因为炸弹更新了之后
3 0 7 3//就是这里,这里本来时间是,3,正确的时间应该是5,(这里其实还要减一,不过这写到了下一层的dfs里面)
4 0 6 4
4 1 5 5
4 2 7 6
4 3 6 7
4 4 5 8
4 5 4 9
4 6 3 10
4 7 2 11
3 7 1 12
3 5 3 10
2 5 2 11
2 4 1 12
11

下面是错误代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 10;
int mp[N][N], n, m, sx, sy, ex, ey, book[N][N], ans, flag;
int moves[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
void dfs(int x, int y, int time, int step) {
	if (time == 0) {
		return;
	}
	if (time > 0 && x == ex && y == ey) {
		flag = 1;
		ans = step;
	}
	for (int i = 0; i < 4; i++) {
		int nx = x + moves[i][0], ny = y + moves[i][1];
		if (nx < 0 || nx >= n || ny < 0 || ny >= m || book[nx][ny]) {
			continue;
		}
		if (mp[nx][ny] == 4 && time - 1 >= 1) {//这里有点问题, 
			time = 7;
		}
		cout << nx << ' ' << ny << ' ' << time << ' ' << step << '\n';
		book[nx][ny] = 1;
		dfs(nx, ny, time - 1, step + 1);
		book[nx][ny] = 0;
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	cin >> t;
	while (t--) {
		cin >> n >> m;
		memset(book, 0, sizeof(book));
		flag = 0;
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				cin >> mp[i][j];
				if (mp[i][j] == 0) {
					book[i][j] = 1;
				}
				if (mp[i][j] == 2) {
					sx = i;
					sy = j;
				}
				if (mp[i][j] == 3) {
					ex = i;
					ey = j;
				}
			}
		}
		dfs(sx, sy, 6, 0);
		if (flag) {
			cout << ans << '\n';
		} else {
			cout << -1 << '\n';
		}
	}
	return 0;
} 

博主再次构想了一下,这里的路径可以重复那么我们应该如何使得用过的路径仍然能够使用,这里博主想起了另外一道题。 就是下面的那道题。

这里我想引入一个量来描述这个地图的海拔高低,命名为时间高度,重置后的高度为顶峰。

下面是TLE代码。为什么会超时呢,因为可能满地都是重置工具这样就成了大平原。

#include <bits/stdc++.h>
using namespace std;
const int N = 10;
int n, m, sx, sy, ex, ey;
int moves[4][2] = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
struct node {
	int x, y, timeh, time, s;//timeh是时间高度,类似于海拔,地势,s是元素,time是时间耗费 
};
node mp[N][N];//地图集合 
void bfs() {
	queue<node> q;
	q.push(mp[sx][sy]);//起点 
	while (!q.empty()) {
		node c = q.front();
		q.pop();
		for (int i = 0; i < 4; i++) {
			int nx = c.x + moves[i][0], ny = c.y + moves[i][1];
			if (nx < 0 || nx >= n || ny < 0 || ny >= m || mp[nx][ny].s == 0 || mp[nx][ny].timeh > c.timeh) {
				continue;//越界,撞墙,逆流而上是非法的。 
			}
			mp[nx][ny].timeh = c.timeh - 1;//时间高度减一 
			mp[nx][ny].time = c.time + 1;//时间耗费加一 
			if (mp[nx][ny].timeh > 0 && mp[nx][ny].s == 4) {
				mp[nx][ny].timeh = 6;//如果有重置工具的话,就重置时间高度 
			}
			if (mp[nx][ny].timeh > 0) {//如果还能走的话 
				q.push(mp[nx][ny]);
			}
		}
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	cin >> t;
	while (t--) {
		cin >> n >> m;
		memset(mp, 0, sizeof(mp));//初始化 
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				cin >> mp[i][j].s;
				mp[i][j].x = i;
				mp[i][j].y = j;//存点 
				if (mp[i][j].s == 2) {
					sx = i;
					sy = j;
					mp[i][j].timeh = 6;//初始时间高度 
				}
				if (mp[i][j].s == 3) {
					ex = i;
					ey = j;
				}
			}
		}
		bfs();
		if (mp[ex][ey].timeh > 0) {//如果有时间高度的话 
			cout << mp[ex][ey].time << '\n';
		} else {
			cout << -1 << '\n';
		}
	}
	return 0;
} 

这里参考别人的代码:

仔细批注了一下了,这个dfs没有通过简单的标记来防止重复走路,而是通过更新地图的方式,使得,就是下面那个要求一定要有时间优势或者路程优势的判断,来特殊的标记,从实现同一个格子可以走多次的效果。

确实是有新的花样。先膜为敬。

#include <bits/stdc++.h>
using namespace std;
int d[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int mp[10][10], step[10][10], bomb[10][10];/*mp是地图,建议不要用map因为可能会和map映射冲突,step*/
int n, m, minn;//minn是最小的时间,是答案
void dfs(int a, int b, int time, int s) {
	if (a < 0 || a >= n || b < 0 || b >= m || mp[a][b] == 0 || time <= 0 || s >= minn) {
		return;
	}//如果撞墙,越界,或者炸弹爆炸,或者
	if (mp[a][b] == 3) {//如果到达终点
		if (s < minn) {
			minn = s;//存储最小的
		}
		return;
	}
	if (mp[a][b] == 4) {
		time = 6;//如果有重置工具,重置
	}
	if (s >= step[a][b] && bomb[a][b] >= time) {
		return;/*如果走了多余的路或者而且时间没有增加的舍去,防止出现大平原任意移动,就是满地都是重置工具的话也不能一直走下去,不然步数肯定会浪费的。必须要有时间或者路程优势*/
	}
	step[a][b] = s;//s是用来记录原点开始的路程的,更新路程
	bomb[a][b] = time;//更新时间
	for(int i = 0; i < 4; i++) {
		dfs(a + d[i][0], b + d[i][1], time - 1, s + 1);//下一个状态
	}
}
int main() {
	int t, sx, sy, sum, time;
	cin >> t;
	while (t--) {
		memset(bomb, 0, sizeof(bomb));
		memset(step, 0x3f, sizeof(step));//初始化为正无穷, 主要是minn是取最小的缘故
		cin >> n >> m;
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < m; j++) {
				cin >> mp[i][j];
				if (mp[i][j] == 2) {
					sx = i;
					sy = j;
				}
			}
		}
		time = 6;
		sum = 0;
		minn = 0x3f3f3f3f;
		dfs(sx, sy, time, sum);
		if (minn == 0x3f3f3f3f) {
			cout << -1 << '\n';
		} else {
			cout << minn << '\n';
		}
	}
	return 0;
}

第六题:滑雪(求矩阵里的最长连续下降序列 ,记忆化dfs)

这是记忆化搜索,因为是有地势高低的,详见博主的【聒絮】。

注意一下这里一道要是mp[nx][ny] >= mp[x][y],可能会出现大平原,这样就无限 

#include <bits/stdc++.h>
using namespace std;
int r, c;
int mp[105][105] = {0};
int dp[105][105] = {0};
int moves[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
int dfs(int x, int y) {
	if (dp[x][y]) {
		return dp[x][y];
	}
	for (int i = 0; i < 4; i++) {
		int nx = x + moves[i][0];
		int ny = y + moves[i][1];
		if (nx < 1 || nx > r || ny < 1 || ny > c || mp[nx][ny] >= mp[x][y]) {
			continue;
		}
		dp[x][y] = max(dp[x][y], dfs(nx, ny) + 1);//状态转移方程
	}
	return dp[x][y];
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> r >> c;
	for (int i = 1; i <= r; i++) {
		for (int j = 1; j <= c; j++) {
			cin >> mp[i][j];
		}
	}
	int ans = 0;
	for (int i = 1; i <= r; i++) {
		for (int j = 1; j <= c; j++) {
			ans = max(ans, dfs(i, j));
		}
	}
	cout << ans + 1 << '\n';
	return 0;
}

这里补充一道和滑雪类似的题目:

hdoj1078

代码如下:

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
int n, k, a[N][N], dp[105][105];
int fx[4][2] = {0, 1, 0, -1, -1, 0, 1, 0};
int check(int x, int y) {
	if (x >= 0 && y >= 0 && x < n && y < n) {
		return 1;
	}
	return 0;
}
int dfs(int x, int y) {
	if (!dp[x][y]) {
		for (int i = 1; i <= k; i++) {
			for (int j = 0; j < 4; j++) {
				int x1 = x + fx[j][0] * i;
				int y1 = y + fx[j][1] * i;
				if (check(x1, y1) && a[x1][y1] > a[x][y]) {
					dp[x][y] = max(dp[x][y], dfs(x1, y1));
				}
			}
		}
		dp[x][y] += a[x][y];
	}
	return dp[x][y];
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	while (cin >> n >> k, n != -1 && k != -1) {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
			   cin >> a[i][j]; 
			}
		}
		memset(dp, 0, sizeof(dp));
		cout << dfs(0, 0) << '\n';
	}
}

第七题:连连看(dfs求拐数)

这题和第五题有类似之处,这里用来标记的数组并不是,单纯的bool数组,至此我们可以总结一下这种类型的题目。连连看和nightmare的类似之处,两者都不是用简单的bool数组来防止重复的。

在nigtmare里面,这里是因为重置工具的存在,导致我们可以重复地走一个点,这样就无法通过简单的标记来解决了。

为了防止它无限的走下去,我们用二维数组存下时间和空间信息,只有当能够更新多的的时间或者更短的路程才可以。这样我们就可以防止它无限地绕圈。

 连连看这道题,我们开一个二维数组来存拐数,虽然这里的路径是不可重复的,但是,这里的面积非常大,1000*1000,如果直接用DFS必然超时,这里借用了动态规划的思维,和nightmare类似,只有当拐数相等或者更小的话,才可以进队,如果直接DFS,BFS并且用bool数组来标记的话,是没有办法或者很确定是否能够到达目的地。大水漫灌似的BFS在复杂的迷宫环境下,直接标记会导致信息失真,得出错误答案,如果是DFS会超时。

ps:博主也是醉了,因为开头定义的时候没看仔细,调了半天,死活也看不出错误。

另外,这个连连看还有很多的潜规则,坑点满满,不过也是合情合理的。

下面是代码: 

#include <bits/stdc++.h>
using namespace std;
int mp[1005][1005], n, m, sx, sy, ex, ey, moves[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}, flag;/*mp是地图,n是行数,m是列数,sx是开始的行数, sy是开始的列数, ex是结束的行数, ey是结束的列数, moves是行动模式, flag是答案记录*/
int book[1005][1005];//用来记录到达某点的最小的拐数
struct node {
	int x, y, to, turn;//to是前进方向
} c, nc;//c是当前节点,nc是历史节点
int bfs() {
	memset(book, 0x3f, sizeof(book));//初始化为正无穷
	queue<node> q;
	node s;
	s.x = sx;
	s.y = sy;
	s.turn = 0;
	s.to = -1;//方向为负一,起点比较特殊,因为第一次是任意方向的,可以和任何方向接上去
	q.push(s);
	while (!q.empty()) {
		node c = q.front();
		q.pop();
		if (c.x == ex && c.y == ey && c.turn <= 2) {
			return 1;//抵达终点,且拐数小于等于2
		}
		for (int i = 0; i < 4; i++) {
			nc = c;//继承
			nc.x = c.x + moves[i][0];//更新
			nc.y = c.y + moves[i][1];
            if (nc.x <= 0 || nc.x > n || nc.y <= 0 || nc.y > m ||(mp[nc.x][nc.y] && (nc.x != ex || nc.y != ey))) {//越界,撞墙
            	continue;
			}
			if (c.to != -1) {//如果不是第一个点
				if (c.to != i) {
					nc.turn++;//拐数加
					nc.to = i;//方向更新
				}
			} else {
				nc.to = i;
			}
			if (nc.turn > 2) {
				continue;//防止超时,地图过大,刚开始可能会拐到一个很大的数,造成大量可能性
			}
			if (nc.turn <=  book[nc.x][nc.y]) {//拐数更新,直到最小拐数
				book[nc.x][nc.y] = nc.turn;
				q.push(nc);
			} 
		}
	}
	return 0;
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	while (cin >> n >> m, n + m) {
		for (int i = 1; i <= n; i++) {
			for (int j = 1; j <= m; j++) {
				cin >> mp[i][j];
			}
		}
	int t;
	cin >> t; 
	while (t--) {
		flag = 1;
		cin >> sx >> sy >> ex >> ey;
		if (mp[sx][sy] != mp[ex][ey]) {
			flag = 0;
		}//两个元素不一样
		if (!mp[sx][sy] || (sx == ex && sy == ey)) {
			flag = 0;//是0, 是同一个元素
		}
		if (flag) {
			flag = bfs();
		}
		if (flag) {
			cout << "YES\n";
		} else {
			cout << "NO\n";
		}
	}
	}
    return 0;
} 

第八题:逃离迷宫(dfs求拐数(相对简单))

闲谈:这道题目和逃离迷宫是差不多的,同样是利用每个点的最小拐数来作为标记数组。

核心代码和连连看几乎是一模一样的,这里有个小的坑点,就是行列和xy的字母顺序有些反人类。

总体觉得,这种以拐数或者其他行进过程中的量作为标记数组的题目还是比较精妙的,不但可以求出到达该点该量的最小值,其路径可能可以重复,但是又不会无止境地走下去。如果是一般的bool数组是无法走回头路,而且若是BFS的话,锯齿形的边缘会带来大量的错误的拐数。

下面是代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
char mp[N][N];
int book[N][N];//记录起点到各个点的最小拐数
int sx, sy, ex, ey, k, n, m, flag, go[4][2] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
struct node {//k是最大拐数
	int x, y, to, turn;
};
void bfs() {
	flag = 0;
	memset(book, 0x3f, sizeof(book));//好像不用定义起点为0也可以
	node p;
	p.x = sx;
	p.y = sy;
	p.to = -1;
	p.turn = 0;
	queue<node> q;
	q.push(p);
	while (!q.empty()) {
		p = q.front();
		q.pop();
		if (p.x == ex && p.y == ey && p.turn <= k) {
			flag = 1;
		}
		for (int i = 0; i < 4; i++) {
			node np;
			np.x = p.x + go[i][0];
			np.y = p.y + go[i][1];
			if (np.x < 0 || np.x >= n || np.y < 0 || np.y >= m || mp[np.y][np.x] == '*') {
				continue;
			}//撞墙,越界
			if (p.to != -1) {//如果上一个点不是起点
				if (p.to != i) {//方向不同
					np.turn = p.turn + 1;
					np.to = i; 
				} else {//方向相同
					np.turn = p.turn;//继承,这里博主之前忘记加怎么也做不出来
					np.to = i;
				}
			} else {//继承
				np.turn = p.turn;
				np.to = i;
			}
			if (np.turn > k) {
				continue;
			}
			
			if (np.turn <= book[np.y][np.x]) {//核心代码
				book[np.y][np.x] = np.turn;
				q.push(np);
			}
		}
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int t;
	cin >> t;
	while (t--) {
		cin >> m >> n;
		for (int i = 0; i < m; i++) {
			cin >> mp[i];
		}
		cin >> k >> sx >> sy >> ex >> ey;
		sx--;//减一转换坐标
		sy--;
		ex--;
		ey--;
		bfs();
	if (flag) {
		cout << "yes\n";
	} else {
		cout << "no\n";
	}
	}
}

第九题:sticks(抽象的DFS)

闲聊:这可能又算是一种类型的搜索,看起来不是搜索的搜索题。

这到题要求我们复原棒子,最小复原,首先,这些棒子的碎片都是整数,原来也肯定是整数,原来的总长我们已经知道了,就是碎片的总长,目标长度必须要整除总长,而且,这个长度肯定要大于最大的长度。

我们枚举的范围是从最长的碎片,到总长的一半,以及总长。

第一步我们找到最大值,求出总长,排序便于搜索。

第二步搜索和剪枝。

#include <bits/stdc++.h>
using namespace std;
const int N = 65;
int len[N], vis[N], n, flag, sum, num;
bool cmp(int a, int b) {
	return a > b;
}
bool dfs(int s, int cur, int cnt, int k) {/*s记录当前和,cur记录当前可以选取的木棒的最小下标,cnt记录已经成功合成木棒,k是目标长度*/ 
	if (cnt == num) {
		return 1;//达成返回 
	}
	for (int i = cur; i < n; i++) {
		if(vis[i] || (i && len[i] == len[i - 1] && !vis[i - 1])) {//如果两根木棒是一样的跳到最后一个 
			continue;
		}
		if (len[i] + s == k) {//如果可以拼起来 
			vis[i] = 1;//标记 
			if (dfs(0, 0, cnt + 1, k)) {//下一步 
				return 1;//如果最后是假的话就会返回零,如果是真的话,会连续返回1,直到第一层dfs 
			}
			vis[i] = 0;//清除标记 
			return 0;//回溯 
		}
		if (len[i] + s < k) {
			vis[i] = 1;
			if (dfs(len[i] + s, i + 1, cnt, k)) {//如果小的话说明前面的能取的都取了,故i+1 
				return 1;
			}
			vis[i] = 0;//清除标记 
			if (!s) return 0;//回溯 
		}
	}
	return 0;//如果长度超过了 
} 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	while (cin >> n, n) {
		sum = 0;
		for (int i = 0; i < n; i++) {
			cin >> len[i];
			sum += len[i];
		}
		sort(len, len + n, cmp);
		flag = 0;
		for (int i = len[0]; i <= sum / 2; i++) {
			if (sum % i == 0) {
				memset(vis, 0, sizeof(vis));//每根棒子只能用一次, 
				num = sum / i;//合成棒子的数目 
				if (dfs(0, 0, 0, i)) {//判断是否可行 
					cout << i << '\n';
					flag = 1;
					break;
				}
			}
		}
		if (!flag) {
			cout << sum << '\n';//如果上面都不行的话就只能合成一根棒子了 
		}
	}
	return 0;
}

第十题:变形课(隐藏的dfs)

这里为什么要标记呢?我们的目的是暴力搜素,可以想象一棵字典树一样发散的的结构,再遍历的过程中我们不可以走回头路,如果不标记可能会陷入死循环。有没有可能咒语需要走回头路呢?不可能,因为如果走到已经走过的点,相当于回到这棵树的上一个分叉点,可能性重复了。

下面是代码:

用的是vector存图,注意清空。

#include <bits/stdc++.h>
using namespace std;
vector<int> g[26];
int book[26];
void init() {
	memset(book, 0, sizeof(book));
	for (int i = 0; i < 26; i++) {
		g[i].clear();
	}
}
int flag;
void dfs(int x) {
	if (x == 'm' - 'a') {
		flag = 1;
		return;
	}
	for (int i = 0; i < g[x].size(); i++) {
		int nx = g[x][i];
		if (book[nx]) {
			continue;
		}
		book[nx] = 1;
		dfs(nx);
		book[nx] = 0;
	}
}
int main() {
	string a;
	init();
	while (cin >> a) {
		if (a != "0") {
			g[a[0] - 'a'].push_back(a[a.size() - 1] - 'a'); 
		} else {
			flag = 0;
			book['b' - 'a'] = 1;
			dfs('b' - 'a');
			init();
			if (flag) {
			cout << "Yes.\n";
		} else {
			cout << "No.\n";
		}
		}
	}
}

第十一题:求拐数(地图方向限定,dfs)

只需按照题意来模拟,注意开始阶段和结束阶段,挡板只能使用一次,这里需要构造三个变量的dfs,含义是,当前位置,和当前运动运动方向。

代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 105;
char mp[N][N];
int n, m; 
int book[N][N];
int cnt = 0;
void dfs(int x, int y, int turn) {
	if (x < 0 || x >= n || y < 0 || y >= m) {//直到小车飞出输出答案
		cout << cnt << '\n';
		return;
	}
	int nx, ny, nturn;
	if (!book[x][y]) {//如果这个挡板没有用过,进入下一个位置,并标记
		book[x][y] = 1;
		if (mp[x][y] == 'D') {
		nx = x + 1;
		ny = y;
		nturn = 1;
	} else if (mp[x][y] == 'R') {
		nx = x;
		ny = y + 1;
		nturn = 2;
	} else if (mp[x][y] == 'U') {
		nx = x - 1;
		ny = y;
		nturn = 3;
	} else {
		nx = x;
		ny = y - 1;
		nturn = 4;
	}//nturn为下一个方向
	if (turn != nturn && turn != 0) {
		cnt++;//如果说当前方向和上一个方向不同,而且上一个不是开始的时候,那么拐数加1
	}//在这种地方特别要小心,可以思考一下在地图里运动的时候的情况,地图里每个点都有一个nturn
//和turn,除了开头我们每次都比较了,也没有遗漏,所以是正确的。
	dfs(nx, ny, nturn);//新的位置和新的运动方向
	} else {//如果没有挡板
		if (turn == 1) {//遵循历史方向,这也就是存当前运动方向的好处
			dfs(x + 1, y, turn);
		} else if (turn == 2) {
			dfs(x, y + 1, turn);
		} else if (turn == 3) {
			dfs(x - 1, y, turn);
		} else if (turn == 4) {
			dfs(x, y - 1, turn);
		}
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	int x, y;
	cin >> n >> m >> x >> y;
	for (int i = 0; i < n; i++) {
		cin >> mp[i];
	}
	x--;//注意减一换成坐标
	y--;
	dfs(x, y, 0);
	return 0;
}

第十二题:记忆化搜索(基础练习)

这题有个坑点,一个abc太大的化会RE,还有如果修改abc的化会影响答案中abc的数值,注意顺序。

#include <bits/stdc++.h>
using namespace std;
long long dp[25][25][25] = {0};
long long dfs(long long a,long long b,long long c)
{
    if(a<=0||b<=0||c<=0) return 1;
    else if(dp[a][b][c]!=0) return dp[a][b][c];
    else if(a>20||b>20||c>20) return dp[a][b][c]=dfs(20,20,20);
    else if(a<b&&b<c) {
    	dp[a][b][c - 1] = dfs(a,b,c-1);
    	dp[a][b - 1][c - 1] = dfs(a,b-1,c-1);
    	dp[a][b - 1][c] = dfs(a,b-1,c);
        return	dp[a][b][c]=dp[a][b][c - 1]+dp[a][b - 1][c - 1]-dp[a][b - 1][c];
	} else {
    	dp[a - 1][b][c] = dfs(a - 1, b, c);
		dp[a - 1][b - 1][c] = dfs(a - 1, b - 1, c);
		dp[a - 1][b][c - 1] = dfs(a - 1, b, c - 1);
		dp[a - 1][b - 1][c - 1] = dfs(a - 1, b - 1, c - 1);
        return dp[a][b][c] = dp[a - 1][b][c] + dp[a - 1][b - 1][c] + dp[a - 1][b][c - 1] - dp[a - 1][b - 1][c - 1];
	}
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	long long a, b, c;
	while (cin >> a >> b >> c, a != -1 || b != -1 || c != -1) {
		printf("w(%lld, %lld, %lld) = ", a, b, c);
		if(a>20) a = 21;
        if(b>20) b = 21;
        if(c>20) c = 21;
        printf("%lld\n", dfs(a, b, c));
	}
	return 0;
}

第十三题:用DFS求组合(注意特判)​​​​​​​

这到题不是求排列而是求组合,类似二叉树的DFS。注释完整,注意边界,注意细节,注意特例。

#include <bits/stdc++.h>
using namespace std;
const int N = 15;
int a[N], b[N];
long long ans = 0x7fffffff;
int n;
void dfs(int x, long long s, long long ss) {//当前状态 
	if (x > n) return;//出口条件 
	if (x == n && s == 1 && ss == 0) return;//特判清水菜肴 
	if (x == n) ans = min(ans, abs(ss - s));//一定要完全,一定要到n 
	dfs(x + 1, s * a[x + 1], ss + b[x + 1]);//这里是组合,不是排序!!! 
	dfs(x + 1, s, ss);//继承 
} 
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) {
		cin >> a[i] >> b[i];
	} 
	dfs(0, 1, 0);//清水状态 
	cout << ans << '\n';
	return 0;
}

第十四题:利用父节点的DFS(树上DFS)

犯了一个低级但是不易察觉的错误,注意递归的本质是,一层层的内存帧存储当前的状态,绝对不可以干涉原本的状态,否则回溯便失去了意义。

另外这种判断叶子的方法很方便,高效。

#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 5;
vector<int> g[N];
int a[N], cnt = 0, m, n;
void dfs(int cur, int fa, int cat) {//标记父节点的dfs可以少开一个book数组 
	if (cat > m) return;
	bool is_leaf = 1; //假定是叶子 
	for (int i = 0; i < g[cur].size(); i++) {
		int nx = g[cur][i];
		if (nx == fa) continue;
		is_leaf = 0;//如果有下一步的话就不是叶子
		//原来我写cat = (a[nx] ? cat + a[nx]: 0) 
        //出大错了,这样修改了“当前内存帧”的cat数,等下回溯的时候就会出问题 
		dfs(nx, cur, a[nx] ? cat + a[nx] : 0);//
	}
	cnt += is_leaf;//加上叶子 
}
int main() {
	ios::sync_with_stdio(false);
	cin.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i];
	for (int i = 1; i < n; i++) {
		int x, y;
		cin >> x >> y;
		g[x].push_back(y);//要双向!!,因为题目给的顺序是随机的 
		g[y].push_back(x);
	}
	dfs(1, 0, a[1]);
	cout << cnt << '\n';
	return 0;
}

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【DFS和BFS习题集+分类总结】(更新至2023.1.1)(17788字) 的相关文章

随机推荐