本文详细解析LeetCode第261题"以图判树",这是一道图论问题。文章提供了从DFS到并查集的多种解法,包含C#、Python、C++三种语言实现,配有详细的算法步骤图解和性能分析。适合想要深入理解图论算法和树的性质的算法学习者。
核心知识点: 图论、DFS、BFS、并查集、树的性质
难度等级: 中等
推荐人群: 图论学习者、算法面试准备者
给定从 0 到 n-1 标号的 n 个结点,和一个无向边列表(每条边以结点对来表示),请编写一个函数用来判断这些边是否能够形成一个合法有效的树结构。
输入: n = 5, edges = [[0,1], [0,2], [0,3], [1,4]]
输出: true
输入: n = 5, edges = [[0,1], [1,2], [2,3], [1,3], [1,4]]
输出: false
1 <= n <= 2000
0 <= edges.length <= 5000
edges[i].length == 2
0 <= ai, bi < n
ai != bi
判断一个图是否是一棵有效的树需要满足两个条件:
有一个重要的数学性质可以帮助我们:对于有 n 个节点的树,恰好有 n-1 条边。因此,如果边的数量不等于节点数减一,那么一定不是树。
我们可以使用DFS来检查图是否连通且无环:
复杂度分析:
BFS的思路与DFS类似,也是检查图是否连通且无环:
复杂度分析:
并查集是解决此类问题的另一个强大工具:
复杂度分析:
步骤 | 操作 | 状态 | 说明 |
---|---|---|---|
初始状态 | n=5, edges=[[0,1],[0,2],[0,3],[1,4]] | 待验证图 | 需要判断是否为树 |
第一步 | 检查边数:4 == 5-1 | 边数检查通过 | 满足树的必要条件 |
第二步 | 构建邻接表 | 图结构建立 | 0->[1,2,3], 1->[0,4], 2->[0], 3->[0], 4->[1] |
第三步 | DFS遍历检查环 | 无环检测 | 从节点0开始,记录父节点避免回退 |
第四步 | 检查连通性 | 全部访问 | 所有节点都被访问到,图连通 |
结果 | 返回true | 验证成功 | 该图是一棵有效的树 |
边 | 节点1的根 | 节点2的根 | 操作结果 | 集合状态 |
---|---|---|---|---|
初始状态 | - | - | - | {0},{1},{2},{3},{4} |
[0,1] | 0 | 1 | 合并成功 | {0,1},{2},{3},{4} |
[0,2] | 0 | 2 | 合并成功 | {0,1,2},{3},{4} |
[0,3] | 0 | 3 | 合并成功 | {0,1,2,3},{4} |
[1,4] | 0 | 4 | 合并成功 | {0,1,2,3,4} |
public class Solution {
public bool ValidTree(int n, int[][] edges) {
// 检查边的数量是否等于节点数减一
if (edges.Length != n - 1) {
return false;
}
// 构建邻接表
List<int>[] graph = new List<int>[n];
for (int i = 0; i < n; i++) {
graph[i] = new List<int>();
}
foreach (int[] edge in edges) {
graph[edge[0]].Add(edge[1]);
graph[edge[1]].Add(edge[0]);
}
bool[] visited = new bool[n];
// DFS检查是否有环
if (!DFS(graph, 0, -1, visited)) {
return false;
}
// 检查是否所有节点都被访问到(连通性检查)
for (int i = 0; i < n; i++) {
if (!visited[i]) {
return false;
}
}
return true;
}
private bool DFS(List<int>[] graph, int node, int parent, bool[] visited) {
visited[node] = true;
foreach (int neighbor in graph[node]) {
// 跳过父节点,避免误判返回路径为环
if (neighbor == parent) {
continue;
}
// 如果邻居已被访问,说明存在环
if (visited[neighbor] || !DFS(graph, neighbor, node, visited)) {
return false;
}
}
return true;
}
}
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
# 检查边的数量是否等于节点数减一
if len(edges) != n - 1:
return False
# 构建邻接表
graph = [[] for _ in range(n)]
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = [False] * n
def dfs(node, parent):
visited[node] = True
for neighbor in graph[node]:
# 跳过父节点,避免误判返回路径为环
if neighbor == parent:
continue
# 如果邻居已被访问,说明存在环
if visited[neighbor] or not dfs(neighbor, node):
return False
return True
# DFS检查是否有环
if not dfs(0, -1):
return False
# 检查是否所有节点都被访问到(连通性检查)
return all(visited)
class Solution {
public:
bool validTree(int n, vector<vector<int>>& edges) {
// 检查边的数量是否等于节点数减一
if (edges.size() != n - 1) {
return false;
}
// 构建邻接表
vector<vector<int>> graph(n);
for (const auto& edge : edges) {
graph[edge[0]].push_back(edge[1]);
graph[edge[1]].push_back(edge[0]);
}
vector<bool> visited(n, false);
// DFS检查是否有环
if (!dfs(graph, 0, -1, visited)) {
return false;
}
// 检查是否所有节点都被访问到(连通性检查)
for (int i = 0; i < n; i++) {
if (!visited[i]) {
return false;
}
}
return true;
}
private:
bool dfs(const vector<vector<int>>& graph, int node, int parent, vector<bool>& visited) {
visited[node] = true;
for (int neighbor : graph[node]) {
// 跳过父节点,避免误判返回路径为环
if (neighbor == parent) {
continue;
}
// 如果邻居已被访问,说明存在环
if (visited[neighbor] || !dfs(graph, neighbor, node, visited)) {
return false;
}
}
return true;
}
};
语言 | 执行用时 | 内存消耗 | 特点 |
---|---|---|---|
C# | 160 ms | 44.3 MB | 代码结构清晰,但性能相对较差 |
Python | 92 ms | 17.2 MB | 代码简洁,性能适中 |
C++ | 12 ms | 12.1 MB | 性能最佳,内存占用较小 |
解法 | 时间复杂度 | 空间复杂度 | 优点 | 缺点 |
---|---|---|---|---|
DFS | O(n + e) | O(n + e) | 直观易懂,实现简单 | 递归可能导致栈溢出 |
BFS | O(n + e) | O(n + e) | 避免递归栈溢出问题 | 实现略复杂,需要使用队列 |
并查集 | O(n + e * α(n)) | O(n) | 性能最佳,适合大规模图 | 实现相对复杂,需要理解并查集原理 |
算法专题合集 - 查看完整合集
关注合集更新:点击上方合集链接,关注获取最新题解!目前已更新第261题。
感谢大家耐心阅读到这里!希望这篇题解能够帮助你更好地理解和掌握这道算法题。
如果这篇文章对你有帮助,请:
你的支持是我持续分享的动力!
一起进步:算法学习路上不孤单,欢迎一起交流学习!