hot100 -- 图论

👂 ▶ 怀抱的温柔并不属于我(弹唱版) (163.com)

👂 ▶ Gotta Have You (163.com)

👂 ▶ 心许百年 (163.com)

目录

🚩岛屿数量

[AC DFS](#AC DFS)

[AC BFS](#AC BFS)

[AC 并查集](#AC 并查集)

解释

代码

🌼腐烂的橘子

[AC 多源bfs](#AC 多源bfs)

🎂课程表

前置知识

[AC BFS](#AC BFS)

[🌼实现 Trie(前缀树)](#🌼实现 Trie(前缀树))

解释

代码


🚩岛屿数量

200. 岛屿数量 - 力扣(LeetCode)

AC DFS

同 bfs,采用 flood fill 大水漫灌,也称感染法

if(grid[x][y] == '1') 对每个点进行 dfs,将走过的陆地 "1" 标记为 "0"

然后套模板

时间 O(mn):每个点遍历 1 次;空间 O(mn):所有点均为陆地,递归深度 m*n

cpp 复制代码
class Solution {
public:
    int ans; 

    int nex[4][2] = {{0,1},{1,0},{0,-1},{-1,0}}; // 4 个方向

    // 从 x, y 开始深度优先搜索
    void dfs(vector<vector<char>>& grid, int x, int y) {
        int m = grid.size(), n = grid[0].size();

        grid[x][y] = '0'; // 大水漫灌 / 感染法

        for (int i = 0; i < 4; ++i) { 
            // 新的坐标
            int xx = x + nex[i][0], yy = y + nex[i][1];
            // 下一位置满足要求才递归,类似递归出口的处理
            if (xx < m && yy < n && xx >= 0 && yy >= 0 && (grid[xx][yy] == '1') )
                dfs(grid, xx, yy);
        }
    }

    int numIslands(vector<vector<char>>& grid) {
        int m = grid.size(), n = grid[0].size();
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                if (grid[i][j] == '1') {
                    ans++; // 岛屿数量 +1
                    dfs(grid, i, j);
                }
        return ans;
    }
};

AC BFS

同 dfs,采用 flood fill 大水漫灌,也称感染法

if(grid[x][y] == '1') 对每个点进行 bfs,将走过的陆地 "1" 标记为 "0"

然后套模板
注意 queue 的声明,queue<pair<int, int>>,而非 queue<int, int>,因为 queue, stack, vector 这种,只能放一个参数;两个参数的是 map<int, int> 和 unordered_map<int, int>
第 31 行,遍历 4 个方向那里,越界的话,是 continue 不是 break,常犯错误

时间 O(mn),空间 O(min(m, n))

cpp 复制代码
class Solution {
public:
    int ans = 0; // 岛屿数量

    int nex[4][2] = {{0,1},{0,-1},{1,0},{-1,0}}; // 4 个方向

    int numIslands(vector<vector<char>>& grid) {

        queue<pair<int, int>> q; // 横纵坐标

        int m = grid.size(), n = grid[0].size();

        // 对陆地上每个点 bfs
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j) 
                if (grid[i][j] == '1') { // 只对陆地 bfs

                    q.push({i, j}); // 入队
                    ans++;
                    grid[i][j] = '0'; // 大水漫灌 / 感染法

                    // 逐层扩展
                    while (!q.empty()) {
                        auto cur = q.front(); // 取队头
                        q.pop();
                        int x = cur.first, y = cur.second; // 取坐标
                        // 4 个方向扩展
                        for (int k = 0; k < 4; ++k) {
                            int xx = x + nex[k][0], yy = y + nex[k][1];
                            if (xx < 0 || yy < 0 || xx >= m || yy >= n)
                                continue; // 不是 break
                            if (grid[xx][yy] == '1') { 
                                q.push({xx, yy});
                                grid[xx][yy] = '0'; // 感染法
                            }
                        }
                    }
                }
        return ans;
    }
};

AC 并查集

解释

1)dad[] 是父节点数组,这里用++一维表示二维++,每个坐标 x, y 在 dad 里表示为 x*n + y

表示 第 x 行,第 y 列
2)初始化 dad 为二维坐标的一维表示,比如,grid[3][4] == '1',假设 4 行 5 列,m=4, n=5

那么 dad[3*5 + 4] = 3*5 + 4,即 dad[19] == 19,自己是自己的祖先
3)初始化时,每次遇到 '1',count++

join() 合并两个陆地时,count--

最后 count 就是岛屿数量

比如,初始化时,count == 5,有 5 块陆地,全图遍历时,合并了 3 次,

5 - 3 = 2,岛屿数量为 2
4)每次 join() 合并完,dad[] 里的数就 = 祖先,比如 dad[0], dad[1], dad[2] 都是 0

表示第 0 行前 3 个数都是一块陆地的
5)join() 合并函数中,count--,要放在 if (xx != yy) {} 里,如果相等,说明不需要合并

6)dad 是 vector 的话,记得 resize() 大小

代码

cpp 复制代码
class Solution {
public:

