当前位置: 首页 > news >正文

BFS 图论【各种题型+对应LeetCode习题练习】

目录

BFS & 图论

BFS题型分类

层次遍历

LeetCode 102 二叉树的层序遍历

LeetCode 103 二叉树的锯齿形层序遍历 

LeetCode 994 腐烂的橘子

最短路径问题

无权图最短路 BFS(含网格)

LeetCode 1091 二进制矩阵中的最短路径

LeetCode 752 打开转盘锁

LeetCode 127 单词接龙

有权正边 Dijkstra(邻接表 + 小根堆)

LeetCode743 网络延迟时间

LeetCode1631 最小体力消耗路径

0-1 BFS(边权只为 0/1)

LeetCode1368 使网格图至少有一条有效路径的最小代价

图的连通性与拓扑排序

LeetCode547 省份数量

LeetCode323 无向图中连通分量的数目

LeetCode 207 课程表

LeetCode 210 课程表 II


BFS & 图论

广度优先搜索(BFS):
基本思想:从一个起点开始,层层扩展,逐层遍历所有节点,适用于最短路径问题。
队列:BFS需要队列来记录当前遍历的节点,确保按层级遍历。
图的表示:图可以用邻接矩阵或邻接表来表示。

图的遍历:
无向图 vs 有向图:有向图边是有方向的,遍历时要注意方向。
连通性判断:用BFS可以判断一个图是否连通。

最短路径问题:
单源最短路径:从一个节点出发,找到到其他所有节点的最短路径。
广度优先搜索:BFS能有效解决无权图的单源最短路径问题。

BFS题型分类

层次遍历(BFS遍历)
用于遍历每一层的节点,通常是树或图的遍历。
重点是理解如何逐层遍历,层与层之间的关系。
 

最短路径问题
解决从一个点到其他点的最短路径问题,尤其是无权图(即每条边的权重都是1)
重点是如何用BFS遍历图,并计算最短路径
 

图的连通性与拓扑排序
连通性:判断一个图是否是连通图,是否可以从一个节点到达其他所有节点。
拓扑排序:有向图中的排序,判断是否有环,如何处理图中的依赖关系。
重点是理解图中的依赖关系,如何用BFS来处理这些依赖。
 

图的克隆与复制
重点是如何复制图中的节点和边,使用BFS来遍历并创建新的图结构。
 

层次遍历

