图论理论基础(1)

文章目录

  • 图论理论基础(1)
    • [1. 图的基本概念](#1. 图的基本概念)
      • [1.1 基本术语](#1.1 基本术语)
    • [2. 图的分类](#2. 图的分类)
      • [2.1 有向图 vs 无向图](#2.1 有向图 vs 无向图)
      • [2.2 加权图 vs 无权图](#2.2 加权图 vs 无权图)
      • [2.3 度(Degree)](#2.3 度(Degree))
    • [3. 图的连通性](#3. 图的连通性)
      • [3.1 连通图 vs 非连通图](#3.1 连通图 vs 非连通图)
      • [3.2 强连通性(有向图)](#3.2 强连通性(有向图))
      • [3.3 连通分量(无向图)](#3.3 连通分量(无向图))
    • [4. 图的存储方式](#4. 图的存储方式)
      • [4.1 邻接矩阵](#4.1 邻接矩阵)
      • [4.2 邻接表](#4.2 邻接表)
      • [4.3 边列表](#4.3 边列表)
    • [5. 图的遍历算法](#5. 图的遍历算法)
      • [5.1 深度优先搜索(DFS)](#5.1 深度优先搜索(DFS))
      • [5.2 广度优先搜索(BFS)](#5.2 广度优先搜索(BFS))
    • [6. 图论算法分类](#6. 图论算法分类)
      • [6.1 图的存储与遍历](#6.1 图的存储与遍历)
      • [6.2 拓扑排序](#6.2 拓扑排序)
      • [6.3 并查集](#6.3 并查集)
      • [6.4 最短路径算法](#6.4 最短路径算法)
      • [6.5 最小生成树](#6.5 最小生成树)
    • [7. 时间复杂度总结](#7. 时间复杂度总结)
    • [8. DFS与BFS对比](#8. DFS与BFS对比)
      • [8.1 算法对比](#8.1 算法对比)
      • [8.2 DFS搜索过程](#8.2 DFS搜索过程)
      • [8.3 DFS代码框架](#8.3 DFS代码框架)
    • [9. BFS详解](#9. BFS详解)
      • [9.1 BFS使用场景](#9.1 BFS使用场景)
      • [9.2 BFS搜索过程](#9.2 BFS搜索过程)
      • [9.3 BFS代码框架](#9.3 BFS代码框架)

图论理论基础(1)

1. 图的基本概念

**图(Graph)**是由顶点(Vertex/Node)和边(Edge)组成的数据结构,用来表示事物之间的关系。

1.1 基本术语

  • 顶点(Vertex):图中的节点,表示事物
  • 边(Edge):连接两个顶点的线,表示关系
  • 路径(Path):从一个顶点到另一个顶点经过的边的序列
  • 环(Cycle):起点和终点相同的路径

2. 图的分类

2.1 有向图 vs 无向图

  • 无向图:边没有方向,A-B 和 B-A 是同一条边
  • 有向图:边有方向,A→B 和 B→A 是不同的边

2.2 加权图 vs 无权图

  • 无权图:边没有权重,只表示连接关系
  • 加权图:边有权重,表示距离、成本等

2.3 度(Degree)

  • :与顶点相连的边的数量
  • 出度:有向图中从顶点出发的边的数量
  • 入度:有向图中指向顶点的边的数量

3. 图的连通性

3.1 连通图 vs 非连通图

  • 连通图:任意两个顶点之间都存在路径
  • 非连通图:存在两个顶点之间没有路径

3.2 强连通性(有向图)

  • 强连通图:任意两个顶点之间都存在双向路径
  • 强连通分量:极大强连通子图

示例

复制代码
有向图:A → B → C → A
        ↓
        D

强连通分量1:{A, B, C} - 这三个节点互相可达
强连通分量2:{D} - 单独一个节点

图解

复制代码
A ──→ B ──→ C
│           │
│           │
└───────────┘
│
↓
D

3.3 连通分量(无向图)

  • 连通分量:无向图中的极大连通子图
  • 连通分量数量:图中连通分量的个数

示例

复制代码
无向图:A-B-C, D-E, F

连通分量1:{A, B, C} - 这三个节点互相连通
连通分量2:{D, E} - 这两个节点互相连通  
连通分量3:{F} - 单独一个节点
连通分量数量:3

图解

复制代码
A ── B ── C    D ── E    F

4. 图的存储方式

4.1 邻接矩阵

cpp 复制代码
// 适合稠密图,空间复杂度O(V²)
vector<vector<int>> graph(V, vector<int>(V, 0));
// graph[i][j] = 1 表示顶点i和j之间有边

4.2 邻接表

cpp 复制代码
// 适合稀疏图,空间复杂度O(V+E)
vector<vector<int>> graph(V);
// graph[i] 存储与顶点i相邻的所有顶点

4.3 边列表

cpp 复制代码
// 存储所有边的信息
vector<pair<int, int>> edges;  // 无权边
vector<tuple<int, int, int>> edges;  // 加权边 (u, v, weight)

5. 图的遍历算法

5.1 深度优先搜索(DFS)

算法特点

  • 深度优先:尽可能深地搜索图的分支
  • 递归实现:使用递归栈,代码简洁
  • 应用场景:路径查找、连通性判断、拓扑排序
  • 时间复杂度:O(V+E),每个顶点和边访问一次
  • 空间复杂度:O(V),递归栈的深度

执行过程示例

复制代码
图:A-B-C
    |   |
    D   E

DFS访问顺序:A → B → C → E → D

代码实现

cpp 复制代码
void dfs(int node, vector<bool>& visited, vector<vector<int>>& graph) {
    visited[node] = true;  // 标记当前节点已访问
    
    // 遍历当前节点的所有邻居
    for (int neighbor : graph[node]) {
        if (!visited[neighbor]) {  // 如果邻居未被访问
            dfs(neighbor, visited, graph);  // 递归访问邻居
        }
    }
}

5.2 广度优先搜索(BFS)

算法特点

  • 广度优先:逐层访问,先访问距离近的节点
  • 队列实现:使用队列维护访问顺序
  • 应用场景:最短路径、层次遍历、连通性判断
  • 时间复杂度:O(V+E),每个顶点和边访问一次
  • 空间复杂度:O(V),队列的最大长度

执行过程示例

复制代码
图:A-B-C
    |   |
    D   E

BFS访问顺序:A → B → D → C → E
层次:第0层:A
     第1层:B, D
     第2层:C, E

代码实现

cpp 复制代码
void bfs(int start, vector<vector<int>>& graph) {
    queue<int> q;  // 队列存储待访问的节点
    vector<bool> visited(graph.size(), false);  // 标记数组
    
    q.push(start);  // 将起始节点加入队列
    visited[start] = true;  // 标记起始节点已访问
    
    while (!q.empty()) {  // 队列不为空时继续
        int node = q.front();  // 取出队首节点
        q.pop();  // 从队列中移除
        
        // 遍历当前节点的所有邻居
        for (int neighbor : graph[node]) {
            if (!visited[neighbor]) {  // 如果邻居未被访问
                visited[neighbor] = true;  // 标记邻居已访问
                q.push(neighbor);  // 将邻居加入队列
            }
        }
    }
}

6. 图论算法分类

6.1 图的存储与遍历

  • DFS/BFS:基础遍历算法
  • 应用:岛屿数量、被围绕的区域

6.2 拓扑排序

  • Kahn算法:基于入度的拓扑排序
  • DFS法:基于深度优先搜索的拓扑排序
  • 应用:课程表、任务调度

6.3 并查集

  • Union-Find:集合合并与查询
  • 应用:省份数量、朋友圈

6.4 最短路径算法

  • Dijkstra:单源最短路径(非负权)
  • Bellman-Ford:单源最短路径(可处理负权)
  • Floyd-Warshall:多源最短路径

6.5 最小生成树

  • Kruskal:基于边的贪心算法
  • Prim:基于顶点的贪心算法

7. 时间复杂度总结

算法 时间复杂度 空间复杂度 适用场景
DFS/BFS O(V+E) O(V) 图遍历
拓扑排序 O(V+E) O(V) DAG排序
并查集 O(α(n)) O(V) 连通性判断
Dijkstra O((V+E)logV) O(V) 单源最短路
Floyd O(V³) O(V²) 多源最短路
Kruskal O(ElogE) O(V) 最小生成树

8. DFS与BFS对比

8.1 算法对比

比较项 DFS(深度优先搜索) BFS(广度优先搜索)
搜索策略 一条路走到黑(递归/栈) 一层一层扩展(队列)
实现方式 通常用 递归显式栈 通常用 队列(queue)
适用场景 适合 全路径搜索、组合问题、回溯问题(如图遍历、排列组合) 适合 最短路径问题、层次遍历(如最短步数、层数搜索)
空间复杂度 相对较小(与递归深度有关) 相对较大(需存整层节点)
典型应用 迷宫求解、图连通分量、全排列 最短路径、最少步数、层序遍历

8.2 DFS搜索过程

假设你在一个迷宫中:

  1. 看到一个路口 → 选一条路走;
  2. 走到底发现死路 → 返回上一个岔口;
  3. 选另一条没走过的路 → 再次深入;
  4. 最终找到所有可能的出口。

这整个过程就叫 深度优先搜索(DFS)

8.3 DFS代码框架

cpp 复制代码
vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径

void dfs (参数){
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本节点所连接的其他节点) {
        处理节点;
        dfs(图,选择的节点); // 递归
        回溯,撤销处理结果
    }
}

9. BFS详解

9.1 BFS使用场景

广度优先搜索(Breadth First Search,简称 BFS)是一种逐层搜索的算法,常用于在图或树结构中寻找最短路径或最小步数的问题。

常见使用场景:

  1. 最短路径问题
    例如在无权图中,寻找从起点到终点的最短路径。
    • 典型例题:二叉树的最短深度、迷宫最短路径、网络中最短连接。
  2. 层序遍历
    BFS 天然按层扩展节点,因此在树结构中可用于层序遍历。
  3. 状态搜索类问题
    如八数码问题、单词接龙、棋盘问题等,BFS 可用于寻找最少操作次数。
  4. 网络流与最短增广路
    在最大流算法(如 Edmonds-Karp)中,BFS 用于寻找最短增广路径。
  5. 多源最短路问题
    同时从多个起点出发,计算所有点到最近起点的最短距离。

9.2 BFS搜索过程

BFS 采用队列(queue)实现"先进先出"的搜索逻辑,从起点开始,依次访问相邻节点,逐层向外扩展。

搜索流程:

  1. 将起点加入队列,并标记为已访问;
  2. 当队列非空时:
    • 取出队首节点;
    • 遍历该节点的所有邻接节点;
    • 若邻接节点未访问,则加入队列并标记;
  3. 直到队列为空或找到目标。

9.3 BFS代码框架

cpp 复制代码
int dir[4][2] = {
    {0, 1}, {1, 0}, {0, -1}, {-1, 0}
};// 表示四个方向
// grid 是地图,也就是一个二维数组
// visited标记访问过的节点,不要重复访问
// x,y 表示开始搜索节点的下标
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& visited, int x, int y) {
    queue<pair<int, int>> que; // 定义队列
    que.push({x, y}); // 起始节点加入队列
    visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点
    //加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过。要不然会导致很多重复
    while(!que.empty()) { // 开始遍历队列里的元素
        pair<int ,int> cur = que.front(); que.pop(); // 从队列取元素
        int curx = cur.first;
        int cury = cur.second; // 当前节点坐标
        for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历
            int nextx = curx + dir[i][0];
            int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标
            if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;  // 坐标越界了,直接跳过
            if (!visited[nextx][nexty]) { // 如果节点没被访问过
                que.push({nextx, nexty});  // 队列添加该节点为下一轮要遍历的节点
                visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问
            }
        }
    }

}
相关推荐
熬了夜的程序员5 小时前
【LeetCode】80. 删除有序数组中的重复项 II
java·数据结构·算法·leetcode·职场和发展·排序算法·动态规划
祁同伟.6 小时前
【OJ】二叉树的经典OJ题
数据结构·c++·算法·容器·stl
自在极意功。6 小时前
贪心算法深度解析:从理论到实战的完整指南
java·算法·ios·贪心算法
wydaicls7 小时前
C语言对单链表的操作
c语言·数据结构·算法
傻童:CPU7 小时前
C语言需要掌握的基础知识点之排序
c语言·算法·排序算法
大数据张老师11 小时前
数据结构——邻接矩阵
数据结构·算法
低音钢琴12 小时前
【人工智能系列:机器学习学习和进阶01】机器学习初学者指南:理解核心算法与应用
人工智能·算法·机器学习
傻童:CPU14 小时前
C语言需要掌握的基础知识点之前缀和
java·c语言·算法
深思慎考14 小时前
从合并两个链表到 K 个链表:分治思想的递进与堆优化
数据结构·链表·递归··队列·合并链表