【算法思维优先搜索-Ref】回溯&BFS思想的实际解决的一些问题【实践】

注意:结合之前的文章——回溯框架 & BFS框架进行本章的实践和理解

在此基于前面的刷题总结一点自己刷题时的思路:

  1. 算法:框架思路;
  2. D.S.:对应 数据结构的需要和构建;
  3. Note:有了大的框架、结构,注意一些细节点:①一方面,去除“变态/极端”的测试题目,②另一方面,针对题目不同点,去写/优化 对应的算法。

前言–综合巩固

一、934.最短的桥

在这里插入图片描述

解题思路: 我们可以首先使用搜索到(可用DFS)一个岛屿,然后再利用广度优先搜索,查找当前岛屿与另一个岛屿的最短距离。

学到的点:区分时间用时优化 以及 注意点 以及 小技巧
1)基础解法:

class Solution
{
public:
    /**
     * @Description: 岛屿最短距离?首先找到一个岛屿 ; 然后从这个岛屿广搜到下个岛屿。出来最短距离。
     * @Param: 
     * @Return: 
     * @Notes: 【注意:下方大佬的方法, 比我的快!100ms  me:188ms】
     * // 也可以不用,因为可以设置为 0;【小技巧】
     */
    vector<int> direction = {-1, 0, 1, 0, -1};
    int shortestBridge(vector<vector<int>> &A)
    {
        if (A.empty())
            return -1;

        int m = A.size(), n = A[0].size();
        queue<pair<int, int>> points;                            // dfs 将第一个岛屿加入到queue中。
        vector<vector<bool>> visited(m, vector<bool>(n, false)); // 也可以不用,因为可以设置为 0;【小技巧】

        for (int i = 0; i < m; ++i)
        {
            for (int j = 0; j < n; ++j)
            {
                // 搜索到第一个1 即break;
                if (A[i][j] == 0)
                    continue;
                dfs(A, visited, points, i, j);
                break;
            }
            if (!points.empty())
                break;
        }

        int dis = -1;
        // 有了queue,开始bfs
        while (!points.empty())
        {
            int sz = points.size();
            for (int j = 0; j < sz; ++j)
            {
                pair<int, int> p = points.front();
                points.pop();
                //结束条件 和 剪枝
                if (A[p.first][p.second] == 1)
                {
                    // 当前数值有1
                    return dis;
                }

                // 邻域入队----除却自身
                for (int k = 0; k < 4; ++k)
                {
                    //上下左右邻域入队 且 不能被访问过
                    int r_new = p.first + direction[k], c_new = p.second + direction[k + 1];
                    if (r_new >= 0 && r_new < A.size() && c_new >= 0 && c_new < A[0].size() && !visited[r_new][c_new]) // 【小心越界】
                    {
                        points.emplace(r_new, c_new);
                        visited[r_new][c_new] = true;
                    }
                }
            }
            dis++;
        }
        return dis;
    }
    // 深度优先搜索,找到第一个岛屿;联通的岛屿
    void dfs(vector<vector<int>> &A, vector<vector<bool>> &visited, queue<pair<int, int>> &points, int r, int c)
    {
        if (r >= 0 && r < A.size() && c >= 0 && c < A[0].size() && A[r][c]) // && A[r][c]
        {
            // do
            points.emplace(r, c);
            A[r][c] = 0;
            visited[r][c] = true;
            // printf("我当前位置为:[%d, %d]\n", r, c);
            for (int k = 0; k < 4; ++k)
            {
                // 四个方向寻找
                dfs(A, visited, points, r + direction[k], c + direction[k + 1]);
            }
        }
    }
};

2)大佬解法:

