火龙信奥
  • 首页
  • 课程
  • 题库
  • 打卡
    • 代码对战
    • 快速对战
  • 题单
  • 团队
  • 荣誉墙
  • 商城
  • 登录 / 注册

第5章 图论

作者: 作者的头像   huolong , 时间:2026-08-15 13:52:08 , 所有人可见, 阅读  2

第5章 图 — 基本概念、存储结构、遍历与最小生成树

本讲义系统整理了图论的基础知识、图的两种核心存储结构(邻接矩阵与邻接表)、图的遍历算法(DFS 与 BFS)、最小生成树算法(Kruskal 与 Prim),并附带了详尽的配套练习题、详细解析以及常用图算法的 C++ 代码实现。


5.1 图的基本概念

1. 图的分类

  • 无向图 (Undirected Graph):图中的边是没有方向的。若顶点 $v_i$ 和 $v_j$ 之间有边,则用无序对 $(v_i, v_j)$ 表示。
  • 有向图 (Directed Graph):图中的边是有方向的(通常称为弧)。若从顶点 $v_i$ 到 $v_j$ 有一条有向边,则用有序对 $\langle v_i, v_j \rangle$ 表示,其中 $v_i$ 为起点(弧尾),$v_j$ 为终点(弧头)。
  • 完全图 (Complete Graph):任意两个不同顶点之间都存在边连接的图。
    • 完全无向图:任意两个顶点间都有一条无向边。含有 $n$ 个顶点的完全无向图边数为: $$\frac{n(n-1)}{2}$$
    • 完全有向图:任意两个顶点间都存在方向相反的两条弧。含有 $n$ 个顶点的完全有向图弧数为: $$n(n-1)$$
  • 子图 (Subgraph):设 $G = (V, E)$ 是一个图,若 $G' = (V', E')$ 满足 $V' \subseteq V$ 且 $E' \subseteq E$,则称 $G'$ 是 $G$ 的一个子图。
  • 带权图/网 (Weighted Graph / Network):边或弧上带有某种数值(称为权,如表示距离、运费等)的图。

2. 边数与顶点的关系

对于一个含有 $n$ 个顶点的图 $G$: * 若 $G$ 是无向图,边数 $e$ 满足: $$0 \le e \le \frac{n(n-1)}{2}$$ * 若 $G$ 是有向图,边数 $e$ 满足: $$0 \le e \le n(n-1)$$


3. 连通性

  • 连通图 (Connected Graph):在无向图中,如果任意两个顶点之间都是连通的(即存在一条路径),则称该图为连通图。
  • 强连通图 (Strongly Connected Graph):在有向图中,若对于任意一对顶点 $v_i$ 和 $v_j$,从 $v_i$ 到 $v_j$ 和从 $v_j$ 到 $v_i$ 都存在路径,则称该图为强连通图。
  • 连通分量 (Connected Component):无向图的极大连通子图。
  • 强连通分量 (Strongly Connected Component):有向图的极大强连通子图。

4. 顶点的度 (Degree)

  • 度 (Degree):与顶点相连的边的数目,记作 $D(v)$。
  • 入度与出度(有向图):
    • 出度 (Out-degree):以顶点 $v$ 为起点的有向边数,记作 $OD(v)$。
    • 入度 (In-degree):以顶点 $v$ 为终点的有向边数,记作 $ID(v)$。
    • 有向图中顶点的度为入度与出度之和:$D(v) = ID(v) + OD(v)$。
  • 度与边数定理: 任何图中,所有顶点的度数之和等于边(或弧)数的 2 倍: $$\sum_{i=1}^{n} D(v_i) = 2e$$

5. 路径与回路 (Paths and Cycles)

  • 路径:顶点 $A$ 到顶点 $B$ 经过的顶点序列及其边。
  • 简单路径:路径中除了起点和终点外,其余顶点各不相同的路径。
  • 回路(环):起点和终点相同的路径。
  • 简单回路:除起点和终点相同外,其余顶点互不相同的回路。

5.2 图的存储结构

1. 邻接矩阵 (Adjacency Matrix) 表示法

