LeetCode 题解随笔:图论(二)(DFS与BFS)

目录

一、深度搜索

841. 钥匙和房间

岛屿系列问题

 200. 岛屿数量

1254. 统计封闭岛屿的数目

1020. 飞地的数量

695. 岛屿的最大面积

1905. 统计子岛屿

二、广度搜索

BFS代码框架

127. 单词接龙

111. 二叉树的最小深度

 752. 打开转盘锁

773. 滑动谜题


一、深度搜索

841. 钥匙和房间

bool canVisitAllRooms(vector<vector<int>>& rooms) {
        vector<bool> visited(rooms.size(), false);
        DFS(0, rooms, visited);
        // 判断是否所有房间都被访问过了
        for (bool i : visited) {
            if (!i)  return false;
        }
        return true;
    }
    // 采用深度优先搜索
    void DFS(int index, const vector<vector<int>>& rooms, vector<bool>& visited) {
        // 若已访问过,直接返回
        if (visited[index]) {
            return;
        }
        visited[index] = true;
        for (int key : rooms[index]) {
            DFS(key, rooms, visited);
        }
    }

本题的本质是判断各个房间连成的有向图(来源:代码随想录),采用广度优先搜索(BFS) 还是 深度优先搜索(DFS) 都可以。 

解决这类问题,和回溯法很类似,心中要有问题的抽象树结构。本题利用visited数组记录访问过的房间,最后判断是否所有元素都为true,即可判定能否遍历所有房间。 

岛屿系列问题

岛屿系列题目的核心考点就是用 DFS/BFS 算法遍历二维数组。二维矩阵本质上是一幅「图」,所以遍历的过程中需要一个 visited 布尔数组防止走回头路。

vector<vector<int>> dirs = { {-1,0}, {1,0}, {0,-1}, {0,1} };
    vector<vector<bool>> visited;
    void dfs(vector<vector<char>>& grid, int i, int j, vector<vector<bool>> visited) {
        int m = grid.size();
        int n = grid[0].size();
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return;
        // 访问判断
        if (visited[i][j])  return;
        // 进入节点 (i, j)
        visited[i][j] = true;
        // 递归遍历上下左右的节点
        for (auto d : dirs) {
            int next_i = i + d[0];
            int next_j = j + d[1];
            dfs(grid, next_i, next_j, visited);
        }
        // 离开节点 (i, j)
    }

 200. 岛屿数量

class Solution {
public:
    int m;
    int n;
    int numIslands(vector<vector<char>>& grid) {
        m = grid.size();
        n = grid[0].size();
        int res = 0;
        // 遍历地图
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                // 发现陆地,岛屿数+1
                if (grid[i][j] == '1')     res++;
                // 随之淹没这个岛屿
                dfs(grid, i, j);
            }
        }
        return res;
    }
    void dfs(vector<vector<char>>& grid, int i, int j) {
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return;
        // 递归返回条件:遇到水
        if (grid[i][j] == '0')    return;
        // 若不是水,把该位置淹没成水
        grid[i][j] = '0';
        // 遍历周围区域,把周围的陆地全部淹没
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

 遇到岛屿,就用dfs淹没该岛屿上所有的陆地。判断是否是海水,即判断是否为'0',也类似visited数组的作用。

1254. 统计封闭岛屿的数目

class Solution {
public:
    int m;
    int n;
    int closedIsland(vector<vector<int>>& grid) {
        m = grid.size();
        n = grid[0].size();
        int res = 0;
        for (int j = 0; j < n; j++) {
            // 把靠上边的岛屿淹掉
            dfs(grid, 0, j);
            // 把靠下边的岛屿淹掉
            dfs(grid, m - 1, j);
        }
        for (int i = 0; i < m; i++) {
            // 把靠左边的岛屿淹掉
            dfs(grid, i, 0);
            // 把靠右边的岛屿淹掉
            dfs(grid, i, n - 1);
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    res++;
                    dfs(grid, i, j);
                }
            }
        }
        return res;
    }
    void dfs(vector<vector<int>>& grid, int i, int j) {
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return;
        // 递归返回条件:遇到水
        if (grid[i][j] == 1)    return;
        // 若不是水,把该位置淹没成水
        grid[i][j] = 1;
        // 遍历周围区域,把周围的陆地全部淹没
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

 本题与上一题的区别是:靠边的陆地不算作「封闭岛屿」。因此先利用dfs将靠边的陆地淹没,那么剩下的岛屿就都是封闭岛屿了。

1020. 飞地的数量

class Solution {
public:
    int m;
    int n;
    int numEnclaves(vector<vector<int>>& grid) {
        m = grid.size();
        n = grid[0].size();
        int res = 0;
        for (int j = 0; j < n; j++) {
            // 把靠上边的岛屿淹掉
            dfs(grid, 0, j);
            // 把靠下边的岛屿淹掉
            dfs(grid, m - 1, j);
        }
        for (int i = 0; i < m; i++) {
            // 把靠左边的岛屿淹掉
            dfs(grid, i, 0);
            // 把靠右边的岛屿淹掉
            dfs(grid, i, n - 1);
        }
        // 遍历 grid,剩下的岛屿都是封闭岛屿。统计封闭岛屿的面积即可。
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    res++;
                }
            }
        }
        return res;
    }
    void dfs(vector<vector<int>>& grid, int i, int j) {
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return;
        // 递归返回条件:遇到水
        if (grid[i][j] == 0)    return;
        // 若不是水,把该位置淹没成水
        grid[i][j] = 0;
        // 遍历周围区域,把周围的陆地全部淹没
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

本题和上一题类似,区别在于淹没掉所有非封闭岛屿后,遍历过程中不需要再淹没封闭岛屿。对封闭岛屿统计其面积即可。

695. 岛屿的最大面积

class Solution {
public:
    int m;
    int n;
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        m = grid.size();
        n = grid[0].size();
        int res = 0;
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                // 遇到陆地,则淹没这片岛屿,并利用dfs求解该岛屿的面积
                if (grid[i][j] == 1) {
                    res = max(res, dfs(grid, i, j));
                }
            }
        }
        return res;
    }
    int dfs(vector<vector<int>>& grid, int i, int j) {
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return 0;
        // 递归返回条件:遇到水
        if (grid[i][j] == 0)    return 0;
        // 若不是水,把该位置淹没成水
        grid[i][j] = 0;
        // 遍历周围区域,把周围的陆地全部淹没。+1是指统计当前所在(i,j)处的陆地面积
        return  dfs(grid, i + 1, j) + dfs(grid, i, j + 1) +
                dfs(grid, i - 1, j) + dfs(grid, i, j - 1) + 1;
    }
};

 本题和前面的题不同,要统计岛屿的面积。岛屿的面积可以利用dfs的返回值获得,记录每次淹没的陆地的个数。