class Solution
{
public:
    vector<int> direction{-1, 0, 1, 0, -1};
    // 主函数
    int shortestBridge(vector<vector<int>> &grid)
    {
        int m = grid.size(), n = grid[0].size();
        queue<pair<int, int>> points;
        // dfs寻找第一个岛屿,并把1全部赋值为2
        bool flipped = false;
        for (int i = 0; i < m; ++i)
        {
            if (flipped)
                break;
            for (int j = 0; j < n; ++j)
            {
                if (grid[i][j] == 1)
                {
                    dfs(points, grid, m, n, i, j);
                    flipped = true;
                    break;
                }
            }
        }
        // bfs寻找第二个岛屿,并把过程中经过的0赋值为2
        int x, y;
        int level = 0;
        while (!points.empty())
        {
            ++level;
            int n_points = points.size();
            while (n_points--)
            {
                auto [r, c] = points.front();
                points.pop();
                for (int k = 0; k < 4; ++k)
                {
                    x = r + direction[k], y = c + direction[k + 1];
                    if (x >= 0 && y >= 0 && x < m && y < n)
                    {
                        if (grid[x][y] == 2)
                        {
                            continue;
                        }
                        if (grid[x][y] == 1)
                        {
                            return level;
                        }
                        points.push({x, y});
                        grid[x][y] = 2;
                    }
                }
            }
        }
        return 0;
    }
    // 辅函数
    void dfs(queue<pair<int, int>> &points, vector<vector<int>> &grid, int m, int n, int i, int j)
    {
        if (i < 0 || j < 0 || i == m || j == n || grid[i][j] == 2)
        {
            return;
        }
        if (grid[i][j] == 0)
        {
            points.push({i, j});
            return;
        }
        grid[i][j] = 2;
        dfs(points, grid, m, n, i - 1, j);
        dfs(points, grid, m, n, i + 1, j);
        dfs(points, grid, m, n, i, j - 1);
        dfs(points, grid, m, n, i, j + 1);
    }
};

3)仿照解法:

/**
 * @Description: 根据题解 修改第二版/最终版
 * @Param: 
 * @Return: 
 * @Notes: 【通过 设置为2,进行剪枝访问。】
 */
class Solution2
{
public:
    /**
     * @Description: 岛屿最短距离?首先找到一个岛屿 ; 然后从这个岛屿广搜到下个岛屿。出来最短距离。
     * @Param: 
     * @Return: 
     * @Notes: 【注意:下方大佬的方法, 比我的快!100ms  me:188ms】
     */
    vector<int> direction = {-1, 0, 1, 0, -1};
    int shortestBridge(vector<vector<int>> &A)
    {
        if (A.empty())
            return -1;

        int m = A.size(), n = A[0].size();
        queue<pair<int, int>> points;                            // dfs 将第一个岛屿加入到queue中。
        vector<vector<bool>> visited(m, vector<bool>(n, false)); // 也可以不用,因为可以设置为 0;【小技巧】

        bool findOneIs = false;
        for (int i = 0; i < m; ++i)
        {
            if(findOneIs) break;
            for (int j = 0; j < n; ++j)
            {
                // 搜索到第一个1 即break;
                if (A[i][j]){
                    dfs(A, visited, points, i, j);
                    findOneIs = true;
                    break;
                }
            }
        }

        int dis = -1;
        // int dis = 0; // 因为周边0的也加上了。
        // 有了queue,开始bfs
        while (!points.empty())
        {
            int sz = points.size();
            for (int j = 0; j < sz; ++j)
            {
                pair<int, int> p = points.front();
                points.pop();
                //结束条件 和 剪枝
                if (A[p.first][p.second] == 1)
                {
                    // 当前数值有1
                    return dis;
                }

                // 邻域入队----除却自身
                for (int k = 0; k < 4; ++k)
                {
                    //上下左右邻域入队 且 不能被访问过
                    int r_new = p.first + direction[k], c_new = p.second + direction[k + 1];
                    if (r_new < 0 || r_new >= A.size() || c_new < 0 || c_new >= A[0].size()|| visited[r_new][c_new] || A[r_new][c_new] == 2){
                        continue;
                    }
                    points.emplace(r_new, c_new);
                    visited[r_new][c_new] = true;
                
                }
            }
            dis++;
        }
        return dis;
    }
    // 深度优先搜索,找到第一个岛屿;联通的岛屿
    void dfs(vector<vector<int>> &A, vector<vector<bool>> &visited, queue<pair<int, int>> &points, int r, int c)
    {
        if (r < 0 || r >= A.size() || c < 0 || c >= A[0].size() || A[r][c]==0 || A[r][c] ==2) // && A[r][c]
            return ;
        if(A[r][c] == 1){  A[r][c] = 2; }
        
        points.emplace(r, c);
        // A[r][c] = 0;
        visited[r][c] = true;
            // printf("我当前位置为:[%d, %d]\n", r, c);
        for (int k = 0; k < 4; ++k)
        {
            // 四个方向寻找
            dfs(A, visited, points, r + direction[k], c + direction[k + 1]);
            // if (x < 0 || x >= a.length || y < 0 || y >= a[0].length || visited[x][y] || a[x][y] == 0|| a[x][y] == 2) {
            //     continue;
            // }

        }

    }
};