邻接矩阵是使用一个二维数组 A[n][n] 来表示图的结构,其中 $n$ 是顶点个数。

  • 无权图的邻接矩阵: $$A[i][j] = \begin{cases} 1, & \text{若顶点 } v_i \text{ 到 } v_j \text{ 有边/弧} \ 0, & \text{否则} \end{cases}$$
    • 无向图的邻接矩阵是对称矩阵。第 $i$ 行(或列)中 $1$ 的个数等于顶点 $v_i$ 的度。
    • 有向图的邻接矩阵可能不对称。第 $i$ 行元素之和等于顶点 $v_i$ 的出度;第 $j$ 列元素之和等于顶点 $v_j$ 的入度。
  • 带权图的邻接矩阵: $$A[i][j] = \begin{cases} w_{ij}, & \text{若顶点 } v_i \text{ 到 } v_j \text{ 有边/弧,且权值为 } w_{ij} \ 0 \text{ 或 } \infty, & \text{若 } i = j \ \infty, & \text{其他情况(即无边连接)} \end{cases}$$
  • 优缺点:
    • 优点:结构简单,容易判断任意两个顶点之间是否有边,也容易计算顶点的度。
    • 缺点:存储空间为 $O(n^2)$,对于稀疏图(边数较少)会浪费大量的空间。适用于边稠密的图。

2. 邻接表 (Adjacency List) 表示法

邻接表是一种链式存储结构,为图中的每个顶点建立一个单链表。

  • 结构组成:
    1. 表头结点表:采用顺序存储结构,记录顶点信息以及指向该顶点第一个邻接点的指针。
    2. 边表(弧表):链表中的结点。每个结点记录邻接顶点在表头数组中的下标、边权(若有),以及指向下一个边结点的指针。
  • 有向图的分类:
    • 邻接表:链表中记录的是顶点的出边(便于求出度)。
    • 逆邻接表:链表中记录的是顶点的入边(便于求入度)。
  • 空间复杂度: 对于含有 $n$ 个顶点和 $e$ 条边的无向图,需要 $n$ 个头结点和 $2e$ 个表结点;对于有向图,需要 $n$ 个头结点和 $e$ 个表结点。适用于稀疏图。

5.3 图的遍历 (Graph Traversal)

从图的某个顶点出发,访问图中所有的顶点,且使每个顶点仅被访问一次。常见的遍历方法有深度优先搜索(DFS)和广度优先搜索(BFS)。

1. 深度优先搜索 (Depth First Search, DFS)

  • 基本思想:类似于树的先序遍历。从起始顶点 $v$ 出发,访问该顶点,然后访问与 $v$ 邻接且未被访问的顶点 $v_1$;再从 $v_1$ 出发深度优先搜索……当无法继续向下访问时,回溯到前一个顶点,寻找其未被访问的其他邻接点。
  • 数据结构:通常使用递归(隐式栈)来实现。

2. 广度优先搜索 (Breadth First Search, BFS)

  • 基本思想:类似于树的层次遍历。从起始顶点 $v$ 出发,访问 $v$ 之后,依次访问 $v$ 的所有未被访问的邻接点;然后再按照这些邻接点被访问的先后顺序,依次访问它们各自未被访问的邻接点,直到图中所有顶点都被访问过。
  • 数据结构:借助队列来实现。

5.4 最小生成树 (Minimum Spanning Tree, MST)

对于一个连通网(带权连通图),包含全部 $n$ 个顶点,且使其中 $n-1$ 条边上的权值之和达到最小的子图称为最小生成树。

1. 经典算法比较

算法名称 核心思想 时间复杂度 适用场景
Kruskal (克鲁斯卡尔) 算法 “加边法”:按权值从小到大选择 $n-1$ 条边,要求不能构成回路。 $O(e \log e)$ 适用于稀疏图
Prim (普里姆) 算法 “加点法”:从某一顶点开始,不断选择与当前树相邻且权值最小的边,将其另一端的顶点加入树中。 $O(n^2)$ 适用于稠密图

5.5 核心算法 C++ 代码实现

以下提供图的存储结构(邻接表)、DFS/BFS 遍历,以及 Kruskal 算法求最小生成树的完整 C++ 实现。

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;

// 边的结构体(用于 Kruskal 算法)
struct Edge {
    int u, v, weight;
    bool operator<(const Edge& other) const {
        return weight < other.weight;
    }
};

// 图的邻接表类
class Graph {
private:
    int numVertices;
    vector<vector<pair<int, int>>> adjList; // pair<邻接点, 边权>

public:
    Graph(int vertices) {
        numVertices = vertices;
        adjList.resize(vertices);
    }

    // 添加无向边
    void addEdge(int u, int v, int weight = 1) {
        adjList[u].push_back({v, weight});
        adjList[v].push_back({u, weight});
    }

    // 1. 深度优先搜索(DFS)辅助递归函数
    void DFSHelper(int v, vector<bool>& visited) {
        visited[v] = true;
        cout << "v" << v << " ";
        for (auto neighbor : adjList[v]) {
            int u = neighbor.first;
            if (!visited[u]) {
                DFSHelper(u, visited);
            }
        }
    }

