使用 DFS 方法求出从指定入口到出口的所有迷宫路径
1、求解迷宫问题,有如下 8*8 的迷宫图:
char Maze[MAxN][MAxN]=
{ {'O','X','X','X','X','X','X','X'},
{'O','O','O','O','O','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','X','X','O','X','X','O'},
{'X','O','X','X','X','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','O','O','O','X','O','O'},
{'X','X','X','X','O','O','O','O'}
}
其中 O 表示通路方块,X 表示障碍方块。假设入口位置为(0, 0),出口为右下角方块位置(7, 7)。
使用 DFS 方法求出从指定入口到出口的所有迷宫路径
1.
dfs 深度优先遍历,节点信息对应是 visit 和 Maze,统一的写法,进入函数就维护信息,出函数之前就回溯消除维护。
维护信息的方式通常有两种,要么是上一节点提前维护,要么是当前节点维护,我们只需要分清楚哪些信息是上一节点维护的,哪些信息是当前节点维护的即可。
上一节点提前维护的优势是可以使用上一节点特有的信息。
只需要分清楚哪些信息是上一节点维护的,哪些信息是当前节点维护的即可。
回溯信息也分两种,要么是当前节点回溯,要么是上一节点递归回来后回溯。
void dfs(int i, int j) {
visit[i][j] = true;
Maze[i][j] = ' ';
//其他代码
visit[i][j] = false;
Maze[i][j] = 'O';
}
统一在当前节点维护信息和回溯信息。
#include<bits/stdc++.h> // 引入所有标准库
using namespace std;
const int MAxN = 8; // 定义迷宫的大小为 8x8
// 定义迷宫布局,'O' 表示可以走的路径,'X' 表示不可以走的墙
char Maze[MAxN][MAxN] =
{ {'O','X','X','X','X','X','X','X'},
{'O','O','O','O','O','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','X','X','O','X','X','O'},
{'X','O','X','X','X','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','O','O','O','X','O','O'},
{'X','X','X','X','O','O','O','O'}
};
int index = 1; // 用于记录和显示路径的序号
// 显示当前找到的迷宫路径
void showMaze() {
cout << "第" << index++ << "条路径" << endl;
for (int i = 0; i < MAxN; i++) {
for (int j = 0; j < MAxN; j++) {
cout << Maze[i][j];
}
cout << endl;
}
}
vector<vector<bool>> visit; // 用于记录迷宫中的某个位置是否已访问
int dx[] = { 1,-1,0,0 }; // 方向数组,表示行的移动(下,上,不动,不动)
int dy[] = { 0,0,1,-1 }; // 方向数组,表示列的移动(不动,不动,右,左)
// 深度优先搜索遍历迷宫
void dfs(int i, int j) {
visit[i][j] = true; // 标记当前位置已访问
Maze[i][j] = ' '; // 用空格表示当前路径的走过的痕迹
// 如果到达终点(右下角)
if (i == j && i == 7) {
showMaze(); // 显示路径
visit[i][j] = false; // 回溯,取消访问标记
Maze[i][j] = 'O'; // 还原迷宫
return;
}
// 尝试四个方向的移动
for (int k = 0; k < 4; k++) {
int x = i + dx[k];
int y = j + dy[k];
// 检查新位置是否有效,是否为开放路径,是否未被访问
if (x >= 0 && x < MAxN && y >= 0 && y < MAxN && Maze[x][y] == 'O' && !visit[x][y]) {
dfs(x, y);
}
}
// 回溯,恢复状态
visit[i][j] = false;
Maze[i][j] = 'O';
}
int main() {
visit = vector<vector<bool>>(MAxN, vector<bool>(MAxN)); // 初始化访问数组
cout << "一条迷宫路径:" << endl;
dfs(0, 0); // 从左上角开始搜索
return 0;
}
使用 BFS 方法求出从指定入口到出口的最优迷宫路径
2、求解迷宫问题,有如下 8*8 的迷宫图:
char Maze[MAxN][MAxN]=
{ {'O','X','X','X','X','X','X','X'},
{'O','O','O','O','O','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','X','X','O','X','X','O'},
{'X','O','X','X','X','X','X','X'},
{'X','O','X','X','O','O','O','X'},
{'X','O','O','O','O','X','O','O'},
{'X','X','X','X','O','O','O','O'}
}
其中 O 表示通路方块,X 表示障碍方块。假设入口位置为(0, 0),出口为右下角方块位置(7, 7)。
使用 BFS 方法求出从指定入口到出口的最优迷宫路径
1.
BFS 利用队列 queue 一层一层走。对于一个节点,如果是第一次遇到他,此时该节点距离出发点路径是最短的。
如果从多个出发点同时一层一层走,对于一个节点,第一次遇到他,此时就是最近的距离。
2.
利用 map 存储每一个第一次遇到的节点的上一个位置的节点,用于手动回溯操作。
对于每一个节点,进入下一个节点一定是第一次进去,说明此时从出发点到下一个节点距离是最近的,而这段最优距离最后一段是从当前节点到下一节点。
3.
对于每一个节点对应的信息是,visit 和 parent,visit 定义在当前节点维护,parent 定义在上一节点维护。因为 parent 维护需要用到上一节点特有的信息和当前节点的某些信息。
反正只需要知道哪些信息是在上一节点完成,哪些节点在当前节点完成,能够保证不混乱即可。
4.
正常来说,知道每一个节点需要维护的信息,可以写出这样的框架,也就是不考虑中间部分的操作,不在乎出口情况,只在乎维护每个节点的信息,对应的代码。
while (!q.empty()) {
pair<int, int> top = q.front();
q.pop();
int curi = top.first;
int curj = top.second;
visited[curi][curj] = true;
for (int k = 0; k < 4; k++) {
int x = curi + dx[k];
int y = curj + dy[k];
if (x >= 0 && x < MAXN && y >= 0 && y < MAXN && Maze[x][y] == 'O' && !visited[x][y]) {
q.push({ x,y });
parent[{x, y}] = { curi,curj };
}
}
}
5.
考虑出口的情况,一定是在当前节点信息维护之后,因为此时当前节点的信息全部维护好了,上一节点维护的以及维护,当前节点维护的也已经维护,所以可以认为当前节点的信息全部维护。
此时再去进行自己的操作。
如果是遇到出口了,此时一定是最优的路径出现了,也就是已经是最短的路径走到出口。
这个路径是什么,就要用到我们回溯的信息。存储每个节点,最短路径的最后一步,叠加起来就是完整的路径。
6.
tie(curi, curj) = parent[{curi, curj}];
tie 函数是标准库里面的函数,意思是用右边的元组中依次复制给左边元组中的值。
parent[{curi, curj}]的返回值是 pair<int,int>类型。
对应 curi,curj,左边元组的个数与右边元组的个数是对应的。依次赋值的意思。
7.
依次回溯,把最优的路径的逆序填充到数组 path 中,然后将 path 数组反转一下就可以得到正序的路径。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 8;
char Maze[MAXN][MAXN] = {
{'O', 'X', 'X', 'X', 'X', 'X', 'X', 'X'},
{'O', 'O', 'O', 'O', 'O', 'X', 'X', 'X'},
{'X', 'O', 'X', 'X', 'O', 'O', 'O', 'X'},
{'X', 'O', 'X', 'X', 'O', 'X', 'X', 'O'},
{'X', 'O', 'X', 'X', 'X', 'X', 'X', 'X'},
{'X', 'O', 'X', 'X', 'O', 'O', 'O', 'X'},
{'X', 'O', 'O', 'O', 'O', 'X', 'O', 'O'},
{'X', 'X', 'X', 'X', 'O', 'O', 'O', 'O'}
};
vector<vector<bool>> visited(MAXN, vector<bool>(MAXN, false));
int dx[] = { 0, 1, 0, -1 };
int dy[] = { 1, 0, -1, 0 };
void showMaze() {
for (int i = 0; i < MAXN; i++) {
for (int j = 0; j < MAXN; j++) {
cout << Maze[i][j];
}
cout << endl;
}
}
void BFS(int i, int j) {
queue<pair<int, int>> q;
map<pair<int, int>, pair<int, int>> parent;
vector<pair<int, int>> path;
q.push({ i,j });
parent[{i, j}] = { -1,-1 };
while (!q.empty()) {
pair<int, int> top = q.front();
q.pop();
int curi = top.first;
int curj = top.second;
visited[curi][curj] = true;
if (curi == MAXN - 1 && curj == MAXN - 1) {
while (!(curi == -1 && curj == -1)) {
path.push_back({ curi,curj });
tie(curi, curj) = parent[{curi, curj}];
}
reverse(path.begin(), path.end());
for (auto& x : path) {
Maze[x.first][x.second] = ' ';
}
showMaze();
return;
}
for (int k = 0; k < 4; k++) {
int x = curi + dx[k];
int y = curj + dy[k];
if (x >= 0 && x < MAXN && y >= 0 && y < MAXN && Maze[x][y] == 'O' && !visited[x][y]) {
q.push({ x,y });
parent[{x, y}] = { curi,curj };
}
}
}
}
int main() {
cout << "一条最优迷宫路径" << endl;
BFS(0, 0);
}
3205. 最优配餐 - AcWing 题库
栋栋最近开了一家餐饮连锁店,提供外卖服务。
随着连锁店越来越多,怎么合理的给客户送餐成为了一个急需解决的问题。
栋栋的连锁店所在的区域可以看成是一个 n×nn×n 的方格图(如下图所示),方格的格点上的位置上可能包含栋栋的分店(绿色标注)或者客户(蓝色标注),有一些格点是不能经过的(红色标注)。
方格图中的线表示可以行走的道路,相邻两个格点的距离为 11。
栋栋要送餐必须走可以行走的道路,而且不能经过红色标注的点。
送餐的主要成本体现在路上所花的时间,每一份餐每走一个单位的距离需要花费 11 块钱。
每个客户的需求都可以由栋栋的任意分店配送,每个分店没有配送总量的限制。
现在你得到了栋栋的客户的需求,请问在最优的送餐方式下,送这些餐需要花费多大的成本。
输入格式
输入的第一行包含四个整数 n,m,k,dn,m,k,d,分别表示方格图的大小、栋栋的分店数量、客户的数量,以及不能经过的点的数量。
接下来 mm 行,每行两个整数 xi,yixi,yi,表示栋栋的一个分店在方格图中的横坐标和纵坐标。
接下来 kk 行,每行三个整数 xi,yi,cixi,yi,ci,分别表示每个客户在方格图中的横坐标、纵坐标和订餐的量。(注意,可能有多个客户在方格图中的同一个位置)
接下来 dd 行,每行两个整数,分别表示每个不能经过的点的横坐标和纵坐标。
输出格式
输出一个整数,表示最优送餐方式下所需要花费的成本。
数据范围
前 30%30% 的评测用例满足:1≤n≤201≤n≤20。
前 60%60% 的评测用例满足:1≤n≤1001≤n≤100。
所有评测用例都满足:1≤n≤1000,1≤m,k,d≤n2,1≤xi,yi≤n1≤n≤1000,1≤m,k,d≤n2,1≤xi,yi≤n。
可能有多个客户在同一个格点上。
每个客户的订餐量不超过 10001000,每个客户所需要的餐都能被送到。
输入样例:
10 2 3 3 1 1 8 8 1 5 1 2 3 3 6 7 2 1 2 2 2 6 8
输出样例:
29
1.
我们需要记录的是,每一个客户到达最近的店家的距离,我们只需要知道客户和距离,其次还需要知道客户的订单数。
利用 BFS,从多个店家开始一次走一步,第一次进入的节点就是最近的,每一个点都有一个最近的距离,也就是上一节点的距离 +1。
2.
下面是一段错误的代码。
首先利用定义的变量存储所有输入的数据,一开始可以这么操作,所有输入的数据使用达到我们希望的目的,此时从结果来看很容易分析出那段数据不需要存储,那段数据需要存储。
也就是完成希望的目的之后需要将不需要存储的数据空间删除。
空间开太多了会导致时间复杂度增加。
这是从数据存储的方面减少空间使用。
3.
第二是 visits 和 distinct 两端空间实际上是可以合并在一起的,用 LLONG_MAX 表示还没有访问过的位置,访问过的位置全部置其他值,墙就置 -1,初始点置 0。
4.
代码的错误,visits[i][j]=true 应该尽可能时时刻刻保证信息维护的同步,BFS 和 DFS 不同,并没有函数的调用,所以不太能把节点拿出来的时候再维护 visits 信息。
需要维护的信息是 visits 和 distinct,尽可能保证信息时时刻刻与状态同步。
因此两个信息都选择在上一节点进行维护。
5.
ios::sync_with_stdio(0);
cin.tie(0);
这两行优化操作很重要,没有这两行优化是没办法通过所有用例的。
6.
尽可能使用 LL==long long,而不使用 int。
用 PII 表示 pair<int,int>。
7.
还有一个比较恶心的地方,很多题目中输入数据是 1-based 索引,因此还需要转化为 0-based 索引。
错误的代码
#include<bits/stdc++.h> // 引入常用库文件
using namespace std; // 使用标准命名空间
using LL = long long; // 定义LL为long long类型
using PII = pair<LL, LL>; // 定义PII为pair<long long, long long>类型
LL n, m, k, d; // 定义n为网格大小,m为分店数量,k为客户数量,d为障碍数量
vector<PII> dian_point; // 存储分店的位置
vector<vector<LL>> kehu_point; // 存储客户的位置和订餐量
vector<PII> qiang_point; // 存储不能经过的点的位置
queue<PII> q; // BFS使用的队列
vector<vector<LL>> visits; // 访问标记数组
vector<vector<LL>> distinct; // 存储每个点到最近分店的距离
int dx[] = { 1,-1,0,0 }; // x方向的移动数组
int dy[] = { 0,0,-1,1 }; // y方向的移动数组
void BFS() { // BFS函数用于计算所有点到最近分店的最短距离
while (!q.empty()) { // 当队列不为空时
PII top = q.front(); // 取出队列头元素
q.pop(); // 弹出队列头元素
LL i = top.first; // 当前点的x坐标
LL j = top.second; // 当前点的y坐标
visits[i][j] = true; // 标记当前点已访问
for (int k = 0; k < 4; k++) { // 遍历四个方向
LL x = i + dx[k], y = j + dy[k]; // 计算新的坐标
if (x >= 0 && x < n && y >= 0 && y < n && !visits[x][y]) { // 如果新坐标在范围内并且未被访问
distinct[x][y] = distinct[i][j] + 1; // 更新距离
q.push({ x,y }); // 将新坐标加入队列
}
}
}
}
int main() { // 主函数
cin >> n >> m >> k >> d; // 输入n, m, k, d
visits.resize(n, vector<LL>(n,0)); // 初始化访问标记数组
distinct.resize(n, vector<LL>(n,0)); // 初始化距离数组
for (int i = 0; i < m; i++) { // 输入分店信息
PII tmp; // 临时变量存储分店坐标
cin >> tmp.first >> tmp.second; // 输入分店坐标
tmp.first--, tmp.second--; // 转换为0-based索引
dian_point.push_back(tmp); // 存储分店坐标
q.push(tmp); // 将分店坐标加入队列
}
for (int i = 0; i < k; i++) { // 输入客户信息
LL x, y, nums; // 客户坐标和订餐量
cin >> x >> y >> nums; // 输入客户坐标和订餐量
x--, y--; // 转换为0-based索引
kehu_point.push_back({ x,y,nums }); // 存储客户信息
}
for (int i = 0; i < d; i++) { // 输入障碍信息
PII tmp; // 临时变量存储障碍坐标
cin >> tmp.first >> tmp.second; // 输入障碍坐标
tmp.first--, tmp.second--; // 转换为0-based索引
qiang_point.push_back(tmp); // 存储障碍坐标
visits[tmp.first][tmp.second] = true; // 标记障碍位置为已访问
}
BFS(); // 执行BFS计算距离
LL ans = 0; // 初始化总成本为0
for (auto& x : kehu_point) { // 遍历每个客户点
LL i = x[0], j = x[1], num = x[2]; // 客户的坐标和订餐量
ans += distinct[i][j] * num; // 累加该客户到最近分店的距离与订餐量的乘积
}
cout << ans; // 输出总成本
}
正确的代码---由错误代码优化过来
#include<bits/stdc++.h> // 引入常用的库文件,简化编程过程
using namespace std;
using LL = long long; // 定义长整型别名LL,用于处理大整数
using PII = pair<LL, LL>; // 定义PII为长整型的pair,用于存储坐标
LL n, m, k, d; // 定义方格图的尺寸n,分店数量m,客户数量k,障碍点数量d
vector<PII> dian_point; // 存储分店的坐标
vector<vector<LL>> kehu_point; // 存储客户的坐标及其订餐量
queue<PII> q; // BFS中使用的队列
vector<vector<LL>> distinct; // 存储各点到最近分店的最短距离
int dx[] = { 1,-1,0,0 }; // 方向数组,用于在x轴上移动
int dy[] = { 0,0,-1,1 }; // 方向数组,用于在y轴上移动
void BFS() { // 广度优先搜索,计算最短距离
while (!q.empty()) { // 当队列不为空时进行循环
PII top = q.front(); // 取队首元素
q.pop(); // 移除队首元素
LL i = top.first; // 当前处理的行坐标
LL j = top.second; // 当前处理的列坐标
for (int k = 0; k < 4; k++) { // 遍历四个方向
LL x = i + dx[k], y = j + dy[k]; // 计算新的坐标
if (x >= 0 && x < n && y >= 0 && y < n && distinct[x][y]==LLONG_MAX) { // 如果坐标在范围内并未被访问
distinct[x][y] = distinct[i][j] + 1; // 更新距离
q.push({ x,y }); // 新坐标加入队列
}
}
}
}
int main() {
ios::sync_with_stdio(0); // 提高cin和cout的数据流处理效率
cin.tie(0); // 解绑cin和cout
cin >> n >> m >> k >> d; // 输入基础数据
distinct.resize(n, vector<LL>(n,LLONG_MAX)); // 初始化距离数组,初始值设为LLONG_MAX
for (int i = 0; i < m; i++) { // 输入所有分店的位置
PII tmp; // 临时存储分店坐标
cin >> tmp.first >> tmp.second; // 输入坐标
tmp.first--, tmp.second--; // 坐标转换为0-based
dian_point.push_back(tmp); // 存储到数组中
distinct[tmp.first][tmp.second]=0; // 分店位置的距离设为0
q.push(tmp); // 分店坐标入队
}
for (int i = 0; i < k; i++) { // 输入所有客户的信息
LL x, y, nums; // 临时存储客户坐标和订餐量
cin >> x >> y >> nums; // 输入数据
x--, y--; // 坐标转换为0-based
kehu_point.push_back({ x,y,nums }); // 存储到数组中
}
for (int i = 0; i < d; i++) { // 输入所有障碍的位置
PII tmp; // 临时存储障碍坐标
cin >> tmp.first >> tmp.second; // 输入坐标
tmp.first--, tmp.second--; // 坐标转换为0-based
distinct[tmp.first][tmp.second] = -1; // 设置障碍点的距离为-1
}
BFS(); // 执行BFS,计算各点到最近分店的最短距离
LL ans = 0; // 初始化总成本
for (auto& x : kehu_point) { // 遍历所有客户点
LL i = x[0], j = x[1], num = x[2]; // 取出客户的坐标和订餐量
if(distinct[i][j] != -1) { // 如果客户位置不是障碍点
ans += distinct[i][j] * num; // 累加送餐成本(距离乘以订餐量)
}
}
cout << ans; // 输出计算的总成本
}
结尾
最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。
同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。
谢谢您的支持,期待与您在下一篇文章中再次相遇!