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); // 大小 = nn+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];
}
| 对比点 | 普通 BFS | 0-1 BFS |
|---|---|---|
| 队列类型 | queue(单端) | deque(双端) |
| 入队规则 | 下一层节点统一 push_back | 权 = 0 → push_front 权 = 1 → push_back |
| 距离更新 | dist[v] = dist[u] + 1 | dist[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;
}
