图论 经典例题

1 拓扑排序

对有向图的节点排序,使得对于每一条有向边 U-->V U都出现在V之前

*有环无法拓扑排序

cpp 复制代码
indegree[], nxs[];//前者表示节点 i 的入度,后者表示节点 i 指向的节点
queue = []
for i in range(n):
    if indege[i] == 0: queue.add(i)// 入度为0的节点加入队列
while queue:
    curnode = queue.popleft()
    for nx in nxs[curnode]:
        indegre[nx] -= 1;
        if indegre[nx] == 0:
            queue.add(nx);

207 课程表1

cpp 复制代码
#include <vector>
#include <deque>

using namespace std;

class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
        // 邻接表
        vector<vector<int>> nxs(numCourses, vector<int>());

        // 入度数组
        vector<int> indegree(numCourses, 0);

        // 填充入度数组和邻接表
        for (auto pre : prerequisites) {
            int a = pre[0];
            int b = pre[1];
            // a 的入度增加
            indegree[a]++;
            // 将 b 加入 a 的邻接表
            nxs[b].push_back(a);
        }

        deque<int> q;
        // 1.找到入度为0的点
        for (int i = 0; i < numCourses; i++) {
            if (indegree[i] == 0) {
                q.push_back(i);
            }
        }

        // 2.迭代,更新入度
        while (!q.empty()) {
            int curr = q.front();
            q.pop_front();
            numCourses--; // 完成一个课程

            for (int neighbor : nxs[curr]) {
                if (--indegree[neighbor] == 0) {
                    q.push_back(neighbor);
                }
            }
        }

        // 如果所有课程都完成了,则返回 true
        return numCourses == 0;
    }
};

210 课程表II

cpp 复制代码
class Solution {
public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        // 邻接表
        vector<vector<int>> nxs (numCourses, vector<int>());

        // 入度数组
        vector<int> indegree (numCourses);

        // 填充入度数组和邻接表
        for (auto pre : prerequisites) {
            int a = pre[0];
            int b = pre[1];
            
            indegree[a]++;
            nxs[b].push_back(a);
        }
        vector<int> res;
        deque<int> q;
        int index = 0;

        for (int i = 0; i < numCourses; ++i) {
            if (indegree[i] == 0) {
                q.push_back(i);
            }
        }

        while (!q.empty()) {
            int k = q.front();
            q.pop_front();
            res.push_back(k);
            index++;

            for (auto neg : nxs[k]) {
                if (--indegree[neg] == 0) {
                    q.push_back(neg); 
                }
            }
        }

        if (index != numCourses) {
            return vector<int>();
        }else{
            return res;
        }    
    }
};

310 最小高度树

依次删去度数为 1 的点

cpp 复制代码
class Solution {
public:
    vector<int> findMinHeightTrees(int n, vector<vector<int>>& edges) {
        // 找到使得树的高度最小的节点
        // 找到最中间的点
        if (n == 1) return vector<int>({0});

        // 储存度数,而非入度
        vector<int> degrees(n, 0);
        // 邻接表
        vector<vector<int>> adjacencyList(n, vector<int>());

        for (auto edge : edges) {
            int a = edge[0];
            int b = edge[1];
            degrees[a]++;
            degrees[b]++;
            adjacencyList[a].push_back(b);
            adjacencyList[b].push_back(a);
        }

        // 队列,储存入度为 1
        deque<int> q;

        // 找到度数为 1 的点
        for (int i = 0; i < n; ++i) {
            if (degrees[i] == 1) {
                q.push_back(i);
            }
        }

        vector<int> res;
        // 遍历所有边缘节点
        while (!q.empty()) {
            res.clear();
            // 层序更新,这一批点处理完之后,先看结果对不对
            int len = q.size();
            for (int i = 0; i < len; ++i) {
                int node = q.front();
                q.pop_front();
                res.push_back(node);

                // 更新两个矩阵
                for (auto nex : adjacencyList[node]) {
                    degrees[nex]--;
                    if (degrees[nex] == 1){
                        q.push_back(nex);
                    }
                }

            }
        }
        return res;
    }
};