1905. 统计子岛屿

class Solution {
public:
    int m;
    int n;
    int countSubIslands(vector<vector<int>>& grid1, vector<vector<int>>& grid2) {
        m = grid1.size();
        n = grid1[0].size();
        int res = 0;
        // 把2中不可能是子岛屿的岛利用dfs淹没
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid1[i][j] == 0 && grid2[i][j] == 1) {
                    // grid2中的这个岛屿肯定不是子岛,淹掉
                    dfs(grid2, i, j);
                }
            }
        }
        // 统计2中剩下的岛屿数量
        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid2[i][j] == 1) {
                    res++;
                    dfs(grid2, i, j);
                }
            }
        }
        return res;
    }
    void dfs(vector<vector<int>>& grid, int i, int j) {
        // 越界判断
        if (i < 0 || j < 0 || i >= m || j >= n)    return;
        // 递归返回条件:遇到水
        if (grid[i][j] == 0)    return;
        // 若不是水,把该位置淹没成水
        grid[i][j] = 0;
        // 遍历周围区域,把周围的陆地全部淹没。+1是指统计当前所在(i,j)处的陆地面积
        dfs(grid, i + 1, j);
        dfs(grid, i, j + 1);
        dfs(grid, i - 1, j);
        dfs(grid, i, j - 1);
    }
};

 如果岛屿 B 中存在一片陆地,在岛屿 A 的对应位置是海水,那么岛屿 B 就不是岛屿 A 的子岛。

借助该思想将不是子岛的岛屿利用dfs淹没后,再求解grid2剩余的岛屿数量即可。

拓展:本题也可以借助 Union Find 并查集算法 来判断。


二、广度搜索

BFS代码框架

// 计算从起点 start 到终点 target 的最近距离
int BFS(Node start, Node target) {
    Queue<Node> q; // 核心数据结构
    Set<Node> visited; // 避免走回头路
    
    q.push(start); // 将起点加入队列
    visited.insert(start);
    int step = 0; // 记录扩散的步数

    while (q not empty) {
        int sz = q.size();
        /* 将当前队列中的所有节点向四周扩散 */
        for (int i = 0; i < sz; i++) {
            Node cur = q.pop();
            /* 划重点:这里判断是否到达终点 */
            if (cur is target)
                return step;
            /* 将 cur 的相邻节点加入队列 */
            for (Node x : cur.adj()) {
                if (x not in visited) {
                    q.push(x);
                    visited.insert(x);
                }
            }
        }
        /* 划重点:更新步数在这里 */
        step++;
    }
}

当问题可以被抽象成一幅图,每个节点有 k个相邻的节点,同时要求最短距离。那么就可以考虑使用BFS算法

127. 单词接龙