4)细致修改提升解法:
在这里插入图片描述
①细致到 提前剪枝——能够缩短时间;return
②小技巧, 岛屿换为2 —— 能够提前回溯,去除visited[][]使用。
③注意点:框架地方可以灵活应变, —— 下方 结束位置visited等可以在当前状态 进行;而不用在下一层提取是使用 —— 又可以加速一点点。【分支深了 平均用时减少的 明显。】
在这里插入图片描述

二、WordLadder II(Hard)

在这里插入图片描述

注意:本体虽然为Hard 但是却是常见的考题,没有特别难理解的技巧;完全可以有已有的知识迁移过来。
主要考察:思路编码和南信调试的能力。所以 建议理清思路后,自行编码和调试完成。

思路分析
如果我们从开始的单词,把与之能够转换的单词连起来,它就会长成下边的样子。

在这里插入图片描述
橙色表示结束单词,上图橙色的路线就是我们要找的最短路径。所以我们要做的其实就是遍历上边的树,然后判断当前节点是不是结束单词,找到结束单词后,还要判断当前是不是最短的路径。说到遍历当然就是两种思路了,DFS 或者 BFS。

又因为要找到最短路径,所以可以剪枝到一个搜索树到上图前四层。—— 即使用 BFS 找到这个搜索树。
在这里插入图片描述
怎么知道结束单词在哪一层呢?只能一层层的找了,也就是 BFS。此外,因为上图需要搜索的树提前是没有的,我们需要边找边更新这个树。而在 DFS 中,我们也需要这个树,其实就是需要每个节点的所有相邻节点。

所以我们在 BFS 中,就把每个节点的所有相邻节点保存到 HashMap 中,就省去了 DFS 再去找相邻节点的时间。
最后用回溯思想的 DFS找到对应的所有可能即可。

解法一:简单BFS + 回溯【注意其中 & 引用的“问题”,传值 不加会超时!】
思路分析:

  • 单词列表、起点、终点构成无向图,容易想到使用广度优先遍历找到最短路径:图中广度优先遍历要使用到;
    • 队列;
    • 标记是否访问过的「布尔数组」或者「哈希表」visited,这里 key 是字符串,故使用哈希表;

说明:本题可以在 wordList (添加到哈希表以后)上做减法,不过标准的、更可靠的做法是,另外使用一个 visited 哈希表,将访问以后的单词添加到 visited 哈希表中,下面给出的参考代码都采用这种不节约空间的做法。

  • 重点:由于要记录所有的路径,广度优先遍历「当前层」到「下一层」的所有路径都得记录下来。因此找到下一层的结点 wordA 以后,不能马上添加到 visited 哈希表里,还需要检查当前队列中未出队的单词是否还能与 wordA 建立联系;
  • 广度优先遍历位于同一层的单词,即使有联系,也是不可以被记录下来的,这是因为同一层的连接肯定不是起点到终点的最短路径的边;【如下图所示,但是我们 visited已经解决了这个问题。如我的解法
  • 使用 BFS 的同时记录遍历的路径,形式:哈希表。哈希表的 key 记录了「顶点字符串」,哈希表的值 value 记录了 key 对应的字符串在广度优先遍历的过程中得到的所有后继结点列表 successors;
  • 最后根据 successors,使用回溯算法(全程使用一份路径变量搜索所有可能结果的深度优先遍历算法)得到所有的最短路径。
    在这里插入图片描述
    为此,设计算法流程如下:
    第 1 步:使用广度优先遍历找到终点单词,并且记录下沿途经过的所有结点,以邻接表形式存储;
    第 2 步:通过邻接表,使用回溯算法得到所有从起点单词到终点单词的路径。
    我的C++解法