    // 执行 DFS
    void DFS(int startVertex) {
        vector<bool> visited(numVertices, false);
        cout << "DFS 遍历结果: ";
        DFSHelper(startVertex, visited);
        cout << endl;
    }

    // 2. 广度优先搜索(BFS)
    void BFS(int startVertex) {
        vector<bool> visited(numVertices, false);
        queue<int> q;

        visited[startVertex] = true;
        q.push(startVertex);

        cout << "BFS 遍历结果: ";
        while (!q.empty()) {
            int v = q.front();
            q.pop();
            cout << "v" << v << " ";

            for (auto neighbor : adjList[v]) {
                int u = neighbor.first;
                if (!visited[u]) {
                    visited[u] = true;
                    q.push(u);
                }
            }
        }
        cout << endl;
    }
};

// 3. 并查集结构体(Kruskal 算法的连通性判断)
struct DisjointSet {
    vector<int> parent;
    DisjointSet(int n) {
        parent.resize(n);
        for (int i = 0; i < n; i++) parent[i] = i;
    }
    int find(int i) {
        if (parent[i] == i)
            return i;
        return parent[i] = find(parent[i]); // 路径压缩
    }
    bool unite(int i, int j) {
        int rootI = find(i);
        int rootJ = find(j);
        if (rootI != rootJ) {
            parent[rootI] = rootJ;
            return true;
        }
        return false;
    }
};

// 4. Kruskal 算法求最小生成树
void KruskalMST(int vertices, vector<Edge>& edges) {
    sort(edges.begin(), edges.end()); // 边权从小到大排序
    DisjointSet ds(vertices);
    vector<Edge> mst;
    int mstWeight = 0;

    for (const auto& edge : edges) {
        if (ds.unite(edge.u, edge.v)) {
            mst.push_back(edge);
            mstWeight += edge.weight;
        }
    }

    cout << "Kruskal 最小生成树边集: " << endl;
    for (const auto& edge : mst) {
        cout << "v" << edge.u << " - v" << edge.v << " : " << edge.weight << endl;
    }
    cout << "最小生成树总权值和为: " << mstWeight << endl;
}

int main() {
    // 构建图结构
    Graph g(6);
    g.addEdge(0, 1, 6);
    g.addEdge(0, 2, 1);
    g.addEdge(0, 3, 5);
    g.addEdge(1, 2, 5);
    g.addEdge(1, 4, 3);
    g.addEdge(2, 3, 5);
    g.addEdge(2, 4, 6);
    g.addEdge(2, 5, 4);
    g.addEdge(3, 5, 2);
    g.addEdge(4, 5, 6);

    g.DFS(0);
    g.BFS(0);

    // 最小生成树测试数据
    vector<Edge> edges = {
        {0, 1, 6}, {0, 2, 1}, {0, 3, 5},
        {1, 2, 5}, {1, 4, 3}, {2, 3, 5},
        {2, 4, 6}, {2, 5, 4}, {3, 5, 2},
        {4, 5, 6}
    };
    KruskalMST(6, edges);

    return 0;
}

5.6 练习题集

一、 填空题

  1. 设无向图 $G$ 中顶点数为 $n$,则图 $G$ 至少有______条边,至多有______条边;若 $G$ 为有向图,则至少有______条边,至多有______条边。
  2. 图的存储结构主要有两种,分别是______和______。
  3. 已知一个有向图的邻接矩阵表示,计算第 $j$ 个顶点的入度的方法是______。
  4. 有向图 $G$ 用邻接矩阵 $A[n][n]$ 存储,其第 $i$ 行的所有元素之和等于顶点 $v_i$ 的______。
  5. $n$ 个顶点的连通图用邻接矩阵表示时,该矩阵至少有______个非零元素。
  6. 表示一个有 $100$ 个顶点,$1000$ 条边的有向图的邻接矩阵有______个非零矩阵元素。
  7. 无向图中所有顶点的度数之和等于所有边数的______倍。
  8. 具有 $n$ 个顶点的无向完全图中包含有______条边,具有 $n$ 个顶点的有向完全图中包含有______条边。
  9. 一个具有 $n$ 个顶点的无向图中,要连通所有顶点则至少需要______条边。
  10. 对于一个具有 $n$ 个顶点和 $e$ 条边的连通图,其生成树中的顶点数和边数分别为______和______。
  11. 对于一个图 $G$ 的遍历,通常有两种方法,它们分别是______和______。