LeetCode 102 二叉树的层序遍历
vector<vector<int>> levelOrder(TreeNode* root) {vector<vector<int>> ans;if (!root) return ans;queue<TreeNode*> q;q.push(root);while (!q.empty()) {int sz = q.size();                // 当前层节点数vector<int> level;for (int i = 0; i < sz; ++i) {TreeNode* cur = q.front(); q.pop();level.push_back(cur->val);if (cur->left)  q.push(cur->left);if (cur->right) q.push(cur->right);}ans.push_back(move(level));}return ans;
}

LeetCode 103 二叉树的锯齿形层序遍历 
vector<vector<int>> zigzagLevelOrder(TreeNode* root) {vector<vector<int>> ans;if (!root) return ans;queue<TreeNode*> q;q.push(root);bool leftToRight = true;while (!q.empty()) {int sz = q.size();vector<int> level(sz);for (int i = 0; i < sz; ++i) {TreeNode* node = q.front(); q.pop();int idx = leftToRight ? i : (sz - 1 - i);level[idx] = node->val;if (node->left)  q.push(node->left);if (node->right) q.push(node->right);}ans.push_back(level);leftToRight = !leftToRight; // 方向交替}return ans;
}

LeetCode 994 腐烂的橘子

思路:
多源 BFS:把所有腐烂橘子当作同时出发的起点一起入队,按“层”扩散。
层数=时间:每扩散一层,分钟数 +1。
判定:BFS 结束后若仍有新鲜橘子,返回 -1;若一开始就没有新鲜橘子,返回 0。

关键细节
入队即标记:当把新鲜橘子入队时,立刻改为 2(防止重复入队)
按层推进计时:用 sz = q.size() 固定当前层大小;本层处理完且发生感染,就 minutes++ 
统计剩余新鲜数:fresh 计数,感染一个就 fresh--。收尾判断是否为 0。

int orangesRotting(vector<vector<int>>& grid) {int n = grid.size(), m = grid[0].size();queue<pair<int,int>> q;int fresh = 0;// 1) 初始化:统计新鲜橘子并将所有腐烂橘子入队(多源)for (int i = 0; i < n; ++i) {for (int j = 0; j < m; ++j) {if (grid[i][j] == 2) q.push({i, j});else if (grid[i][j] == 1) ++fresh;}}if (fresh == 0) return 0; // 没有新鲜橘子,时间为 0int minutes = 0;int dx[4] = {1, -1, 0, 0};int dy[4] = {0, 0, 1, -1};// 2) 按层扩散while (!q.empty()) {int sz = q.size();bool progressed = false; // 本层是否发生了新的感染while (sz--) {pair<int, int> cur = q.front();int x = cur.first; int y = cur.second; q.pop();for (int k = 0; k < 4; ++k) {int nx = x + dx[k], ny = y + dy[k];if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;if (grid[nx][ny] != 1) continue; // 不是新鲜就不感染grid[nx][ny] = 2;               // 入队即标记为腐烂q.push({nx, ny});--fresh;progressed = true;}}if (progressed) ++minutes; // 只有这一层确实扩散了,时间才+1}return fresh == 0 ? minutes : -1;
}

if (progressed) ++minutes; 的作用是用来只有最后一层队列不加时间,避免多加一分钟

最短路径问题

无权图最短路 BFS(含网格)

LeetCode 1091 二进制矩阵中的最短路径

无权图最短路使用BFS作解:每走一步代价相同(=1),BFS 按层扩展,首次到达终点即为最短
8 方向:与常见 4 方向 BFS 不同,这题要把 8 个方向都列上
起点/终点可用性:若 grid[0][0]==1 或 grid[n-1][n-1]==1,直接 -1
距离从 1 开始:题目定义路径长度包含起点,所以 dist[0][0]=1

int shortestPathBinaryMatrix(vector<vector<int>>& grid) {int n = grid.size();if (grid[0][0] == 1 || grid[n-1][n-1] == 1) return -1;vector<vector<int>> dist(n, vector<int>(n, -1));queue<pair<int,int>> q;dist[0][0] = 1; // 起点距离按题意从1开始计步q.push({0,0});// 方向顺序:上、下、左、右、右上、右下、左上、左下int dx[8] = {-1, 1, 0, 0, -1, 1, -1, 1};int dy[8] = {0, 0, -1, 1, 1, 1, -1, -1};while(!q.empty()){pair<int, int> cur = q.front();int x = cur.first; int y = cur.second;  q.pop();if (x==n-1 && y==n-1) return dist[x][y];for(int k=0;k<8;k++){int nx=x+dx[k], ny=y+dy[k];if(nx<0||nx>=n||ny<0||ny>=n) continue;if(grid[nx][ny]==1 || dist[nx][ny]!=-1) continue;dist[nx][ny]=dist[x][y]+1;q.push({nx,ny});}}return -1;
}

LeetCode 752 打开转盘锁

题目要最短步数,每一步代价相同,拨一格算 1 步,没有不同权重
状态有限且可枚举,题目最多 10⁴ 个状态:"0000"~"9999"
一步能定义清晰的邻居,对任意状态,改任意一位 ±1,共 8 个邻居
只求最短距离,不需要所有路径或带权代价
所以是无权图最短路

int openLock(vector<string>& deadends, string target) {unordered_set<string> dead(deadends.begin(), deadends.end());if (dead.count("0000")) return -1;if (target == "0000") return 0;queue<string> q;unordered_set<string> vis; // 已探索过的状态 避免重复走q.push("0000");vis.insert("0000");int steps = 0;while (!q.empty()) {int sz = q.size();while (sz--) {string cur = q.front(); q.pop();if (cur == target) return steps;for (int i = 0; i < 4; ++i) { // i=0~3:对应4个拨盘string nxt1 = cur, nxt2 = cur;// +1nxt1[i] = (cur[i] == '9' ? '0' : char(cur[i] + 1));// -1nxt2[i] = (cur[i] == '0' ? '9' : char(cur[i] - 1));if (!dead.count(nxt1) && !vis.count(nxt1)) {vis.insert(nxt1);q.push(nxt1);}if (!dead.count(nxt2) && !vis.count(nxt2)) {vis.insert(nxt2);q.push(nxt2);}}}++steps; // 这一层处理完,步数+1}return -1;
}

上面的解法是单向BFS,数据量大的时候也可使用双向BFS求解,速度更快

双向BFS解法如下:

int openLock(vector<string>& deadends, string target) {unordered_set<string> dead(deadends.begin(), deadends.end());if (dead.count("0000")) return -1;if (target == "0000") return 0;unordered_set<string> beginSet, endSet, vis;beginSet.insert("0000");endSet.insert(target);vis.insert("0000"); // 统一 visited(两端共用)int steps = 0; // 从起点出发尚未移动while (!beginSet.empty() && !endSet.empty()) {// 始终扩展更小的一侧if (beginSet.size() > endSet.size()) beginSet.swap(endSet);unordered_set<string> next; // 当前层的下一层状态for (auto it = beginSet.begin(); it != beginSet.end(); ++it) {const string cur = *it;for (int i = 0; i < 4; ++i) {// +1string a = cur;a[i] = (a[i] == '9' ? '0' : char(a[i] + 1));if (!dead.count(a)) {if (endSet.count(a)) return steps + 1;  // 邻居命中对侧if (!vis.count(a)) { vis.insert(a); next.insert(a); }}// -1string b = cur;b[i] = (b[i] == '0' ? '9' : char(b[i] - 1));if (!dead.count(b)) {if (endSet.count(b)) return steps + 1;  // 邻居命中对侧if (!vis.count(b)) { vis.insert(b); next.insert(b); }}}}beginSet.swap(next);++steps; // 本轮扩展完成,步数+1}return -1;
}

for (auto it = beginSet.begin(); it != beginSet.end(); ++it) {
    string cur = *it;
    // 对 cur 做处理
}
等价于
for (auto &cur : beginSet) {
    // 对 cur 做处理
}
 

LeetCode 127 单词接龙

为什么用 BFS?
每一步的“代价”都相同(一步),要最少步数,所以是无权图最短路 = BFS
把“单词”看做图中的“点”,相差一个字母的两个单词之间连“边”

单向BFS解法如下:

int ladderLength(string beginWord, string endWord, vector<string>& wordList) {unordered_set<string> dict(wordList.begin(), wordList.end());if (!dict.count(endWord)) return 0;   queue<string> q;q.push(beginWord);int steps = 1; while (!q.empty()) {int sz = q.size();while (sz--) {string cur = q.front(); q.pop();for (int i = 0; i < cur.size(); ++i) {char old = cur[i];for (char c = 'a'; c <= 'z'; ++c) {if (c == old) continue;cur[i] = c;if (cur == endWord) return steps + 1; if (dict.count(cur)) {q.push(cur);dict.erase(cur); // 避免重复加队}}cur[i] = old; // 还原}}++steps; // 扩完一层}return 0;
}

dict.erase(cur) 的本质是标记 cur 已被探索,且用的是最短路径

双向BFS解法如下:

int ladderLength(string beginWord, string endWord, vector<string>& wordList) {unordered_set<string> dict(wordList.begin(), wordList.end());if (!dict.count(endWord)) return 0;unordered_set<string> beginSet, endSet;beginSet.insert(beginWord);endSet.insert(endWord);int steps = 1; // beginWord 本身算 1while (!beginSet.empty() && !endSet.empty()) {// 始终扩展更小的一端if (beginSet.size() > endSet.size()) beginSet.swap(endSet);unordered_set<string> next;for (auto cur : beginSet) {for (int i = 0; i < cur.size(); ++i) {char old = cur[i];for (char c = 'a'; c <= 'z'; ++c) {if (c == old) continue;cur[i] = c;if (endSet.count(cur)) return steps + 1; // 两端相遇if (dict.count(cur)) {next.insert(cur);dict.erase(cur); // 直接在 dict 中删掉当 visited}}cur[i] = old;}}beginSet.swap(next);++steps;}return 0;
}

注意代码中是for (auto cur : beginSet) 而不是 for (auto &cur : beginSet)
因为后面直接对cur进行了修改,所以不能加 &

有权正边 Dijkstra(邻接表 + 小根堆)

LeetCode743 网络延迟时间
int networkDelayTime(vector<vector<int>>& times, int n, int k) {// 建图,邻接表 g[u] = { {v, w}, ... }vector<vector<pair<int,int>>> g(n + 1);for (int i = 0; i < times.size(); ++i) {int u = times[i][0];  // 起点节点int v = times[i][1];  // 终点节点int w = times[i][2];  // 延迟时间g[u].push_back(make_pair(v, w));  // 把边加入邻接表}const int INF = 1e9;vector<int> dist(n + 1, INF);dist[k] = 0;// 小根堆的排序规则:按距离从小到大(greater<pair<int,int>> 表示升序)priority_queue<pair<int,int>, vector<pair<int,int>>, greater<pair<int,int>>> pq;pq.push(make_pair(0, k));  // 先把起点k加入堆(距离0,节点k)while (!pq.empty()) {pair<int,int> top = pq.top(); pq.pop();int d = top.first; int u = top.second;for (int i = 0; i < g[u].size(); ++i) {int v = g[u][i].first;  // 邻居节点vint w = g[u][i].second; // 从u到v的延迟时间if (dist[v] > d + w) {dist[v] = d + w;  // 更新k到v的最短距离pq.push(make_pair(dist[v], v));}}}int ans = 0;for (int i = 1; i <= n; ++i) {if (dist[i] == INF) return -1; // 有不可达节点if (dist[i] > ans) ans = dist[i];}return ans;
}

n个元素(索引0到n-1)
vector<vector<pair<int,int>>> g(n);  // 大小 = n

n+1个元素(索引0到n)
vector<vector<pair<int,int>>> g(n + 1);  // 大小 = n+1

题目中节点编号是1,2,3,...,n
使用 n+1 时,g[1] 对应节点1,g[2] 对应节点2,...,g[n] 对应节点n

LeetCode1631 最小体力消耗路径
int minimumEffortPath(vector<vector<int>>& heights) {int n = heights.size(), m = heights[0].size();if (n == 1 && m == 1) return 0;// 距离矩阵dist[x][y]表示从(0,0)到(x,y)的最小总消耗const int INF = 1e9;  vector<vector<int>> dist(n, vector<int>(m, INF));// {权重, {x坐标, y坐标}}typedef pair<int, pair<int, int>> State;priority_queue<State, vector<State>, greater<State>> pq;dist[0][0] = 0;pq.push(make_pair(0, make_pair(0, 0)));const int dir[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};while (!pq.empty()) {State top = pq.top();pq.pop();int d = top.first; int x = top.second.first, y = top.second.second;if (x == n-1 && y == m-1) return d;for (int k = 0; k < 4; ++k) {int nx = x + dir[k][0];int ny = y + dir[k][1];if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;// 计算从当前格子到邻居的单次消耗(高度差绝对值)int w = abs(heights[nx][ny] - heights[x][y]);// 新路径的总消耗int nd = max(d, w);if (nd < dist[nx][ny]) {dist[nx][ny] = nd;pq.push(make_pair(nd, make_pair(nx, ny)));}}   }return 0;
}

0-1 BFS(边权只为 0/1)

LeetCode1368 使网格图至少有一条有效路径的最小代价
int minCost(vector<vector<int>>& grid) {int n = grid.size();int m = grid[0].size();// 因为题目设定使得这里方向只能是右左下上const int dx[4] = {0, 0, 1, -1}; const int dy[4] = {1, -1, 0, 0};  const int INF = 1e9;vector<vector<int>> dist(n, vector<int>(m, INF)); // 存储从起点到每个点的最小代价deque<pair<int, int>> dq;  // 双端队列,用于0-1 BFSdist[0][0] = 0;  // 起点到自身的代价为0dq.push_front(make_pair(0, 0));  // 将起点加入队列前端// 开始BFS遍历while (!dq.empty()) {// 从队列前端取出当前节点pair<int, int> cur = dq.front(); dq.pop_front();int x = cur.first, y = cur.second;int d = dist[x][y];  // 当前节点的最小代价// 当前格子的推荐方向(将题目中的1..4转换为0..3的索引)int prefer = grid[x][y] - 1;  // 遍历四个可能的方向for (int k = 0; k < 4; ++k) {int nx = x + dx[k], ny = y + dy[k];  // 计算相邻节点坐标// 检查边界if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;// 计算代价// 如果移动方向与推荐方向相同,代价为0 否则代价为1int w = (k == prefer) ? 0 : 1;int nd = d + w;  // 新路径的总代价// 如果找到更小的代价,更新距离并调整队列if (nd < dist[nx][ny]) {dist[nx][ny] = nd;  // 更新最小代价// 根据代价决定插入队列的位置// 代价为0 插入队列前端(优先处理)// 代价为1 插入队列后端if (w == 0) dq.push_front(make_pair(nx, ny));else dq.push_back(make_pair(nx, ny));}}}// 返回到达右下角的最小代价return dist[n-1][m-1];
}
对比点普通 BFS0-1 BFS
队列类型queue(单端)deque(双端)
入队规则下一层节点统一 push_back权 = 0 → push_front
权 = 1 → push_back
距离更新dist[v] = dist[u] + 1dist[v] = dist[u] + w(w ∈ {0,1})
出队顺序FIFO(层序)权 = 0 的点优先出队(相当于 Dijkstra)
是否需要 dist 数组可选(层数即步数)必须,用于判更优路径

图的连通性与拓扑排序

1. 连通性

无向图
连通:任意两点可达
连通分量:极大连通子图(常用来数有多少块)

有向图
强连通:u 可达 v 且 v 可达 u
强连通分量(SCC):极大强连通子图(拓扑通常在 DAG 上做,SCC 用于把有向图缩成 DAG)

SCC用于把有向图缩成DAG是SCC最重要的应用之一。

步骤:

1. 找出一个有向图中所有的强连通分量(SCC)

2. 把每一个SCC看作一个单一的超级节点(或称“缩点”)

3. 如果原图中存在从一个SCC中的某个节点到另一个SCC中的某个节点的边,那么就在这两个超级节点之间连一条有向边

结果:这样形成的新图,一定是一个有向无环图。

为什么?
因为如果新的图里存在一个环,那么这个环上的所有超级节点(即原图的SCC)就可以通过双向路径连接起来,它们就应该属于同一个更大的SCC,这与我们最初“极大”的定义矛盾。所以缩点后的图不可能有环,即是一个DAG。

拓扑通常在DAG上做
拓扑排序只能应用于有向无环图。很多问题在一般的带环有向图上很难解决,但通过SCC缩点技术可以先将原图转化为DAG,然后在DAG这个更简单的结构上进行拓扑排序和动态规划等操作,从而解决问题。

2. 拓扑排序

何时使用
有向无环图(DAG)上的线性序,使得每条边 u → v , u 在 v 之前
应用:课程安排(依赖关系)、任务调度、编译顺序等

是否有环:
Kahn 算法:若最终取出的点少于 n,有环
DFS:出现回边/递归栈二次进入,有环

LeetCode547 省份数量

解法一:并查集 DSU

思路:把相连的城市 i、j 合并到同一集合,最后统计不同的根节点个数。为减少重复合并,可以只遍历上三角 j=i+1..n-1


// 并查集 (Disjoint Set Union) 数据结构
struct DSU {vector<int> p, r; // p: parent数组,存储每个节点的父节点; r: rank数组,用于按秩合并优化DSU(int n): p(n), r(n,0) { // 初始化:每个节点都是自己的父节点,形成n个独立的集合for (int i=0;i<n;++i) p[i]=i; }// 查找操作:找到节点x所在集合的根节点(代表元)// 包含路径压缩优化:在查找过程中将路径上的节点直接连接到根节点int find(int x){ return p[x]==x ? x : p[x]=find(p[x]); }// 合并操作:将节点a和节点b所在的集合合并bool unite(int a, int b){a = find(a); // 找到a的根节点b = find(b); // 找到b的根节点if (a == b) return false; // 如果已经在同一集合,不需要合并// 按秩合并优化:将秩较小的树合并到秩较大的树下if (r[a] < r[b]) p[a] = b;        // 将a的根节点指向b的根节点else if (r[a] > r[b]) p[b] = a;        // 将b的根节点指向a的根节点else { p[b] = a;        // 秩相等时,任意合并,但需要增加秩r[a]++; }return true; // 合并成功}
};class Solution {
public:// 计算省份数量(连通分量个数)// isConnected: 邻接矩阵表示的图,isConnected[i][j]=1表示城市i和j直接相连int findCircleNum(vector<vector<int>>& isConnected) {int n = isConnected.size(); // 城市总数DSU dsu(n); // 初始化并查集,每个城市初始独立// 遍历所有城市对,合并相连的城市for (int i = 0; i < n; ++i) {for (int j = i + 1; j < n; ++j) {// 如果城市i和j直接相连,将它们合并到同一集合if (isConnected[i][j] == 1) dsu.unite(i, j);}}// 统计连通分量数量:根节点的数量就是省份数量int cnt = 0;for (int i = 0; i < n; ++i) // 如果节点的父节点是自己,说明它是根节点,代表一个连通分量if (dsu.find(i) == i) ++cnt;return cnt;}
};

解法二:DFS

思路:对每个未访问的城市 i 开一趟 DFS,把与它直接或间接相连的城市全标记为已访问;开了几次,就有几个省份

// 计算省份数量(连通分量个数)
int findCircleNum(vector<vector<int>>& isConnected) {int n = isConnected.size();  // 城市总数vector<char> vis(n, 0);      // 访问标记数组,0=未访问,1=已访问int provinces = 0;           // 省份计数// 遍历所有城市for (int i = 0; i < n; ++i) {// 如果当前城市未被访问,说明发现一个新的省份if (!vis[i]) {++provinces;     // 省份数量+1dfs(i, isConnected, vis, n);  // 从当前城市开始DFS,标记整个省份}}return provinces;
}void dfs(int u,vector<vector<int>>& g, vector<char>& vis, int n) {vis[u] = 1; for (int v = 0; v < n; ++v) {// 如果城市v未被访问,且与城市u直接相连if (!vis[v] && g[u][v] == 1) {dfs(v, g, vis, n);   // 递归访问城市v}}
}

解法三:BFS

思路:
把每个城市当作节点;isConnected[i][j]==1 表示无向边
从每个未访问的城市 i 出发做一趟 BFS,能到的都标记已访问;开启了几次 BFS,就有几个省份

int findCircleNum(vector<vector<int>>& isConnected) {int n = isConnected.size();           // 城市总数vector<char> vis(n, 0);               // 访问标记数组,0=未访问,1=已访问queue<int> q;                       int provinces = 0;                    // 遍历所有城市作为起点for (int s = 0; s < n; ++s) {if (vis[s]) continue;// 发现一个新的连通分量(省份)++provinces;vis[s] = 1;                      q.push(s);                        // BFS遍历当前连通分量中的所有城市while (!q.empty()) {int u = q.front(); q.pop();   // 遍历所有其他城市,寻找与u相连的未访问城市for (int v = 0; v < n; ++v) {// 如果城市v未被访问,且与城市u直接相连if (!vis[v] && isConnected[u][v] == 1) {vis[v] = 1;           q.push(v);            }}}}return provinces;
}

LeetCode323 无向图中连通分量的数目

解法一:并查集

// 并查集(Disjoint Set Union)数据结构
struct DSU {vector<int> p, r;  // p: parent数组,存储每个节点的父节点;r: rank数组,存储树的深度(用于按秩合并)// 构造函数:初始化n个元素的并查集DSU(int n): p(n), r(n, 0) { // 初始化每个元素的父节点为自己,形成n个独立的集合for (int i = 0; i < n; ++i) p[i] = i; }       // 查找操作:找到元素x所在集合的根节点(带路径压缩优化)int find(int x){   return p[x] == x ? x : p[x] = find(p[x]); }// 合并操作:将元素a和b所在的集合合并bool unite(int a, int b){a = find(a);  b = find(b);  if (a == b) return false;// 按秩合并if (r[a] < r[b]) {p[a] = b;           // 将a的根节点指向b的根节点} else if (r[a] > r[b]) {p[b] = a;           // 将b的根节点指向a的根节点} else {p[b] = a;           // 深度相等时,任意合并,但深度要+1r[a]++;             // 因为合并后树的深度增加了}return true;            // 合并成功}
};class Solution {
public:// 计算无向图中连通分量的数量   int countComponents(int n, vector<vector<int>>& edges) {DSU dsu(n);         // 初始化包含n个节点的并查集int comps = n;      // 初始时每个节点都是一个独立的连通分量// 遍历所有的边for (int i = 0; i < edges.size(); ++i) {int u = edges[i][0];  // 边的起点int v = edges[i][1];  // 边的终点// 如果成功合并了两个节点(即它们原本不在同一个连通分量中)if (dsu.unite(u, v)) {--comps;    // 连通分量数量减1}// 如果合并失败,说明两个节点已经在同一个连通分量中,comps不变}return comps;       // 返回最终的连通分量数量}
};

解法二:BFS

建邻接表,逐个未访问节点启动一次 BFS;启动次数就是分量数。

int countComponents(int n, vector<vector<int>>& edges) {vector<vector<int> > g(n);for (int i = 0; i < edges.size(); ++i) {int u = edges[i][0], v = edges[i][1];g[u].push_back(v);g[v].push_back(u);}vector<char> vis(n, 0);queue<int> q;int comps = 0;for (int s = 0; s < n; ++s) if (!vis[s]) {++comps;vis[s] = 1; q.push(s);while (!q.empty()) {int u = q.front(); q.pop();for (int i = 0; i < g[u].size(); ++i) {int v = g[u][i];if (!vis[v]) { vis[v] = 1; q.push(v); }}}}return comps;
}

假设 n = 5, edges = [[0,1],[1,2],[3,4]]
构建邻接表如下
g[0]: [1]
g[1]: [0,2]  
g[2]: [1]
g[3]: [4]
g[4]: [3]

即使edges = [[3,4],[0,1],[1,2]]  构建的邻接表还是上面这样

解法三:DFS

若递归深度很大,可选用之前的 BFS/DSU 作答

int countComponents(int n, vector<vector<int>>& edges) {vector<vector<int> > g(n);for (int i = 0; i < edges.size(); ++i) {int u = edges[i][0], v = edges[i][1];g[u].push_back(v);g[v].push_back(u);}vector<char> vis(n, 0);int comps = 0;for (int i = 0; i < n; ++i) {if (!vis[i]) {++comps;dfs(i, g, vis);}}return comps;
}void dfs(int u, const vector<vector<int> >& g, vector<char>& vis) {vis[u] = 1;for (int i = 0; i < g[u].size(); ++i) {int v = g[u][i];if (!vis[v]) dfs(v, g, vis);}
}

countComponents

├── dfs(0)
│   │
│   └── dfs(1)
│       │
│       └── dfs(2)

├── (主循环继续)

└── dfs(3)
    │
    └── dfs(4)

LeetCode 207 课程表

解法:Kahn 拓扑排序

拓扑排序是对有向无环图(DAG)的所有顶点进行线性排序,使得对于任何从顶点u到顶点v的有向边(u,v),在任意排序中u都出现在v之前。

Kahn算法:总是优先处理当前入度为0的节点,这些节点代表没有未处理前置依赖的任务。

bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {// 构建邻接表表示的图,并记录每个节点的入度// g[i] 存储所有以课程i为先修课程的后续课程vector<vector<int>> g(numCourses);// indeg[i] 表示课程i的先修课程数量(入度)vector<int> indeg(numCourses, 0);// 遍历先决条件,构建图结构for (int i = 0; i < prerequisites.size(); ++i) {int a = prerequisites[i][0];  // 目标课程int b = prerequisites[i][1];  // 先修课程// 建立边:b → a(b是a的先修课程)g[b].push_back(a);// 目标课程a的入度加1indeg[a]++;}// 使用队列进行拓扑排序(BFS)queue<int> q;// 将所有入度为0的课程(没有先修要求的课程)加入队列for (int i = 0; i < numCourses; ++i)if (indeg[i] == 0) q.push(i);// 记录已修完的课程数量int taken = 0;// BFS遍历:每次取出入度为0的课程while (!q.empty()) {int u = q.front(); q.pop();++taken;  // 修完一门课程// 遍历当前课程的所有后续课程for (int i = 0; i < g[u].size(); ++i) {int v = g[u][i];  // 后续课程v// 将后续课程v的入度减1(因为先修课程u已修完)--indeg[v];if (indeg[v] == 0) q.push(v);  // 如果v的所有先修课程都已修完,加入队列      }}// 如果所有课程都能修完(无环),返回true;否则返回falsereturn taken == numCourses;
}

LeetCode 210 课程表 II
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {vector<vector<int> > g(numCourses);vector<int> indeg(numCourses, 0);for (int i = 0; i < prerequisites.size(); ++i) {int a = prerequisites[i][0];  // 目标课程int b = prerequisites[i][1];  // 先修课程g[b].push_back(a);            // 添加边 b->aindeg[a]++;                   // 课程a的入度+1}queue<int> q;for (int i = 0; i < numCourses; ++i)if (indeg[i] == 0) q.push(i);// 存储拓扑排序结果(学习顺序)vector<int> order; order.reserve(numCourses);  // 预分配空间提高效率while (!q.empty()) {int u = q.front(); q.pop();order.push_back(u);  // 将当前课程加入学习顺序for (int i = 0; i < g[u].size(); ++i) {int v = g[u][i];  // 后续课程v// 减少后续课程的入度(因为先修课程u已完成)if (--indeg[v] == 0) q.push(v);  }}// 如果排序结果数量不等于课程总数,说明有环if (order.size() != numCourses) return vector<int>(); // 有环,无法完成所有课程return order;
}

http://www.dtcms.com/a/588905.html

相关文章:

  • 威联通怎么建设网站人类命运共同体
  • 【ElasticSearch实用篇-05】基于脚本script打分
  • 微前端框架选型
  • Java 17 密封类(Sealed Classes)实战:从类型安全到架构解耦的范式升级
  • 保健品网站模板wordpress简约主题分享
  • 前端低代码平台
  • 八字排盘原理
  • 40.交叉编译
  • RT-Thread Studio开发环境搭建
  • jdbc基础(连接篇)
  • 免费云服务器网站有哪些为什么手机进网站乱码
  • 从入门到精通 LlamaIndex RAG 应用开发
  • 算法基础篇:(五)基础算法之差分——以“空间”换“时间”
  • 潍坊中企动力做的网站怎么样wordpress显示摘要
  • leetcode1771.由子序列构造的最长回文串长度
  • 【JUnit实战3_31】第十九章:基于 JUnit 5 + Hibernate + Spring 的数据库单元测试
  • 双11释放新增量,淘宝闪购激活近场潜力
  • MySQL快速入门——内置函数
  • 中小网站建设都有哪些网易企业邮箱申请
  • 预测电流控制在光伏逆变器中的低延迟实现:华为FPGA加速方案与并网稳定性验证
  • C语言--文件读写函数的使用
  • 网站的网站维护的原因可以做公众号的网站
  • 使用waitpid回收多个子进程
  • leetcode1547.切棍子的最小成本
  • ThinkPHP8学习篇(十一):模型关联(一)
  • 深入理解Ribbon的架构原理
  • 力扣(LeetCode)100题:3.无重复字符的最长子串
  • 前端接口安全与性能优化实战
  • ssh网站怎么做wordpress搬家_后台错乱
  • LangChain V1.0 Messages 详细指南