802 逆向拓扑

找到不能进入环的点,跟它在不在环里面没关系

有向图找环

从出度为 0 的点出发,它们不可能在环中

cpp 复制代码
class Solution {
public:
    vector<int> eventualSafeNodes(vector<vector<int>>& graph) {
        int n = graph.size();
        // 出度
        vector<int> outdegree(n);

        // 逆向邻接表
        vector<vector<int>> pre_nodes(n, vector<int>());

        for (int i = 0; i < n; ++i) {
            for (auto nx : graph[i]) {
                // i --> nx
                outdegree[i]++;
                pre_nodes[nx].push_back(i);
            }
        }

        deque<int> q;
        // 找到出度为 0 的点
        for (int i = 0; i < n; ++i) {
            if (outdegree[i] == 0) {
                q.push_back(i);
            }
        }

        // 储存结果
        vector<int> res;
        while (!q.empty()) {
            int node = q.front();
            q.pop_front();
            res.push_back(node);

            for (auto nex : pre_nodes[node]) {
                outdegree[nex]--;
                if (outdegree[nex] == 0) {
                    q.push_back(nex);
                }
            }
        }

        sort(res.begin(), res.end());
        return res;
    }
};

2 并查集

查找连通块的数量

cpp 复制代码
int[] fa;

void init(int n) {
    fa = new int[n];// 初始化
    for (int i = 0; i < n; ++i) fa[i] = i;// 遍历,都指向自己
}

// 0 和 3 是亲戚,则 0 和 3建立链接
int find(int x) {
    // 如果 x 是自己的 boss 则返回 x
    // 如果不是,则 
    return x == fa[x] ? x : (fa[x] = find(fa[x]));// 是否是根节点
}

void union(int x, int y) {
    fa[find(x)] = find(y);// 连通
}
cpp 复制代码
    vector<int> fa; // 并查集的父节点数组

    // 初始化并查集,设置每个节点的父节点为自己
    // 0 -- n-1
    void init(int n) {
        fa.resize(n);
        for (int i = 0; i < n; ++i) {
            fa[i] = i;
        }
    }

    // 查找节点x所在的集合的根节点,同时进行路径压缩
    int find(int x) {
        return x == fa[x] ? x : (fa[x] = find(x));
    }

    // 合并两个节点所在的集合
    void uni(int x, int y) {
        fa[find(x)] = find(y);
    }

构建并查集的操作基本都是一样的

1.查询根节点 + 路径压缩

2.合并块

题眼一般是多个集合的合并

547 省份数量

并查集 + 求连通块的数量

cpp 复制代码
class Solution {
public:
    vector<int> fa;
    // 初始化 n 个城市的父节点为它们自己
    void init(int n) {
        fa.resize(n, 0);
        for (int i = 0; i < n; ++i) {
            fa[i] = i;
        }
    }

    // 找 x 的夫节点
    int find(int x) {
        return x == fa[x] ? x : (fa[x] = find(fa[x]));
    }

    // 合并
    void uni(int x, int y) {
        fa[find(x)] = find(y);
    }

    // 判断多少个根节点
    int findCircleNum(vector<vector<int>>& isConnected) {
        int n = isConnected.size();
        // 初始化
        init(n);

        for (int i = 0; i < n; ++i){
            for (int j = i + 1; j < n; ++j) {
                if (isConnected[i][j] == 1) uni(i, j);
            }
        }

        // 检查最后有几个点的父节点是它自己,即根的数目
        int cnt = 0;
        for (int i = 0; i < n; ++i) {
            if (fa[i] == i) {
                cnt++;
            }
        }
        return cnt;
    }
};

684 冗余连接

根据父节点的特点找冗余路径