class Solution
{
public:
    /** 每次变一个, 且邻域在 wordlist中。
     * @Description: 单词接龙解题思路:①首先,BFS寻找最短路径的层数 和 构建搜索树的邻接表。②使用回溯思想,深搜并记录track 返回最短路径的 单词接龙多种解决方案。
     * @Param: 结构需要:见下方
     * @Return: 
     * @Notes: 注意:①subVisited ②不要 同一层相连,去除这样构建的搜索树——使不相连。
     */
    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList)
    {
        vector<vector<string>> ans;
        unordered_set<string> wordList1(wordList.begin(), wordList.end());
        if(wordList1.count(endWord) == 0) { return ans; } // 上来就没有end,返回空列表。

        queue<string> q; // 记录bfs的层
        unordered_set<string> visited;
        unordered_map<string, vector<string>> souTree;

        // 开始BFS
        q.push(beginWord);
        visited.insert(beginWord);
        bool isFound = false;

        int level = 0;
        while(!q.empty()){
            unordered_set<string> subVisited; // 方便在最外层添加进入visited
            int sz = q.size();
            
            // vector<string> successor; // 错了位置
            vector<string> preSuccessor;
            while(sz--){

                string cur = q.front();
                q.pop();
                // 结束条件,但是仍要在当前层结束后, 结束所有的bfs
                // 但是再者说,我们在 下面直接 visit了。所以不需要等到当前层提取结束,我们在访问上次一层的时候 就添加到邻域中了。
                if(cur == endWord && !isFound){
                    isFound = true;
                    break;
                }

                // 寻找邻域 并 添加到搜索树
                vector<string> neiborWord;
                vector<string> successor; // 错了位置
                findCurNeibor(cur, neiborWord, wordList1);
                for( string nei : neiborWord ){
                    if(!visited.count(nei)){

                        // [丢失 去除同一层的重复单词]
                        subVisited.insert(nei);
                        successor.push_back(nei);
                        q.push(nei);
                    }

                }
                souTree.emplace(cur, successor);

            }
            // 一层结束
            if( isFound ) { break; } // 如果找到了 break;
            visited.insert(subVisited.begin(), subVisited.end());
            level++;
            cout << "调试当前的层次为:" << level << endl;
        }// BFS结束

        // 深搜 搜索树,构建answer
        vector<string> track;
        track.push_back(beginWord);
        dfs( souTree, track, beginWord, endWord, ans);
        return ans;
    }
    // 辅助函数 —— 回溯搜索树 并 返回从begin 到 end的所有路径解
    void dfs( unordered_map<string, vector<string>> &souTree, vector<string> &track, string curWord, string endWord,  vector<vector<string>> &ans){

        // 结束条件
        if(curWord == endWord) {
            ans.push_back(track);
            return ;
        }
        if(!souTree.count(curWord)) return;

        for(string s: souTree[curWord]){
            // 剪枝
            track.push_back(s);
            // 深搜
            dfs(souTree, track, s, endWord, ans);
            // 状态重置
            track.pop_back();
        }
        

    }
    // 辅助函数--寻找当前string的所有邻域单词
    /**
     * @Description: 
     * @Param: 
     * @Return: 注意:①返回当前string的所有邻域单词 ②在里面 传入?去除同一层的单词 不要连接起来树。
     * @Notes: 
     */
    void findCurNeibor(string cur, vector<string> &neiborWord, unordered_set<string> &wordList){
        
        //26 字母只要有一个相等 且 在wordlist中就算是邻域单词
        for( char ch = 'a'; ch <= 'z' ; ch++ ){
            for(int i = 0; i<cur.size() ;++i){
                string temp = cur;
                if(ch != cur[i]){
                    cur[i] = ch;
                    if(wordList.count(cur)) { neiborWord.push_back(cur); } //存在 
                    cur = temp;//还原
                }
            }
        }

    }

};

