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

题目描述

给定一个包含 n 个顶点的无向图(双向图),顶点编号从 0n-1,边由二维数组 edges 表示(edges[i] = [ui, vi] 表示 uivi 之间有一条双向边)。要求判断是否存在从 source 顶点到 destination 顶点的有效路径。

核心问题

本质是无向图的连通性判断问题:判断两个顶点是否处于同一个连通分量中。

输入输出约束

  • 1 <= n <= 2 * 10^5(顶点数量较大,要求算法时间复杂度不能过高)
  • 0 <= edges.length <= 2 * 10^5
  • edges[i].length == 2
  • 0 <= ui, vi <= n - 1
  • ui != vi(无自环)
  • 0 <= source, destination <= n - 1
  • 边是唯一的(无重边)

算法分析

针对无向图连通性问题,有两种经典解法:

解法 1:并查集(Union-Find/Disjoint Set Union, DSU)

核心思想
  • 初始化:每个顶点自成一个集合(父节点指向自己)
  • 合并:遍历所有边,将每条边的两个顶点所在集合合并
  • 查询:判断 sourcedestination 是否属于同一个集合
优势
  • 时间复杂度接近线性(带路径压缩和按秩合并优化)
  • 空间复杂度低,适合处理大规模数据(本题顶点数可达 2e5)
(1)路径压缩(Find 操作优化)
  • 问题:如果不优化,查找路径可能退化成链表(如 0→1→2→3→4),查找效率低
  • 优化:查找时将节点直接指向根节点,后续查找只需一步
  • 示例:查找 0 的根节点时,将 0、1、2 都直接指向根节点 4
(2)按秩合并(Union 操作优化)
  • 问题:如果随意合并,树可能变得很高,查找效率低
  • 优化:记录每个根节点的「秩」(树的高度),将秩小的树合并到秩大的树下
  • 示例:秩为 2 的树合并到秩为 3 的树下,避免树高增加

3. 并查集操作流程

  1. 初始化:每个节点的父节点是自己,秩为 1
  2. 合并:遍历所有边,将每条边的两个节点合并
  3. 查询:判断起点和终点的根节点是否相同(相同则连通)
objectivec 复制代码
// 并查集数组:parent[i]表示顶点i的父节点
int* parent;
// 秩数组:rank[i]表示以i为根的集合的高度(用于按秩合并)
int* rank;
objectivec 复制代码
#include <stdio.h>
#include <stdlib.h>

// 初始化并查集
void initUnionFind(int n, int** parent, int** rank) {
    *parent = (int*)malloc(sizeof(int) * n);
    *rank = (int*)malloc(sizeof(int) * n);
    for (int i = 0; i < n; i++) {
        (*parent)[i] = i;  // 初始父节点为自身
        (*rank)[i] = 1;    // 初始秩为1
    }
}

// 查找根节点(带路径压缩)
int find(int* parent, int x) {
    if (parent[x] != x) {
        parent[x] = find(parent, parent[x]); // 路径压缩:直接指向根节点
    }
    return parent[x];
}

// 合并两个集合(按秩合并)
void unionSets(int* parent, int* rank, int x, int y) {
    int rootX = find(parent, x);
    int rootY = find(parent, y);
    if (rootX == rootY) return; // 已在同一集合
    
    // 按秩合并:将秩小的树合并到秩大的树下
    if (rank[rootX] < rank[rootY]) {
        parent[rootX] = rootY;
    } else if (rank[rootX] > rank[rootY]) {
        parent[rootY] = rootX;
    } else {
        parent[rootY] = rootX;
        rank[rootX]++; // 秩相等时,合并后秩+1
    }
}

// 主函数:判断是否存在有效路径
bool validPath(int n, int** edges, int edgesSize, int* edgesColSize, int source, int destination) {
    // 边界条件:起点=终点直接返回true
    if (source == destination) return true;
    
    int* parent = NULL;
    int* rank = NULL;
    initUnionFind(n, &parent, &rank);
    
    // 合并所有边的两个顶点
    for (int i = 0; i < edgesSize; i++) {
        int u = edges[i][0];
        int v = edges[i][1];
        unionSets(parent, u, v);
    }
    
    // 判断起点和终点是否在同一集合
    bool result = (find(parent, source) == find(parent, destination));
    
    // 释放内存
    free(parent);
    free(rank);
    return result;
}