cpp 复制代码
class Solution {
public:
    vector<int> fa;
    // 节点是 1 -- n
    void init(int n) {
        fa.resize(n + 1, 0);
        for (int i = 1; i <= n; ++i) {
            fa[i] = i;
        }
    }

    int find(int x) {
        return fa[x] == x ? x : find(fa[x]);
    }

    void uni(int x, int y) {
        fa[find(x)] = find(y);
    }


    vector<int> findRedundantConnection(vector<vector<int>>& edges) {
        int n = edges.size();
        init(n);

        for (auto edge : edges) {
            int a = edge[0];
            int b = edge[1];
            // 如果父类节点都一样,那么找到了冗余路径
            if (find(a) == find(b)) {
                return edge;
            } else {
                uni(a, b);
            }
        }

        return vector<int>();
    }
};

1319

先连通,看连通块的数量,连接 n 个块需要 n - 1 个边

cpp 复制代码
class Solution {
public:
    vector<int> fa;

    void init(int n) {
        fa.resize(n);
        for (int i = 0; i < n; ++i) {
            fa[i] = i;
        }
    }

    int find(int x) {
        return fa[x] == x ? x : (fa[x] = find(fa[x]));
    }

    void uni(int x, int y) {
        fa[find(x)] = find(y);
    }

    int makeConnected(int n, vector<vector<int>>& connections) {

        // 判断端点是否连通
        // 如果已经连通可以拆除
        // 连接 n 个连通块需要 n - 1 个边
        init(n);
        int cnt = 0;
        for (auto con : connections) {
            int a = con[0];
            int b = con[1];
            if (find(a) == find(b)) {
                cnt++;
            }else{
                uni(a, b);
            }
        }
        // 判断连通块的数量
        int num = 0; // 初始化
        for (int i = 0; i < n; ++i) {
            if (fa[i] == i) {
                num++;
            }
        }
        // 判断边是不是够用
        if (cnt >= num - 1) {
            return num - 1;
        }

        return -1;

    }
};

水域大小

变体,需要维护连通块的数量

cpp 复制代码
class Solution {
public:
    // 需要维护每个连通块的数量

    vector<int> fa;
    vector<int> cnts;// 只对根节点生效

    void init(int n) {
        
        fa.resize(n);
        cnts.resize(n);

        for (int i = 0; i < n; ++i) {
            fa[i] = i;
            cnts[i] = 1;
        }
    }

    int find(int x) {
        return fa[x] == x ? x : (fa[x] = find(fa[x]));
    }

    void uni(int x, int y) {
        int xp = fa[find(x)], yp = find(y);
        fa[xp] = yp;
        cnts[yp] += cnts[xp];

    }
    
    int getId(int x, int y, int col) {
        return x * col + y;
    }
    vector<int> pondSizes(vector<vector<int>>& land) {
        // x * col + y

        // 表示八个方向的方向数组
        vector<vector<int>> dirs = {
            {0, 1},   // 向右
            {0, -1},  // 向左
            {1, 0},   // 向下
            {-1, 0},  // 向上
            {1, 1},   // 右下
            {1, -1},  // 右上
            {-1, 1},  // 左下
            {-1, -1}  // 左上
        };

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

        init(n * m);
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                if (land[i][j] == 0) {
                    for (auto dir : dirs) {
                        // 遍历八个方向
                        int nx = i + dir[0];
                        int ny = j + dir[1];
                        // 如果方向不越界 且 为水域
                        if (nx < 0 || ny < 0 || nx >= n || ny >= m || land[nx][ny] != 0) {
                            continue;
                        }
                        else{
                            int id1 = getId(i, j, m);
                            int id2 = getId(nx, ny, m);
                            if (find(id1) != find(id2)) {
                                uni(id1, id2);
                            }
                        }
                    }
                }
            }
        }

        vector<int> res;

        for (int i = 0; i < n; ++i) {
            for (int j = 0; j < m; ++j) {
                int id = getId(i, j, m);
                if (fa[id] == id && land[i][j] == 0) {
                    res.push_back(cnts[id]);
                }
            }
        }
        sort(res.begin(), res.end());
        return res;

    }
};

