【算法】使用BFS算法(队列、哈希等)解决最短路径问题(C++)

2024-01-24

1. 前言

1.1 什么是最短路问题?

在这里插入图片描述

我们这里主要探讨 权值为1 的最短路问题。

1.1.1 什么是权值?

在这里插入图片描述

而对于 权值为1的最短路问题 :即不考虑每个路径点的值,只考虑最短的路径是什么即可。

1.2 如何解决此类最短路径?

在这里插入图片描述

1.3 BFS解最短路径 前提 (FloodFill / 洪流问题)

关于如何用dfs或bfs算法对二维数组进行搜索,可以看下面的一篇文章:

【算法】bfs与dfs算法解决FloodFill(洪流)问题(C++)


2. 算法题

1926.迷宫中离入口最近的出口

在这里插入图片描述

思路

在这里插入图片描述

  • 题目分析:本题是一道经典的迷宫问题,我们可以将其转化理解为上图所示。
  • 主思路:BFS,我们从起始位置开始,一层一层像外扩(上下左右四个方向都执行扩展)直到扩展到边界(即出口),此时扩展的层数就是最短的路径
  • 思路步骤:
    1. 创建一个visited数组,用于标记迷宫每位是否走过。
    2. 创建队列,存储迷宫中每一位的下标(pair类型)
    3. 将起始位置下标加入到队列中,进行循环,直至队列为空
      • 循环中每次++step,并执行sz次(队列元素个数)扩充,每次扩充即向上下左右四个方向扩。
      • 每向一个方向扩后,检查该位置是否为终点。
    4. 根据前言中的洪流问题,结合下面的代码理解!

代码

class Solution {
public:
    int dx[4] = {0, 0, -1, 1}; // 标记x, y 坐标的四个方向走向
    int dy[4] = {-1, 1, 0, 0};

    int nearestExit(vector<vector<char>>& maze, vector<int>& entrance) {
        int step = 0; // 记录走过的步数
        int a = entrance[0], b = entrance[1]; // 提取起始位置坐标
        int m = maze.size(), n = maze[0].size();
    
        vector<vector<bool>> visit; // 标记是否被遍历过
        visit.resize(m, vector<bool>(n, false)); // 初始化
        visit[a][b] = true;
        queue<pair<int, int>> q; // 存储位置坐标
        q.push({a, b});

        while(!q.empty())
        {
            ++step;
            int sz = q.size(); // 按层遍历当前队中元素
            for(int j = 0; j < sz; ++j)
            {
                auto [_x, _y] = q.front(); // 提取队头元素坐标
                q.pop();
                for(int i = 0; i < 4; ++i) // 搜索上下左右四个位置
                {
                    int x = _x + dx[i];
                    int y = _y + dy[i];
                    if((x < m && x >= 0) && (y < n && y >= 0) && !visit[x][y] && maze[x][y] == '.')
                    {
                        // 判断此时是否是终点
                        if(x == 0 || x == m - 1 || y == 0 || y == n - 1) return step;
                        q.push({x, y});
                        visit[x][y] = true;
                    }
                }
            }
        }
        return -1;
    }
};

433.最小基因变化

在这里插入图片描述

思路

  • 题意分析 :通过将字符串startGene 转化为 endGene,且每次变化都在数组bank中存在,求转化的最少次数。

  • 当我们把startGene 转化 求得最终结果endGene时,如下图所示,其本质上属于 边权为1的最路径问题,据此我们有以下思路:
    在这里插入图片描述

  • 解法 哈希表 + 队列 BFS

在这里插入图片描述

代码

  • 对于下面的代码,主要就一处进行解释:

  • 为什么要用临时字符串tmp ?

    • 对于两层for循环:每次外层循环只对一个字符进行更改,使用临时字符串tmp,而不对字符串t一直进行更改
    • (比如第一次外层循环AAAA->…->TAAA后,由于更改的是字符串tmp,此时t依然是AAAA,第二次外层循环执行时依然是从AAAA->ACAA开始)