    int dad[90000];
    int count = 0; // 岛屿数量
    int nex[4][2] = {{1,0},{-1,0},{0,1},{0,-1}}; // 4 个方向

    // 找领头
    int Find(int x) { 
        if (dad[x] == x)
            return x;
        else
            return dad[x] = Find(dad[x]); // 路径压缩
    }

    // 合并两个团体
    void join(int x, int y) {
        int xx = Find(x), yy = Find(y);
        if (xx != yy) {
            dad[yy] = xx;
            count--; // 合并陆地后,岛屿数量 -1
        }
    }

    int numIslands(vector<vector<char>>& grid) {

        int m = grid.size(), n = grid[0].size();

        // dad.resize(m*n); // 声明大小

        // 初始化 dad[]
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j)
                if (grid[i][j] == '1') {
                    count++;
                    dad[n*i + j] = n*i + j; // 自己的祖先是自己
                }
        // 全图遍历
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j) 
                if (grid[i][j] == '1') {
                    grid[i][j] = '0'; // 防止多次合并同一块陆地

                    // 对 4 个方向进行合并
                    for (int k = 0; k < 4; ++k) {
                        // 新的坐标
                        int x = i + nex[k][0], y = j + nex[k][1];
                        if (x < 0 || y < 0 || x >= m || y >= n)
                            continue;
                        if (grid[x][y] == '1')
                            join(n*i + j, n*x + y); // 合并两点
                    }
                }
        return count;
    }
};

🌼腐烂的橘子

994. 腐烂的橘子 - 力扣(LeetCode)

AC 多源bfs

和单源 bfs 不同的是,每个腐烂橘子,往上下左右 4 个方向扩散,所以,在 while (!q.empty && fresh > 0)

下,要多加一个 for (int size = q.size(); size >= 0; --size)
多这个一个 for 表示,单源 bfs 从一个点开始往四周扩散,而多源 bfs,从当前所有腐烂的点同时开始扩散

时空 O(mn)

cpp 复制代码
class Solution {
public:
    int orangesRotting(vector<vector<int>>& grid) {

        int nex[4][2] = {{1,0},{-1,0},{0,1},{0,-1}};

        queue<pair<int, int>> q; // 存放坐标的队列
        int fresh = 0; // 新鲜橘子数
        int m = grid.size(), n = grid[0].size();
        int min = 0;

        // 初始化:得到新鲜橘子数 && 腐烂橘子加入队列
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] == 1)
                    fresh++;
                if (grid[i][j] == 2)
                    q.push({i, j});
            }

        // 多源 bfs
        while (!q.empty() && fresh) {

            min++; 

            // 当前所有腐烂橘子,同时向 4 个方向扩散
            for (int size = q.size(); size; --size) {
                auto node = q.front(); // 队头
                q.pop();
                int x = node.first, y = node.second;
                // 腐烂上下左右(4 个方向)的橘子
                for (int i = 0; i < 4; ++i) {
                    int xx = x + nex[i][0], yy = y + nex[i][1];
                    // 越界
                    if (xx < 0 || yy < 0 || xx >= m || yy >= n)
                        continue;
                    // 新鲜的才能继续被感染
                    if (grid[xx][yy] == 1) {
                        grid[xx][yy] = 2;
                        fresh--; // 新鲜橘子 -1
                        q.push({xx, yy});
                    }
                }
            }
        }

        return fresh ? -1 : min;
    }
};

🎂课程表

207. 课程表 - 力扣(LeetCode)

前置知识

1)离散数学:自反,反自反,对称,反对称,出边,入边等概念

2)拓扑排序图解:图-拓扑排序_哔哩哔哩_bilibili
3)图的存储遍历(DFS,BFS)(👇先看视频,文章看 C++ 代码)

a. 图论基础及遍历算法 | labuladong 的算法笔记

b. 【labuladong】图论基础及遍历算法_哔哩哔哩_bilibili

++邻接表++

vector< vector<int> > graph 或 vector<int> graph[]

表示一个二维动态数组

++邻接矩阵++

bool matrix[][]

二维静态数组
邻接矩阵:判断 x, y 之间是否有边 O(1),只需 if (matrix[x][y] || martrix[y][x])

邻接表:而邻接表,需要遍历 x 或 y 右边的列表,时间 O(n)
4)拓扑排序:【labuladong】拓扑排序算法及应用_哔哩哔哩_bilibili

5)补充理解:拓扑排序详解(超详细+模板)_拓扑排序模板-CSDN博客

AC BFS

思路

BFS 模拟 拓扑排序:

选择入度 0 的点,加入序列,删除该点和所有出边,更新入度,重复上面两步,直到不存在入度为 0 的点。此时,判断是否所有点都加入序列,如果还有剩余的点,说明有环

(环的存在,导致这部分节点,入度不可能为 0)

要素

1)邻接表 :vector< vector<int> > v(++动态++二维数组)

++2)节点的入度++:vector<int> in

3)入度为 0 的节点:priority_queue<int, vector<int>, greater<int> > q(最小堆,queue 也可)

第 9,10 行,记得声明 vector 大小为 numCourses