int main() {
    // 示例1:存在路径
    int n1 = 3;
    int edges1[][2] = {{0,1}, {1,2}, {2,0}};
    int edgesSize1 = 3;
    int edgesColSize1[] = {2,2,2};
    int source1 = 0, dest1 = 2;
    bool res1 = validPath(n1, (int**)edges1, edgesSize1, edgesColSize1, source1, dest1);
    printf("示例1结果:%s\n", res1 ? "true" : "false"); // 输出true
    
    // 示例2:不存在路径
    int n2 = 6;
    int edges2[][2] = {{0,1}, {0,2}, {3,5}, {5,4}, {4,3}};
    int edgesSize2 = 5;
    int edgesColSize2[] = {2,2,2,2,2};
    int source2 = 0, dest2 = 5;
    bool res2 = validPath(n2, (int**)edges2, edgesSize2, edgesColSize2, source2, dest2);
    printf("示例2结果:%s\n", res2 ? "true" : "false"); // 输出false
    
    return 0;
}

解法 2:深度优先搜索(DFS)/ 广度优先搜索(BFS)

核心思想
  • 构建邻接表表示图
  • source 出发,通过 DFS/BFS 遍历可达顶点
  • 检查 destination 是否被遍历到
局限性
  • 对于大规模图(2e5 顶点),递归 DFS 可能栈溢出,迭代 DFS/BFS 需要额外的访问标记数组
  • 时间复杂度虽为线性,但常数项高于并查集

BFS 核心思路

  1. 构建邻接表:存储每个节点的相邻节点(无向图需双向添加)
  2. 初始化队列:将起点入队,并标记为已访问
  3. 遍历队列:取出队首节点,遍历其所有相邻节点
    • 若相邻节点是终点,返回 true
    • 若未访问过,标记为已访问并入队
  4. 队列为空仍未找到终点,返回 false
objectivec 复制代码
// 邻接表:adj[i]存储与顶点i相连的所有顶点
typedef struct {
    int* nodes;   // 顶点数组
    int size;     // 当前元素个数
    int capacity; // 容量
} AdjListNode;
AdjListNode* adj;
objectivec 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

// 邻接表节点结构
typedef struct {
    int* nodes;
    int size;
    int capacity;
} AdjListNode;

// 队列结构(BFS用)
typedef struct {
    int* data;
    int front;
    int rear;
    int capacity;
} Queue;

// 初始化队列
Queue* initQueue(int capacity) {
    Queue* q = (Queue*)malloc(sizeof(Queue));
    q->data = (int*)malloc(sizeof(int) * capacity);
    q->front = 0;
    q->rear = 0;
    q->capacity = capacity;
    return q;
}

// 入队
void enqueue(Queue* q, int val) {
    q->data[q->rear++] = val;
}

// 出队
int dequeue(Queue* q) {
    return q->data[q->front++];
}

// 判断队列是否为空
bool isEmpty(Queue* q) {
    return q->front == q->rear;
}

// 释放队列
void freeQueue(Queue* q) {
    free(q->data);
    free(q);
}

// 初始化邻接表
AdjListNode* initAdjList(int n) {
    AdjListNode* adj = (AdjListNode*)malloc(sizeof(AdjListNode) * n);
    for (int i = 0; i < n; i++) {
        adj[i].nodes = (int*)malloc(sizeof(int) * 4); // 初始容量4
        adj[i].size = 0;
        adj[i].capacity = 4;
    }
    return adj;
}

// 添加边到邻接表
void addEdge(AdjListNode* adj, int u, int v) {
    // 扩容检查
    if (adj[u].size >= adj[u].capacity) {
        adj[u].capacity *= 2;
        adj[u].nodes = (int*)realloc(adj[u].nodes, sizeof(int) * adj[u].capacity);
    }
    adj[u].nodes[adj[u].size++] = v;
    
    // 无向图:双向添加
    if (adj[v].size >= adj[v].capacity) {
        adj[v].capacity *= 2;
        adj[v].nodes = (int*)realloc(adj[v].nodes, sizeof(int) * adj[v].capacity);
    }
    adj[v].nodes[adj[v].size++] = u;
}