int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
        // 转化为set便于判断是否存在
        unordered_set<string> wordSet(wordList.begin(), wordList.end());
        // endWord不存在直接返回0
        if (wordSet.find(endWord) == wordSet.end())  return 0;
        // 访问标记,同时记录了当前路径长度
        unordered_map<string, int> visited;

        // 采用广度搜索,最先返回的一定是最短路径
        queue<string> ladder;
        ladder.push(beginWord);
        visited.insert(make_pair(beginWord, 1));
        while (!ladder.empty()) {
            string word = ladder.front();
            int pLength = visited[word];
            ladder.pop();
            // 广度搜索:寻找单词的所有可能解
            for (int i = 0; i < word.size(); i++) {
                string nextWord = word;
                // 寻找满足要求的新单词
                for (int j = 0; j < 26; j++) {
                    nextWord[i] = 'a' + j;
                    // 找到了新单词且为最后一个
                    if (nextWord == endWord)    return pLength + 1;
                    // 找到了符合要求的新单词,入队
                    if (wordSet.find(nextWord) != wordSet.end() 
                        && visited.find(nextWord) == visited.end()) {
                        visited.insert(make_pair(nextWord, pLength + 1));
                        ladder.push(nextWord);
                    }
                }
            }
        }
        return 0;
    }

本题只需要求出最短长度就可以了,不用找出路径(来源:代码随想录):

所以这道题要解决两个问题:

  • 图中的线是如何连在一起的:针对每个字符,从a至z进行改变,判断是否与上一个字符只相差一位;
  • 起点和终点的最短路径长度:无向图求最短路,采用广搜(以起点中心向四周扩散),广搜只要搜到了终点,那么一定是最短的路径

无向图需要标记位,标记着节点是否走过,否则就会死循环;同时可以利用set加快搜索速度。

111. 二叉树的最小深度

【本题在“二叉树”章节中,采用了后序遍历的方式实现】

int minDepth(TreeNode* root) {
        if (!root)   return 0;
        queue<TreeNode*> q;
        q.push(root);
        int res = 0;
        while (!q.empty()) {
            res++;
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                TreeNode* cur = q.front();
                q.pop();
                if (!cur->left && !cur->right)   return res;
                if (cur->left)      q.push(cur->left);
                if (cur->right)     q.push(cur->right);
            }
        }
        return res;
    }

BFS 可以找到最短距离,但是空间复杂度高,而 DFS 的空间复杂度较低。

假设二叉树是满二叉树时,节点数为 N。

对于 DFS 算法来说:空间复杂度无非是递归堆栈,最坏情况下是树的高度,也就是 O(logN)

但是对于BFS 算法:队列中每次都会储存着二叉树一层的节点,最坏情况下空间复杂度是树的最底层节点的数量N/2,用 Big O 表示的话即 O(N)

 752. 打开转盘锁

class Solution {
public:
    int openLock(vector<string>& deadends, string target) {
        // deadends转化为set方便查找
        set<string> deadends_set(deadends.begin(), deadends.end());
        if (deadends_set.count("0000") != 0)    return -1;
        // 有可能出现走回头路的情况,如0000向上拨得到1000,之后1000向下拨又返回0000,需要去重
        set<string> visited;
        int res = 0;
        // BFS算法:将可能的八种选择抽象为与当前节点相邻的八个节点,求最短路径
        queue<string> q;
        q.push("0000");
        visited.insert("0000");
        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                string cur = q.front();
                q.pop();
                if (cur == target)   return res;
                for (int j = 0; j < 4; ++j) {
                    string temp_up = plusOne(cur, j);
                    if (deadends_set.count(temp_up) == 0 && visited.count(temp_up) == 0) {
                        q.push(temp_up);
                        visited.insert(temp_up);
                    }
                        
                    string temp_down = minusOne(cur, j);
                    if (deadends_set.count(temp_down) == 0 && visited.count(temp_down) == 0) {
                        q.push(temp_down);
                        visited.insert(temp_down);
                    }
                }
            }
            res++;
        }
        // 可选结果为空仍没找到结果,说明无解
        return -1;
    }
    // 将 s[j] 向上拨动一次
    string plusOne(string s, int j) {
        if (s[j] == '9')
            s[j] = '0';
        else
            s[j] += 1;
        return s;
    }
    // 将 s[i] 向下拨动一次
    string minusOne(string s, int j) {
        if (s[j] == '0')
            s[j] = '9';
        else
            s[j] -= 1;
        return s;
    }
};

本题需要注意走回头路的情况。比如从 "0000" 拨到 "1000",但是之后从队列拿出 "1000" 时,还会拨出一个 "0000",这样会产生死循环。因此要利用visited数组进行去重。