二、 选择题

  1. 在一个无向图中,所有顶点的度数之和等于所有边数的( )倍。 A. 1/2 B. 1 C. 2 D. 4

  2. 含 $n$ 个顶点的连通图中的任意一条简单路径,其长度(路径上的边数)不可能超过( )。 A. 1 B. $n/2$ C. $n-1$ D. $n$

  3. 对于一个具有 $n$ 个顶点的无向图,若采用邻接矩阵存储,则该矩阵的大小是( )。 A. $n$ B. $(n-1)^2$ C. $n-1$ D. $n^2$

  4. 关于图的生成树,下列说法正确的是( ),$n$ 个顶点的生成树有( )条边。 A. 唯一 B. 不唯一 C. 唯一性不能确定 D. $n$ E. $n+1$ F. $n-1$

  5. 【NOIP 2019 提高组】 $G$ 是一个非连通无向图(无重边和自环),共有 $28$ 条边,则该图至少有( )个顶点。 A. 6 B. 7 C. 8 D. 9

  6. 最小生成树指的是( )。 A. 由连通网所得到的边数最少的生成树 B. 由连通网所得到的顶点数相对较少的生成树 C. 连通网中所有生成树中权值之和为最小的生成树 D. 连通网的极小连通子图

  7. 某无向图的邻接矩阵 $A$ 大小为 $3 \times 3$ 且对角线元素全为 $0$,非对角线元素不全为 $0$,可以看出,该图共有( )个顶点。 A. 3 /B. 6 C. 9 D. 以上答案均不正确

  8. 在一个具有 $n$ 个顶点的有向完全图中包含有( )条边。 A. $\frac{n(n-1)}{2}$ B. $n(n-1)$ C. $\frac{n(n+1)}{2}$ D. $n^2$

  9. 在一个图中,所有顶点的度数之和等于所有边数的( )倍。 A. 1/2 B. 1 C. 2 D. 4

  10. 具有 $4$ 个顶点的无向完全图有( )条边。 A. 6 B. 12 C. 16 D. 20

  11. 具有 $6$ 个顶点的无向图至少应有( )条边才能确保是一个连通图。 A. 5 B. 6 C. 7 D. 8

  12. $n$ 个结点的完全有向图含有边的数目为( )。 A. $n^2$ B. $n(n+1)$ C. $n/2$ D. $n(n-1)$

  13. 有向图中一个顶点的度是该顶点的( )。 A. 入度 B. 出度 C. 入度与出度之和 D. (入度+出度)/2

  14. 在含 $n$ 个顶点和 $e$ 条边的无向图的邻接矩阵中,零元素的个数为( )。 A. $e$ B. $2e$ C. $n^2 - e$ D. $n^2 - 2e$


5.7 练习题答案与详细解析

一、 填空题 答案及解析

  1. $0$,$\frac{n(n-1)}{2}$,$0$,$n(n-1)$
    • 解析:无向图边集可为空(最少为 0 边),无向完全图边数最大为 $\frac{n(n-1)}{2}$;有向图最少为 0,最大(有向完全图)边数(弧数)为 $n(n-1)$。
  2. 邻接矩阵,邻接表
    • 解析:图的常见存储结构为二维数组(对稠密图更优的邻接矩阵)以及链式结构(对稀疏图更优的邻接表)。
  3. 求第 $j$ 列的所有元素之和
    • 解析:有向图邻接矩阵中,第 $j$ 列代表所有指向顶点 $v_j$ 的有向边,其和即为 $v_j$ 的入度。
  4. 出度
    • 解析:第 $i$ 行元素表示以 $v_i$ 为起点的有向边。这一行各元素值之和即为 $v_i$ 的出度。
  5. $2(n-1)$
    • 解析:$n$ 个顶点的连通无向图最少需要 $n-1$ 条边。在邻接矩阵中,因为无向图的边是对称存储的,每条边对应两个非零元素,因此邻接矩阵中非零元素个数至少为 $2(n-1)$。
  6. $1000$
    • 解析:有向图的一条边在邻接矩阵中只需占用一个非零元素位置,因此 $1000$ 条边对应 $1000$ 个非零元。
  7. $2$
    • 解析:每一条无向边均与两个顶点相连,因此会为图的总度数贡献 2,故所有顶点的度数之和等于边数的 2 倍。
  8. $\frac{n(n-1)}{2}$,$n(n-1)$
    • 解析:基本概念结论,分别对应完全无向图与完全有向图的最大边数。
  9. $n-1$
    • 解析:连通 $n$ 个顶点的无向极小连通图是一棵树,所含的边数为 $n-1$ 条。
  10. $n$,$n-1$
    • 解析:根据生成树定义,生成树必须包含原图的全部 $n$ 个顶点,且是用极小无回路的 $n-1$ 条边来保持连通。
  11. 深度优先搜索(DFS),广度优先搜索(BFS)

