【六十五】【算法分析与设计】使用DFS方法求出从指定入口到出口的所有迷宫路径,使用BFS方法求出从指定入口到出口的最优迷宫路径,3205. 最优配餐 - AcWing题库,BFS广度优先遍历的应用

使用 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; // 输出计算的总成本
}

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

  • 21
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

妖精七七_

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值