BFS 算法的优化思路:双向 BFS——从起点和终点同时开始扩散,当两边有交集的时候停止。【要求必须知道终点位置,比如752.打开转盘锁问题是已知终点位置的】

  • 不再使用队列,而是使用 HashSet 方便快速判断两个集合是否有交集
  • while 循环的最后交换 q1 和 q2 的内容,所以只要默认扩散 q1 就相当于轮流扩散 q1 和 q2
  • 每次都选择一个较小的集合进行扩散,那么占用的空间增长速度就会慢一些,效率就会高一些
  • 参考连接:BFS 算法解题套路框架 :: labuladong的算法小抄

773. 滑动谜题

int slidingPuzzle(vector<vector<int>>& board) {
        // 将二维数组压缩为一维数组
        string target = "123450";
        string s = "";
        for (int i = 0; i < 2; ++i) {
            for (int j = 0; j < 3; ++j) {
                s.push_back('0' + board[i][j]);
            }
        }
        // 定义一维数组元素在原二维数组位置的相邻位置
        vector<vector<int>> neighbors = {
            {1, 3},
            {0, 4, 2},
            {1, 5},
            {0, 4},
            {3, 1, 5},
            {4, 2}
        };
        // BFS算法
        int res = 0;
        set<string> visited;
        queue<string> q;
        q.push(s);
        visited.insert(s);
        while (!q.empty()) {
            int sz = q.size();
            for (int i = 0; i < sz; ++i) {
                string cur_s = q.front();
                q.pop();
                if (cur_s == target)   return res;
                // 进行一次交换
                int zero_idx = cur_s.find('0');
                for (auto ex_idx : neighbors[zero_idx]) {
                    string next_s = cur_s;
                    swap(next_s[zero_idx], next_s[ex_idx]);
                    if (visited.count(next_s) != 1) {
                        q.push(next_s);
                        visited.insert(next_s);
                    }
                }
            }
            res++;
        }
        return -1;
    }

本题在于选择恰当的表征方式,把board转化为数的一个结点(采用一维字符串的形式),然后利用BFS进行搜索。

1210. 穿过迷宫的最少移动次数

int minimumMoves(vector<vector<int>>& grid) {
        int n = grid.size();
        // 存储每个单元格蛇身1/0时的距离
        vector<vector<array<int, 2>>> dist(n, vector<array<int, 2>>(n, { -1, -1 }));
        dist[0][0][0] = 0;
        // 三元组 (x,y,dir) 表示蛇尾位置和蛇身方向
        queue<tuple<int, int, int>> q;
        q.push(make_tuple(0, 0, 0));
        while (!q.empty()) {     
            int x, y, dir;
            tie(x, y, dir) = q.front();
            q.pop();
            if (dir == 0) {
                // 向右移动一个单元格
                if (y + 2 < n && dist[x][y + 1][0] == -1 && grid[x][y + 2] == 0) {
                    dist[x][y + 1][0] = dist[x][y][0] + 1;
                    q.emplace(x, y + 1, 0);
                }
                // 向下移动一个单元格
                if (x + 1 < n && dist[x + 1][y][0] == -1 && grid[x + 1][y] == 0 && grid[x + 1][y + 1] == 0) {
                    dist[x + 1][y][0] = dist[x][y][0] + 1;
                    q.emplace(x + 1, y, 0);
                }
                // 顺时针旋转 90 度
                if (x + 1 < n && y + 1 < n && dist[x][y][1] == -1 && grid[x + 1][y] == 0 && grid[x + 1][y + 1] == 0) {
                    dist[x][y][1] = dist[x][y][0] + 1;
                    q.emplace(x, y, 1);
                }
            }
            else {
                // 向右移动一个单元格
                if (y + 1 < n && dist[x][y + 1][1] == -1 && grid[x][y + 1] == 0 && grid[x + 1][y + 1] == 0) {
                    dist[x][y + 1][1] = dist[x][y][1] + 1;
                    q.emplace(x, y + 1, 1);
                }
                // 向下移动一个单元格
                if (x + 2 < n && dist[x + 1][y][1] == -1 && grid[x + 2][y] == 0) {
                    dist[x + 1][y][1] = dist[x][y][1] + 1;
                    q.emplace(x + 1, y, 1);
                }
                // 逆时针旋转 90 度
                if (x + 1 < n && y + 1 < n && dist[x][y][0] == -1 && grid[x][y + 1] == 0 && grid[x + 1][y + 1] == 0) {
                    dist[x][y][0] = dist[x][y][1] + 1;
                    q.emplace(x, y, 0);
                }
            }
        }
        return dist[n - 1][n - 2][0];
    }

广度优先搜索方法的正确性在于:我们一定不会到达同一个位置两次及以上,因为这样必定不是最少的移动次数。 

本题用蛇尾坐标表示蛇的所在位置,利于旋转坐标变换。 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值