注意,邻接表 x -> y, x -> z, x -> t,所以 x --- y, z, t(以出边为标准)

时间 O(m + n):初始化 = 边数 m(先修课程数) **+**拓扑排序 = 节点数 n

空间 O(m + n):队列 n;邻接表 m + n,表头节点 n 个,边数 m

cpp 复制代码
class Solution {
public:
    // 这里的 prerequisites 先修课程,实际就是 图的点和边
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        
        int count = numCourses; // 统计入度为 0 课程总数

        // 初始化:邻接表 和 节点的入度
        vector<vector<int>> v(numCourses); 
        vector<int> in(numCourses);
        int n = prerequisites.size();
        for (int i = 0; i < n; ++i) {
            // y -> x,y 是 x 的先修课程
            // 图:节点 y 指向 节点 x
            int x = prerequisites[i][0], y = prerequisites[i][1];
            v[y].push_back(x); // 邻接表加边
            in[x]++; // 节点 x 入度 +1
        }

        // 入度为 0 的点加入队列 q
        priority_queue<int, vector<int>, greater<int>> q; // 最小堆
        for (int i = 0; i < numCourses; ++i) 
            if (in[i] == 0) {
                q.push(i); 
                count--; // 统计是否有环
            }

        // 拓扑排序, bfs 逐层扩展
        while (!q.empty()) {
            int node = q.top(); // 取入度为 0 的最小节点 node
            q.pop();
            // 根据邻接表 v[] 在 in[] 删除该点所有出边
            for (int i = 0; i < v[node].size(); ++i) {
                int k = v[node][i]; // node -> k,k 的入度 -1
                in[k]--;
                if (in[k] == 0) { // 入度为 0
                    q.push(k);
                    count--;
                }
            }
        }
        return count ? false : true;
    }
};

🌼实现 Trie(前缀树)

208. 实现 Trie (前缀树) - 力扣(LeetCode)

解释

前缀树 == 字典树 == Trie,多叉树的一种(用于高效的前缀匹配)

cpp 复制代码
struct TrieNode {
    bool isEnd; //该结点是否是一个串的结束
    TrieNode* next[26]; //字母映射表
};

前缀树,顾名思义,前缀组成的树,上图所示,5 个单词转化为前缀树

l 是根节点,不存放任何数据

第 15,27,39 行,注意移动到下一节点,同时,Trie* node = this,这里的 node 相当于

根节点

代码

m 个字母,插入,查询,前缀匹配,时间复杂度 都是 O(m)

字典树高度 n,最坏情况,不存在前缀相同的单词,空间复杂度 O(m^n)

cpp 复制代码
class Trie {
private:
    bool isEnd; // 结束标志位
    vector<Trie*> children; // 下一前缀树节点
public:
    Trie() : isEnd(false), children(26) {}
    
    void insert(string word) {
        Trie* node = this; // 相当于根节点开始
        // 遍历单词,类似构造链表,没有该字母,就新建节点
        for (auto x : word) {
            int ch = x - 'a'; // 字母偏移量
            if (node->children[ch] == nullptr)
                node->children[ch] = new Trie();
            node = node->children[ch]; // 移动下一节点
        }
        node->isEnd = true; 
    }
    
    bool search(string word) {
        Trie* node = this; // 相当于根节点开始
        // 遍历单词
        for (auto x : word) {
            int ch = x - 'a';
            if (node->children[ch] == nullptr)
                return false;
            node = node->children[ch]; // 移动到下一字母
        }
        return node->isEnd; // 单词是否结束
    }
    
    bool startsWith(string prefix) {
        Trie* node = this; // 根节点
        // 遍历前缀
        for (auto x : prefix) {
            int ch = x - 'a';
            if (node->children[ch] == nullptr)
                return false;
            node = node->children[ch]; // 移动到下一字母
        }
        return true; // 前缀存在
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 */
相关推荐
是糖不是唐10 小时前
代码随想录算法训练营第五十三天|Day53 图论
c语言·数据结构·算法·图论
vir021 天前
好奇怪的游戏(BFS)
数据结构·c++·算法·游戏·深度优先·图论·宽度优先
一个不喜欢and不会代码的码农2 天前
李春葆《数据结构》——图相关代码
数据结构·算法·图论
是糖不是唐2 天前
代码随想录算法训练营第五十二天|Day52 图论
c语言·算法·深度优先·动态规划·图论
南宫生2 天前
力扣-Hot100-图论【算法学习day.38】
java·学习·算法·leetcode·链表·图论
张焚雪2 天前
关于图论建模的一份介绍
python·数学建模·图论
是糖不是唐2 天前
代码随想录算法训练营第五十一天|Day51 图论
c语言·数据结构·算法·深度优先·图论
汉克老师3 天前
GESP4级考试语法知识(贪心算法(六))
开发语言·数据结构·c++·算法·贪心算法·图论
是糖不是唐3 天前
代码随想录算法训练营第五十天|Day50 图论
c语言·数据结构·算法·图论
Romanticroom4 天前
图论之最小生成树计数(最小生成树的应用)
算法·图论