int minMutation(string startGene, string endGene, vector<string>& bank) {
    unordered_set<string> visited; // 用于记录所有检索过的状态
    unordered_set<string> hash(bank.begin(), bank.end()); // 记录基因库中的所有序列
    string change = "ACGT"; // 用于对序列中字符进行更改

    // 处理边界情况
    if(startGene == endGene) return 0;
    if(!hash.count(endGene)) return -1;

    queue<string> q; // 创建队列存储满足条件的序列变化
    q.push(startGene);
    visited.insert(startGene);

    int minCount = 0; // 最小变化次数(最终结果)
    while(q.size())
    {
        ++minCount;
        int sz = q.size();
        while(sz--) // 队列大小即为 下一次待变化的序列数
        {
            string t = q.front(); // 提取当前序列
            q.pop();
            for(int i = 0; i < 8; ++i) // 每个序列有8个字符,对所有字符进行转化
            {
                string tmp = t; // 每次循环只对一个字符进行更改,使用临时字符串tmp,使不对t一直进行更改(比如GAAA->TAAA后,继续进)
                for(int j = 0; j < 4; ++j) // 每个字符进行"ACGT"四种变化
                {
                    tmp[i] = change[j]; // 更改字符
                    if(hash.count(tmp) && !visited.count(tmp)) // 此时序列是合法的更改(在bank中)
                    {
                        if(tmp == endGene) return minCount; // 此时的合法序列为最终结果,返回
                        q.push(tmp);
                        visited.insert(tmp);
                    }
                }
            }
        }
    }
    return -1;
}

127.单词接龙

在这里插入图片描述

思路

  • 题意分析 :这道题与上一道基因变化很类似,代码编写也十分类似,,下面是思路:
  • 解法 哈希表 + 队列BFS
    1. 题目要求找到 最短转换序列的 最小单词数 ,而beginWord自身也算一个,则在创建结果变量时我们初始化为1。
    2. 哈希表:
      • visited 用于记录所有检索过的单词,使不重复改变
      • hash 标记字典wordList的所有单词
    3. 对于两个for循环:
      • 外层循环:通过提取队头字符串,对该单词进行遍历,执行对每一位的更改
      • 内层循环:对每一位更改的具体操作,从’a’~‘z’,依次对字符进行替换

代码

int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
    unordered_set<string> hash(wordList.begin(), wordList.end());
    unordered_set<string> visited;

    if(!hash.count(endWord)) return 0;

    queue<string> q;
    q.push(beginWord);
    visited.insert(beginWord);
    
    int minCount = 1; // 首先加上beginWord自身
    while(q.size())
    {
        ++minCount;
        int sz = q.size();
        while(sz--)
        {
            string t = q.front();
            q.pop();
            for(int i = 0; i < t.size(); ++i) // 对每个单词的每一位进行改变
            {
                string tmp = t;
                for(char ch = 'a'; ch <= 'z'; ++ch)
                {
                    tmp[i] = ch;
                    if(!visited.count(tmp) && hash.count(tmp))
                    {
                        if(tmp == endWord) return minCount;
                        q.push(tmp);
                        visited.insert(tmp);
                    }
                }
            }
        }
    }
    return 0;
}

675.为高尔夫比赛砍树

在这里插入图片描述

思路

  • 题意分析 :题目要求按照从低到高的顺序砍树,求砍树所走的最小步数。这道题实则是一个对洪流和迷宫问题的结合考研,编写代码时也是如此。
  • 对于示例一
    在这里插入图片描述
  • 解法 队列BFS
  • 主要思路
    1. 利用二维数组存储树的下标,并排序得到砍树的顺序
      • 直接两层循环创建数组,并自定义比较函数排序
    2. 执行砍树过程
      • 从起始位置开始,每一小部分(1->2)都进行依次bfs,bfs用于寻找从起始树到目标树的步数。
      • 如果其中一个部分无法执行,最终就无法执行
    3. bfs
      • 创建visited与dx、dy全局数组
      • 在进行多次 BFS 搜索时,每次搜索的起点和终点都可能不同,因此需要将访问标记数组 visited 重置,以免对下一次搜索结果产生影响。
      • 随后利用队列进行bfs的具体步骤,我们在floodfill写过很多,这里不再赘述,看代码就能理解。

代码