解法二:双向BFS + 回溯【且使用 大佬的方法】

由于我们这个问题知道目标值,因此可以让起点和终点「同时」做 BFS,直到它们又交集,两边一起扩散形成的路径也是最短路径;
双向 BFS 有点像山洞挖隧道,两边一起挖,直到打通为止。双向 BFS 搜索到的集合更小,相当于做了一些剪枝操作,因此会更快一些。
在这里插入图片描述

  • 参考解法二 与参考解法一 仅仅把「广度优先遍历」方法的实现改成了「双向广度优先遍历」,回溯的代码是一样的;【但是 双向BFS中 提前returnfound的加入 更加速了算法时间用时。】
  • q1q2交替使用,实现类似队列(符合顺序性,先进先出)的效果;【但用 hash_set实现 队的形态】
  • 注意:不一定非要严格按照「一左一右」的顺序交替向中间扩散,每次都从较小的集合扩散,统计意义上搜索的范围更小;【注意 reversed的巧妙运用】

我的/大佬的C++解法

class Solution1 {
public:

    /**
     * @Description: 
     * @Param: 
     * @Return: 
     * @Notes: 
     */
    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string> &wordList)
    {
        //开始  双向BFS + 回溯法
        vector<vector<string>> ans;
        unordered_set<string> dict(wordList.begin(), wordList.end());

        if(!dict.count(endWord)){
            return ans;
        }

        dict.erase(beginWord);
        dict.erase(endWord);
        unordered_set<string> q1{beginWord}, q2{endWord};
        unordered_map<string, vector<string>> next;
        bool reversed = false, found = false;
        while(!q1.empty()){
            unordered_set<string> q; //当前值 下一个邻域需要广搜的。
            for(const auto &w:q1){
                string s = w;
                for(size_t i = 0; i<s.size() ; i++){
                    char ch = s[i];
                    for(int j = 0;j<26;j++){ // 当前位置寻找邻域
                        s[i] = j + 'a';
                        if(q2.count(s)){  // 相交了 —— 返回!  【这里是关键 —— 蕴含着 相交 + 如何加入到next中去】
                            reversed? next[s].push_back(w) : next[w].push_back(s);
                            found = true;
                        }
                        if(dict.count(s)){// 找到邻域
                            reversed ? next[s].push_back(w) : next[w].push_back(s);
                            q.insert(s);
                        }
                    }
                    //还原回去
                    s[i] = ch;
                }
            }
            if(found){
                break;
            }
            for(const auto &w : q){
                dict.erase(w);  //【新颖——防止重复访问。】
            }
            // 接来来下一层遍历, 取最小的一边双向BFS搜索
            if(q.size() <= q2.size()){
                q1 = q; // 照常
            }else{
                // 互换
                reversed = !reversed;
                q1 = q2;
                q2 = q;
            }
        }
        if(found) {
            vector<string> track = {beginWord};
            backtracking(beginWord, endWord, next, track, ans);
        }
        return ans;
    }


    // 辅助函数 —— 回溯搜索树 并 返回从begin 到 end的所有路径解
    void backtracking( string curWord, string endWord, unordered_map<string, vector<string>> &souTree, vector<string> &track,  vector<vector<string>> &ans){

        // 结束条件
        if(curWord == endWord) {
            ans.push_back(track);
            return ;
        }
        if(!souTree.count(curWord)) return;

        for(string s: souTree[curWord]){
            // 剪枝
            track.push_back(s);
            // 深搜
            backtracking(s, endWord, souTree, track, ans);
            // 状态重置
            track.pop_back();
        }
        

    }
    
};

大纲

  1. 回溯算法团灭子集、排列、组合问题

  2. 回溯算法最佳实践:解数独

  3. 回溯算法最佳实践:括号生成

  4. 如何用 BFS 算法秒杀各种智力题

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值