721 账户合并(字符串)

建立映射 [0, a, b] 其中 0 代表人名,a 代表邮箱地址

最后还要倒过来输出

cpp 复制代码
#include <vector>
#include <string>
#include <map>
#include <algorithm>

class Solution {
public:
    // 并查集代码
    vector<int> fa; // 并查集的父节点数组

    // 初始化并查集,设置每个节点的父节点为自己
    void init(int n) {
        fa.resize(n);
        for (int i = 0; i < n; ++i) {
            fa[i] = i;
        }
    }

    // 查找节点x所在的集合的根节点,同时进行路径压缩
    int find(int x) {
        return x == fa[x] ? x : find(fa[x]);
    }

    // 合并两个节点所在的集合
    void uni(int x, int y) {
        fa[find(x)] = find(y);
    }

    vector<vector<string>> accountsMerge(vector<vector<string>>& accounts) {
        int n = accounts.size(); // 账号的数量
        init(n); // 初始化并查集

        // 邮箱到账号ID的映射
        map<string, int> email_accId;

        // 构建并查集,合并具有相同邮箱地址的账号
        for (int accId = 0; accId < n; accId++) {
            int m = accounts[accId].size(); // 当前账号的邮箱数量
            for (int i = 1; i < m; ++i) {
                string email = accounts[accId][i]; // 获取邮箱地址
                // 如果邮箱地址不存在,建立映射关系
                // 
                if (email_accId.find(email) == email_accId.end()) {
                    email_accId[email] = accId; 
                } else {
                // 当前id和之前id合并
                    uni(accId, email_accId[email]); // 如果邮箱地址已存在,合并账号
                }
            }
        }

        // 账号ID到邮箱的映射
        map<int, vector<string>> accId_emails;

        // 遍历所有邮箱账号,将它们归类到相同的账号ID下
        for (auto& pair : email_accId) {
            string email = pair.first;
            int accId = find(pair.second); // 获取根账号ID
            accId_emails[accId].push_back(email);
        }

        // 构建最终的合并后的账户列表
        vector<vector<string>> mergedAccounts;

        for (auto& pair : accId_emails) {
            int accId = pair.first;
            vector<string> emails = pair.second;

            // 将账号ID添加到前面
            vector<string> mergedAccount = {accounts[accId][0]};
            sort(emails.begin(), emails.end()); // 对邮箱地址排序
            mergedAccount.insert(mergedAccount.end(), emails.begin(), emails.end());

            mergedAccounts.push_back(mergedAccount);
        }

        return mergedAccounts; // 返回合并后的账户列表
    }
};
相关推荐
daily_23335 分钟前
数据结构——小小二叉树第三幕(链式结构的小拓展,二叉树的创建,深入理解二叉树的遍历)超详细!!!
数据结构·c++·算法
浦东新村轱天乐27 分钟前
神经网络反向传播算法公式推导
神经网络·算法·机器学习
SUN_Gyq38 分钟前
什么是 C++ 中的模板特化和偏特化? 如何进行模板特化和偏特化?
开发语言·c++·算法
码上一元42 分钟前
【百日算法计划】:每日一题,见证成长(026)
算法
愿天垂怜1 小时前
【C++】C++11引入的新特性(1)
java·c语言·数据结构·c++·算法·rust·哈希算法
淡写青春2091 小时前
计算机基础---进程间通信和线程间通信的方式
java·开发语言·数据结构
kitesxian1 小时前
Leetcode200. 岛屿数量(HOT100)
算法·深度优先
特种加菲猫1 小时前
初阶数据结构之栈的实现
开发语言·数据结构·笔记
LNTON羚通1 小时前
算法定制LiteAIServer视频智能分析平台工业排污检测算法智控环保监管
算法·目标检测·音视频·监控·视频监控
好好学习O(∩_∩)O1 小时前
11-23刷题记录
算法·leetcode·职场和发展