class Solution {
public:
    int m, n; // 定义全局变量便于访问
    int cutOffTree(vector<vector<int>>& forest) {
        m = forest.size(), n = forest[0].size();
        // 1. 通过容器存储 需要砍的树
        vector<pair<int, int>> trees;
        for(int i = 0; i < m; ++i)
            for(int j = 0; j < n; ++j)
                if(forest[i][j] > 1) trees.push_back({i, j}); // 大于1的是待砍的树

        // 1.5 排序,找出 砍树顺序
        sort(trees.begin(), trees.end(), [&](const pair<int, int>& p1, const pair<int, int>& p2){
            return forest[p1.first][p1.second] < forest[p2.first][p2.second];
        });

        // 2. 砍树步骤
        int bx = 0, by = 0; // 起始位置 坐标
        int minCount = 0;
        for(auto &[a, b] : trees)
        {
            // step : 从当前位置到下一位置的步数
            int step = bfs(forest, bx, by, a, b); // 后四个参数分别为起始位置的x, y坐标以及目标位置的x, y坐标
            if(step == -1) return -1; // 其中一棵树无法砍到,最终就无法砍掉所有的树
            minCount += step;
            bx = a, by = b; // 更新下一次的起始位置
        }
        return minCount;
    }

    bool visited[51][51]; // 用于记录当前位置是否被检索过
    int dx[4] = {0, 0, -1, 1};  // 用于x坐标的上下左右方向更新
    int dy[4] = {-1, 1, 0, 0};  // 用于y坐标的上下左右方向更新
    int bfs(vector<vector<int>> &forest, int beginX, int beginY, int endX, int endY)
    {
        if(beginX == endX && beginY == endY) return 0;

        memset(visited, false, sizeof(visited));
        queue<pair<int, int>> q; // 创建队列用于搜索
        q.push({beginX, beginY});
        visited[beginX][beginY] = true;

        int step = 0;
        while(q.size())
        {
            ++step;
            int sz = q.size();
            while(sz--)
            {
                auto [a, b] = q.front(); // 提取队头元素,进行上下左右四个方向的搜索
                q.pop();
                for(int i = 0; i < 4; ++i)
                {
                    int x = a + dx[i], y = b + dy[i];
                    if((x >= 0 && x < m) && (y >= 0 && y < n) && !visited[x][y] && forest[x][y]) // 在合法范围内对所有未被检索的位置执行:
                    {
                        if(x == endX && y == endY) return step;
                        q.push({x, y});
                        visited[x][y] = true;
                    }
                }
            }
        }
        return -1;
    }
};
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【算法】使用BFS算法(队列、哈希等)解决最短路径问题(C++) 的相关文章

  • Python 的局限性是什么? [关闭]

    Closed 这个问题是基于意见的 help closed questions 目前不接受答案 我花了几天时间阅读有关 C 和 Python 的内容 发现 Python 非常简单且易于学习 所以我想知道它真的值得花时间学习吗 或者我应该花时
  • 更改 botframework Formflow 中的确认选项

    我在 botframework 中创建了一个表单流 我想更改确认选项 默认情况下需要 是 和 否 但我希望它继续进行 而不是 是 即使用户输入 确定 是 是 等 我如何添加确认选项 您需要将新条款添加到YesFormBuilder 配置的数
  • C++ 中的映射的多个键

    我有一个表 其中的条目是这样的 Row Column1 Column2 Column3 Column4 1 0X0A 1 2 A 2 0X0B 2 2 B 3 0x0C 3 2 C 现在我想使用映射 以便我可以使用第 1 列或第 2 列作为
  • 以编程方式最大化屏幕一半的窗口

    我想最大化屏幕左侧的随机窗口 我可以在我的代码中使用 Windows Aero 函数吗 这个窗口can像用鼠标一样最大化 我只想以编程方式做到这一点 I use C 我可以得到IntPtr窗户的 如果可能的话 不要伪造鼠标或键盘输入 这可以
  • 在 MVC 5 中,如何在单个 Ajax POST 请求中发送 ViewModel 和文件?

    我有一个 ASP NET MVC 5 应用程序 我正在尝试发送带有模型数据的 POST 请求 并且还包括用户选择的文件 这是我的 ViewModel 为了清晰起见进行了简化 public class Model public string
  • QSpinBox 具有用于十六进制输入的 Unsigned Int

    这里写了很多关于 QSpinBox 使用 int 作为其数据类型的限制的问题 人们通常希望显示更大的数字 就我而言 我希望能够以十六进制显示无符号 32 位整数 这意味着我希望我的范围为 0x0 0xFFFFFFFF 正常的 QSpinBo
  • 在 Silverlight 中编辑并继续?

    Edit And Continue 是我最喜欢的调试工具之一 我之前曾在基于 C 的 Winforms 和 ASP NET 项目中使用过它 但是 我在 VS 2008 上运行 Silverlight 3 0 应用程序 每当我尝试进行更改 中
  • 抑制第 3 方库控制台输出?

    我需要调用一个第三方库 该库恰好会向控制台输出一堆内容 代码就像这样 int MyMethod int a int b ThirdPartyLibrary Transform a spews unwanted console output
  • 如何在 Visual C++ 中创建 ActiveX DLL

    是否有在 Visual Studio 2008 C 中创建 ActiveX DLL 的教程 参考 我有一个使用 DLLRegisterServer UnregisterServer 构建的 DLL 并且已注册 但我在弄清楚使用什么名称来引用
  • EF Core:同时使用ID作为主键和外键

    我有两个实体 Prospect and Person 我想做的是使用Prospect ID作为主键Prospect表并作为外键PersonID 我的想法是对两个实体使用相同的 ID 而不需要PersonID on my Prospect实体
  • 无法打开作为链接添加的 configSource 文件

    在我的 MVC 应用程序中 我使用外部配置文件来保持干净的 web config 有些文件很常见 我将它们作为链接从一个位置添加到项目中 对于这些文件 我将 复制 选项设置为 始终复制 这些文件将被复制到目标文件夹 我会看到它们 但是当我尝
  • 为了安全起见,在注销时禁用浏览器的后退按钮,例如 Yahoo、Gmail 等

    首先 我在 global asax 文件中将会话变量设置为 Session SessionId 如下所示 void Session Start object sender EventArgs e Code that runs when a
  • 线程忙等待

    基本上 我需要忙着等待一些 html 出现在网页上 我创建了以下代码来忙等我 public void ExecuteBusyWaitThreads foreach Canidate canidate in allCanidates Thre
  • 未初始化的枚举变量值

    我使用 enum 声明新类型 DAY 然后从中声明两个变量 day1 和 day2 然后当我使用未初始化的值时 我应该看到 0 到 6 之间的值 因为 enumlist 中的值介于 0 到 6 之间 但我收到了这些值改为 858993460
  • 提升精神带走关键字并忽略船长

    这是使用表达式的语法的一小部分 prefix lit L not gt gt prefix lit gt gt prefix postfix 我在 postfix 内部有某种方式纯名称获取标识符 name pure lexeme boost
  • __syncthreads() 死锁

    如果只有部分线程执行 syncthreads 会导致死锁吗 我有一个这样的内核 global void Kernel int N int a if threadIdx x
  • C 结构体中的 Typedef

    首先是令我困惑的代码 typedef struct Object typedef int MyInt void destructor Object void constructor struct Object Object 为什么编译器阻止
  • 使用 ViewBag 时出现 RuntimeBinderException

    我们收到 Layout cshtml 中使用的 Viewbag 项目的 RuntimeBinderException 我们在内存分析器中观察到这些异常 它们不是致命的 一切正常 但很烦人 我们想清除它们 例如 以下代码会导致异常 Rende
  • 在一个整数中找到另一个整数的 MSB 位置左侧的 N 个连续零位

    问题是 给定一个整数val1然后 给定第二个整数 找到最高位组 最高有效位 的位置val2找到第一个整数生成的位置左侧的未设置位的连续区域 width指定minimum必须在连续中找到的未设置位的数量 即width里面没有 0 这是我的解决
  • 如何在 asp.net 中用空字符串替换字符串中的任何“/ \\ [ ] : | < > + = ; , ? *”字符

    我想用 asp net c 中的空字符串替换字符串中出现的任何以下字符 我正在尝试将其替换为 mystring contains mystring Replace 目前我正在按照上面的方法进行 有没有更干净的方法来做到这一点 感谢致敬 有很

随机推荐