目录
[BFS & 图论](#BFS & 图论)
[LeetCode 102 二叉树的层序遍历](#LeetCode 102 二叉树的层序遍历)
[LeetCode 103 二叉树的锯齿形层序遍历](#LeetCode 103 二叉树的锯齿形层序遍历)
[LeetCode 994 腐烂的橘子](#LeetCode 994 腐烂的橘子)
[无权图最短路 BFS(含网格)](#无权图最短路 BFS(含网格))
[LeetCode 1091 二进制矩阵中的最短路径](#LeetCode 1091 二进制矩阵中的最短路径)
[LeetCode 752 打开转盘锁](#LeetCode 752 打开转盘锁)
[LeetCode 127 单词接龙](#LeetCode 127 单词接龙)
[有权正边 Dijkstra(邻接表 + 小根堆)](#有权正边 Dijkstra(邻接表 + 小根堆))
[LeetCode743 网络延迟时间](#LeetCode743 网络延迟时间)
[LeetCode1631 最小体力消耗路径](#LeetCode1631 最小体力消耗路径)
[0-1 BFS(边权只为 0/1)](#0-1 BFS(边权只为 0/1))
[LeetCode1368 使网格图至少有一条有效路径的最小代价](#LeetCode1368 使网格图至少有一条有效路径的最小代价)
[LeetCode547 省份数量](#LeetCode547 省份数量)
[LeetCode323 无向图中连通分量的数目](#LeetCode323 无向图中连通分量的数目)
[LeetCode 207 课程表](#LeetCode 207 课程表)
[LeetCode 210 课程表 II](#LeetCode 210 课程表 II)
BFS & 图论
广度优先搜索(BFS):
基本思想:从一个起点开始,层层扩展,逐层遍历所有节点,适用于最短路径问题。
队列:BFS需要队列来记录当前遍历的节点,确保按层级遍历。
图的表示:图可以用邻接矩阵或邻接表来表示。
图的遍历:
无向图 vs 有向图:有向图边是有方向的,遍历时要注意方向。
连通性判断:用BFS可以判断一个图是否连通。
最短路径问题:
单源最短路径:从一个节点出发,找到到其他所有节点的最短路径。
广度优先搜索:BFS能有效解决无权图的单源最短路径问题。
BFS题型分类
层次遍历(BFS遍历)
用于遍历每一层的节点,通常是树或图的遍历。
重点是理解如何逐层遍历,层与层之间的关系。
最短路径问题
解决从一个点到其他点的最短路径问题,尤其是无权图(即每条边的权重都是1)
重点是如何用BFS遍历图,并计算最短路径
图的连通性与拓扑排序
连通性:判断一个图是否是连通图,是否可以从一个节点到达其他所有节点。
拓扑排序:有向图中的排序,判断是否有环,如何处理图中的依赖关系。
重点是理解图中的依赖关系,如何用BFS来处理这些依赖。
图的克隆与复制
重点是如何复制图中的节点和边,使用BFS来遍历并创建新的图结构。
层次遍历
LeetCode 102 二叉树的层序遍历
cpp
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 二叉树的锯齿形层序遍历
cpp
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。
cpp
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; // 没有新鲜橘子,时间为 0
int 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
cpp
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 个邻居
只求最短距离,不需要所有路径或带权代价
所以是无权图最短路
cpp
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;
// +1
nxt1[i] = (cur[i] == '9' ? '0' : char(cur[i] + 1));
// -1
nxt2[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解法如下:
cpp
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) {
// +1
string 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); }
}
// -1
string 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解法如下:
cpp
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解法如下:
cpp
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 本身算 1
while (!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 网络延迟时间
cpp
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; // 邻居节点v
int 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 最小体力消耗路径
cpp
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 使网格图至少有一条有效路径的最小代价
cpp
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 BFS
dist[0][0] = 0; // 起点到自身的代价为0
dq.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 否则代价为1
int 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最重要的应用之一。
步骤:
找出一个有向图中所有的强连通分量(SCC)
把每一个SCC看作一个单一的超级节点(或称"缩点")
如果原图中存在从一个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
cpp
// 并查集 (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,把与它直接或间接相连的城市全标记为已访问;开了几次,就有几个省份
cpp
// 计算省份数量(连通分量个数)
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; // 省份数量+1
dfs(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,就有几个省份
cpp
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 无向图中连通分量的数目

解法一:并查集
cpp
// 并查集(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; // 深度相等时,任意合并,但深度要+1
r[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;启动次数就是分量数。
cpp
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 作答
cpp
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的节点,这些节点代表没有未处理前置依赖的任务。
cpp
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的入度加1
indeg[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;否则返回false
return taken == numCourses;
}
LeetCode 210 课程表 II
cpp
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->a
indeg[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;
}