二、 选择题 答案及解析

  1. C
    • 解析:根据定理,度数之和 $= 2 \times$ 边数。
  2. C
    • 解析:简单路径中没有重复顶点。包含 $n$ 个顶点的连通图,简单路径上最多包含所有的 $n$ 个顶点,此时路径上的边数为 $n-1$ 条。
  3. D
    • 解析:邻接矩阵是大小为 $n \times n$ 的二维数组,共包含 $n^2$ 个元素。
  4. C,F
    • 解析:对于连通网,最小生成树一般不唯一(若各边权值不相同则唯一,这里是一般生成树);生成树所含边数固定为 $n-1$ 条。
  5. D
    • 解析:要让顶点数最少,应尽可能将 $28$ 条边放入一个完全无向图(连通的子图)中,最后再加上一个孤立顶点使其成为非连通图。 设连通子图有 $k$ 个顶点,其最多能容纳的边数为 $\frac{k(k-1)}{2}$ 条。 令 $\frac{k(k-1)}{2} \ge 28 \implies k(k-1) \ge 56$,解得 $k \ge 8$(因为 $8 \times 7 = 56$)。 这 $8$ 个顶点构成一个完全图能提供 28 条边。为使其非连通,需要再增加至少 1 个孤立顶点。所以该非连通图至少含有 $8 + 1 = 9$ 个顶点。
  6. C
    • 解析:生成树权值之和最小即为最小生成树(MST)。
  7. A
    • 解析:邻接矩阵的大小为 $n \times n$,其行数和列数即为顶点的个数。由于 $A$ 是 $3 \times 3$ 的矩阵,因此顶点数为 3。
  8. B
    • 解析:完全有向图的边数公式为 $n(n-1)$。
  9. C
    • 解析:任何图中,所有顶点的度数之和均为边数 $e$ 的 2 倍。
  10. A
    • 解析:$\frac{4 \times (4 - 1)}{2} = 6$ 条。
  11. A
    • 解析:具有 $n$ 个顶点的图,要确保能成为连通图,最少应具有极小连通子图(树)的边数,即 $n-1$ 条边。对于 6 个顶点的图,至少需要 5 条边才能确保其连通(一条链形式)。
  12. D
    • 解析:完全有向图含有 $n(n-1)$ 条弧。
  13. C
    • 解析:有向图中,顶点的度定义为指向该顶点的边(入度)和从该顶点指出的边(出度)之和。
  14. D
    • 解析:大小为 $n \times n$ 的邻接矩阵一共有 $n^2$ 个元素。由于这是一个无向图,每条无向边在矩阵中对称记录两次(即占用 2 个非零元位置),非零元素总数为 $2e$。因此零元素的个数为 $n^2 - 2e$。

—— 本文来自火龙信奥(义乌睿码科技):义乌青少年信息学奥赛与编程教育平台,专注 CSP-J/S、NOIP、GESP 竞赛培训,线上线下融合教学,助力编程升学。网址:hlcoding.com

关于火龙

  • 关于我们
  • 学员获奖
  • 预约试听
  • ACM课程
  • CSP课程
  • 学习指南

帮助中心

  • 用户协议
  • 打字练习
  • 在线画图
  • DevC++下载
  • CSP报名
  • GESP官网

推荐课程

  • C++零基础入门(可试看)
  • C++进阶提升
  • GESP考级辅导
  • GESP打卡
  • CSP-J/S打卡

公众号

火龙信奥公众号二维码

地址:义乌市北门街188号新天地商厦二楼2F 邮箱:wdlok305@126.com

© 2017-2026 义乌市睿码科技有限公司版权所有 浙ICP备2021013995号

火龙信奥
请输入登录信息


请完成安全验证
验证码底图 滑块
向右拖动滑块完成验证
请输入用户名 / 绑定的手机号码



请输入注册信息(手机号验证码注册)





验证码5分钟有效,60秒内不可重复获取,每日最多3次

微信登录

微信登录二维码

正在生成二维码...

账号已过期,请续期。
去续期

绑定手机号

📱

为了更好地保护您的账号安全,享受完整的平台服务

请您尽快绑定手机号码