// BFS判断路径
bool validPathBFS(int n, int** edges, int edgesSize, int* edgesColSize, int source, int destination) {
    if (source == destination) return true;
    
    // 初始化邻接表
    AdjListNode* adj = initAdjList(n);
    for (int i = 0; i < edgesSize; i++) {
        addEdge(adj, edges[i][0], edges[i][1]);
    }
    
    // 访问标记数组
    bool* visited = (bool*)calloc(n, sizeof(bool));
    Queue* q = initQueue(n);
    
    // 起点入队
    enqueue(q, source);
    visited[source] = true;
    
    // BFS遍历
    while (!isEmpty(q)) {
        int curr = dequeue(q);
        // 遍历当前节点的所有邻接节点
        for (int i = 0; i < adj[curr].size; i++) {
            int next = adj[curr].nodes[i];
            if (next == destination) {
                // 释放资源
                free(visited);
                freeQueue(q);
                for (int j = 0; j < n; j++) {
                    free(adj[j].nodes);
                }
                free(adj);
                return true;
            }
            if (!visited[next]) {
                visited[next] = true;
                enqueue(q, next);
            }
        }
    }
    
    // 释放资源
    free(visited);
    freeQueue(q);
    for (int j = 0; j < n; j++) {
        free(adj[j].nodes);
    }
    free(adj);
    return false;
}

int main() {
    int n1 = 3;
    int edges1[][2] = {{0,1}, {1,2}, {2,0}};
    int edgesSize1 = 3;
    int edgesColSize1[] = {2,2,2};
    printf("BFS示例1:%s\n", validPathBFS(n1, (int**)edges1, edgesSize1, edgesColSize1, 0, 2) ? "true" : "false");
    
    int n2 = 6;
    int edges2[][2] = {{0,1}, {0,2}, {3,5}, {5,4}, {4,3}};
    int edgesSize2 = 5;
    int edgesColSize2[] = {2,2,2,2,2};
    printf("BFS示例2:%s\n", validPathBFS(n2, (int**)edges2, edgesSize2, edgesColSize2, 0, 5) ? "true" : "false");
    
    return 0;
}

复杂度分析

并查集解法

时间复杂度
  • 初始化:O (n)
  • 查找操作(带路径压缩):均摊 O (α(n)),α 是阿克曼函数的反函数,增长极慢(对于 n≤2^65536,α(n)≤5)
  • 合并操作:O (α(n))
  • 整体时间复杂度:O (n + m・α(n)),其中 m 是边数(edgesSize)对于本题数据规模(n,m≤2e5),实际效率接近 O (n + m)
空间复杂度
  • O (n):主要为 parentrank 数组的空间开销

BFS 解法

时间复杂度
  • 构建邻接表:O (n + m)
  • BFS 遍历:O (n + m)(每个顶点和边仅访问一次)
  • 整体时间复杂度:O (n + m)
空间复杂度
  • O (n + m):邻接表存储所有边(O (m)),队列和访问标记数组(O (n))
相关推荐
Felven28 分钟前
A. Shizuku Hoshikawa and Farm Legs
算法
仰泳的熊猫28 分钟前
1150 Travelling Salesman Problem
数据结构·c++·算法·pat考试
练习时长一年37 分钟前
LeetCode热题100(最小栈)
java·算法·leetcode
vi121231 小时前
土壤与水分遥感反演技术综述:原理、方法与应用
人工智能·算法·无人机
智者知已应修善业1 小时前
【蓝桥杯龟兔赛跑】2024-2-12
c语言·c++·经验分享·笔记·算法·职场和发展·蓝桥杯
Tisfy1 小时前
LeetCode 955.删列造序 II:模拟(O(mn)) + 提前退出
算法·leetcode·字符串·题解·遍历
im_AMBER1 小时前
Leetcode 82 每个字符最多出现两次的最长子字符串 | 删掉一个元素以后全为 1 的最长子数组
c++·笔记·学习·算法·leetcode
姓蔡小朋友1 小时前
后端面试八股文
面试·职场和发展
java修仙传1 小时前
力扣hot100:旋转排序数组中找目标值
算法·leetcode·职场和发展
式5161 小时前
量子力学基础(二)狄拉克符号与复数向量空间
人工智能·算法·机器学习