哈喽各位,我是前端小L。

欢迎来到我们的新专题——“图论”!准备好,我们的思维即将从“一条线”升维到一张“网”。

今天,我们将从图论最基础、最核心的问题开始:A点和B点,是连通的吗? 这个问题看似简单,却蕴含了后续所有复杂图算法的基础。我们将用它来““磨刀”,彻底掌握图的表示和遍历。

力扣 1971. 寻找图中是否存在路径

https://leetcode.cn/problems/find-if-path-exists-in-graph/

题目分析:

  • 输入n (顶点数, 0到n-1),edges (一个 [u, v] 列表),source (起点),destination (终点)。

  • 图的类型无向图[u, v] 意味着 uv 之间有一条双向的路。

  • 目标:判断从 source 出发,能否“走”到 destination

第一关:如何把“边”建成“地图”?—— 邻接表

计算机并不“认识” [[0, 1], [1, 2], [0, 2]] 这种“边列表”。它太慢了,要找 0 的邻居,我们得遍历整个列表。 我们需要一种更高效的“地图”——邻接表 (Adjacency List)

邻接表在 C++ 中,就是一个“向量的向量”:vector<vector<int>> graph(n);

  • graph 是一个大 vector,长度为 n

  • graph[i] 是一个小 vector,里面存储了所有与 i 直接相连的邻居。

建图过程 (O(E)): 我们遍历 edges 列表(E 是边的数量),对于每一条边 [u, v]

  1. graph[u].push_back(v); (添加一条 u -> v 的路)

  2. graph[v].push_back(u); (因为是无向图,还要添加 v -> u 的路)

C++

// 1. 建图 (邻接表)
vector> graph(n);
for (const auto& edge : edges) {
    int u = edge[0];
    int v = edge[1];
    graph[u].push_back(v);
    graph[v].push_back(u); // 无向图,双向添加
}

第二关:如何“走”这张地图?—— DFS 与 BFS

有了地图,我们就可以从 source 出发“探险”了。有两种经典的探险策略:

  1. DFS (深度优先搜索):像在走迷宫,“一条路走到黑”

    • 策略:从 source 出发,访问它的第一个邻居 v1,然后再访问 v1 的第一个邻居 v2... 直到走到“死胡同”,再回溯(返回上一层),去探索 v1 的第二个邻居。

    • 实现:通常用递归

  2. BFS (广度优先搜索):像“水波纹”一样,“一层一层地向外扩散”

    • 策略:从 source 出发(第0层),访问它所有“一度人脉”(邻居,第1层),然后再访问所有“二度人脉”(邻居的邻居,第2层)...

    • 实现:通常用队列 (Queue)

“Aha!”时刻:防止“兜圈”的灵魂—— visited 数组

无论DFS还是BFS,我们都会遇到一个致命问题: [0, 1] 这条边,graph[0] 里有 1graph[1] 里有 0

  • DFS: dfs(0) -> 看到邻居 1 -> 调用 dfs(1)

  • dfs(1) -> 看到邻居 0 -> 调用 dfs(0)

  • dfs(0) -> 看到邻居 1 -> 调用 dfs(1)

  • ... 栈溢出 (Stack Overflow)!

解决方案:我们需要一个“足迹”数组,走过的地方就不要再走了! vector<bool> visited(n, false);

遍历的“铁律”

  1. (BFS):当一个节点入队 (push) 时,必须立刻标记 visited[node] = true

  2. (DFS):在 dfs(node) 函数的第一行必须立刻标记 visited[node] = true

代码实现 (O(V+E) 时间, O(V+E) 空间)

我们提供两种解法,它们的核心思想都是“遍历”。

解法一:DFS (递归)

class Solution {
public:
    bool validPath(int n, vector>& edges, int source, int destination) {
        // 1. 建图 (邻接表)
        vector> graph(n);
        for (const auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
            graph[edge[1]].push_back(edge[0]);
        }
        // 2. “灵魂”:visited 数组
        vector visited(n, false);
        // 3. 启动 DFS
        dfs(source, graph, visited);
        // 4. 检查终点是否被访问到
        return visited[destination];
    }
    void dfs(int u, vector>& graph, vector& visited) {
        // “铁律”:立刻标记
        visited[u] = true;
        // 探索所有邻居
        for (int v : graph[u]) {
            if (!visited[v]) {
                dfs(v, graph, visited);
            }
        }
    }
};

解法二:BFS (队列)

C++

#include  // 引入队列
class Solution {
public:
    bool validPath(int n, vector>& edges, int source, int destination) {
        // 1. 建图
        vector> graph(n);
        for (const auto& edge : edges) {
            graph[edge[0]].push_back(edge[1]);
            graph[edge[1]].push_back(edge[0]);
        }
        vector visited(n, false);
        queue q;
        // 3. 启动 BFS
        q.push(source);
        visited[source] = true; // “铁律”:入队时立刻标记
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            if (u == destination) {
                return true; // 可以在这里提前退出
            }
            // 探索所有邻居
            for (int v : graph[u]) {
                if (!visited[v]) {
                    visited[v] = true; // “铁律”:入队时立刻标记
                    q.push(v);
                }
            }
        }
        // 4. 检查终点是否被访问到
        return visited[destination];
    }
};

深度复杂度分析

  • V (Vertices):顶点数,即 n

  • E (Edges):边数,即 edges.size()

  • 时间复杂度 O(V + E)

    • 建图:需要 O(E) 时间,因为我们遍历了 edges 数组一次。

    • 遍历 (DFS/BFS):我们需要访问每个顶点 V 最多一次(因为 visited 数组的保护)。在访问每个顶点 u 时,我们会遍历它的所有邻居,这相当于遍历了它的所有“出边”。对于整个图,所有“出边”的总和,在无向图中等于 2 * E

    • 总时间 = O(E) + O(V + 2E) = O(V + E)

  • 空间复杂度 O(V + E)

    • 邻接表 graph:需要存储所有的边,总空间是 O(V + E)。

    • visited 数组:需要 O(V) 空间。

    • 辅助空间:DFS 需要 O(V) 的递归栈空间(最坏情况,如一条长链);BFS 需要 O(V) 的队列空间(最坏情况,如一个“星型图”)。

    • 总空间 = O(V + E) + O(V) = O(V + E)

总结

今天,我们为“图论”专题打下了最坚实的地基。我们学会了:

  1. 邻接表vector<vector<int>> 来“建图”。

  2. 图的遍历有 DFS(递归)和 BFS(队列)两种核心方式。

  3. visited 数组是防止无限循环的“灵魂”。

在下一篇中,我们将把今天学到的 DFS/BFS,应用到最常见的“隐式图”——二维网格(“岛屿问题”)上!

下期见!