【知识点】图与图论入门

· 浏览次数 : 0

小编点评

## 图论简介 图论(Graph Theory)是数学的一个分支,主要研究由若干给定的点及连接两点的线所构成的图形。这种图形通常用来描述某些事物之间的某种特定关系,其中点代表事物,连接两点的线表示相应两个事物间具有这种关系。 图论的应用非常广泛,包括计算机科学、网络分析、物流、社会网络分析等领域。 ## 图的基本概念 - **顶点(Node)**:图的基本单位,也称为节点。 - **边(Edge)**:连接两个顶点的线段或弧,可以是无向的,也可以是有向的。 - **无向图(Undirected Graph)**:图中的边没有方向,即 `(u, v)` 和 `(v, u)` 是同一条边。 - **有向图(Directed Graph/Digraph)**:图中的边有方向,即 `(u, v)` 和 `(v, u)` 不是同一条边。 - **简单图(Simple Graph)**:表示含有重边(两个顶点之间的多条边)和自环(顶点到自身的边)的图。 - **多重图(Multigraph)**:允许有重边和自环的图。 - **边权(Weight of an Edge)**:一般表示经过这一条边的代价,代价一般是由命题人定义的。 ## 图的表示方法 图可以通过邻接矩阵和邻接表两种方法来表示。 ### 邻接矩阵 若一个图中有 `N` 个顶点,那么可以用一个 `N × N` 的矩阵来表示这个图。矩阵的元素 `A[i, j]` 表示从节点 `i` 到节点 `j` 有一条有向边,边的权值为 `A[i, j]`。 ### 邻接表 邻接表本质上就是用链表表示图。数组的每个元素表示一个顶点,元素的值是一个链表,链表中存储该顶点的所有邻接顶点。 ## 图的基本操作 - **深度优先搜索(DFS)**:从某个顶点出发,遍历所有可达的顶点。 - **广度优先搜索(BFS)**:从某个顶点出发,按层次遍历所有可达的顶点。 - **Dijkstra 算法**:在加权图中寻找单源最短路径。 - **Bellman-Ford 算法**:在有负权边的图中寻找单源最短路径。 - **Floyd-Warshall 算法**:寻找所有顶点对之间的最短路径。 - **Kruskal 算法**:求解最小生成树(MST - Minimum Spanning Tree)。 - **Prim 算法**:求解最小生成树(MST - Minimum Spanning Tree)。 - **拓扑排序(Topological Sorting)**:适用于有向无环图(DAG),用于任务调度等应用。 - **Tarjan 算法**:用于求解图中的强连通分量、割点、桥。 ## 总结 图论是数学的一个重要分支,其研究内容包括图的基本概念、表示方法以及各种算法。图论的应用范围广泛,包括计算机科学、网络分析、物流、社会网络分析等领域。掌握图论的知识对于理解和解决实际问题具有重要价值。

正文

两三个星期没有发布新文章了,今天再来讲一个新的数据结构:

何为图论

见名知意,图论 (Graph Theory) 就是研究 图 (Graph) 的数学理论和方法。图是一种抽象的数据结构,由 节点 (Node) 和 连接这些节点的 边 (Edge) 组成。图论在计算机科学、网络分析、物流、社会网络分析等领域有广泛的应用。

如下,这就是一个图,可以看到这个图有 \(5\) 个顶点,分别编号为 \(\{0, 1, 2, 3, 4\}\)。同时这个图有 \(4\) 条边,例如,在顶点 \(2\) 和 顶点 \(4\) 之间存在着一条边。

image

图的基本概念

在详细讲解图论和有关图论算法之前,先来了解一下在图论中的一些基本表述和规范。

  1. 图 (Graph):图是一种由一组顶点和一组边组成的数据结构,记做 \(G = (V, E)\),其中 \(V\) 代表顶点集合,\(E\)​ 代表边集合。
  2. 顶点 (Vertex):顶点是图的基本单位,也称为节点。
  3. 边 (Edge):一条边是连接两个顶点的线段或弧。可以是无向的,也可以是有向的。一条边可以记做为 \((u, v)\)。在无向图中,若存在一条\((u, v)\),表示可以从 \(u\) 点直接走到 \(v\) 点,反之亦然。但若在有向图中,存在一条边 \((u, v)\),表示可以从 \(u\) 节点直接走向 \(v\) 节点,如果不存在一条边 \(v, u\),那么 \(v\) 节点就没有办法直接走向 \(u\) 节点。
  4. 无向图 (Undirected Graph):图中的边没有方向,即 \((u, v)\)\((v, u)\) 是同一条边。
  5. 有向图 (Directed Graph/ Digraph):图中的边有方向,即 \((u, v)\)\((v, u)\)​ 不是同一条边。
  6. 简单图 (Simple Graph):表示含有重边(两个顶点之间的多条边)和自环(顶点到自身的边)的图。
  7. 多重图 (Multigraph):允许有重边和自环的图。
  8. 边权 (Weight of an Edge):一般表示经过这一条边的代价(代价一般是由命题人定义的)。

如下图,就是一个有向的简单图(通常来说,在有向图中边的方向用箭头来表示):

image

如下图,就是一个无向的多重图,其中存在两条边可以从顶点 \(5\) 到顶点 \(2\)

image

与此同时,为了方便起见,对于无向图的处理,我们只需要在两个顶点之间建立两个方向相反的无向边就可以表示一个无向图,具体如下:

image

图的表示方法

在计算机中,图可以通过许多方式来构建和表示。总的可以分成图的邻接矩阵和邻接表两种方法(关于链式前向星本文不过多展开叙述,有兴趣的可以自行查阅相关文档)。

图的邻接矩阵 (Adjacency Matrix)

若一个图中有 \(N\) 个顶点,那么我们就可以用一个 \(N \times N\) 的矩阵来表示这个图。我们一般定义,若矩阵的元素 \(A_{i, j} \neq -\infty\) 表示从节点 \(i\)\(j\) 有一条有向边,其中边的权值为 \(A_{i, j}\)​。

假设存在一个有 \(3\) 个顶点的图,并且有三条有向边 \(E = \{(1, 2), (2, 3), (3, 2)\}\),那么就可以用邻接矩阵表示为:

\[G = \begin{bmatrix} & \mathtt{1} & \mathtt{2} & \mathtt{3}\\ \mathtt{1} & 0 & 1 & 0 \\ \mathtt{2} & 0 & 0 & 1 \\ \mathtt{3} & 0 & 1 & 0 \end{bmatrix} \]

画成可视化的图就长这个样子:

image

在 C++ 中,我们可以简单地用一个二维数组来表示:

// 定义一个矩阵。
int map[50][50];

// 将所有的边初始化为负无穷大。
for (int i=1; i<=50; i++)
    for (int j=1; j<=50; j++)
        map[i][j] = -0x7f7f7f7f;

// 建边,其中所有的边权为1。
map[1][2] = map[2][3] = map[3][2] = 1;

图的邻接表 (Adjacency List)

邻接表本质上就是用链表表示图。数组的每个元素表示一个顶点,元素的值是一个链表,链表中存储该顶点的所有邻接顶点。假设存在一个有 \(4\) 个顶点的图,并且有四条有向边 \(E = \{(1, 2), (2, 3), (3, 2), (3, 4)\}\),那么就可以用邻接表表示为:

image

画成可视化的图就长这个样子:

image

在 C++ 中,我们可以使用 STL模板库 中的 vector 来实现:

#include <vector>
vector<int> G[50];  // 建图。
G[1].push_back(2);
G[2].push_back(3);
G[3].push_back(2);
G[3].push_back(4);

一般情况下,推荐使用邻接表的方式来存图,因为使用邻接矩阵比较浪费空间。在顶点数量非常多但边非常少的图中,\(N^2\) 的时空复杂度会导致 MLE 或 TLE 等问题。

图的各种性质

  1. 度数 (Degree):一个顶点的度是连接该顶点的边的数量。在有向图中,有 入度 (Indegree)出度 (Outdegree) 之分(具体例子见后文)。
  2. 路径 (Path):从一个顶点到另一个顶点的顶点序列,路径上的边没有重复。
  3. 回路 (Cycle):起点和终点相同的路径。
  4. 连通图 (Connected Graph):任意两个顶点之间都有路径相连的无向图。
  5. 强连通图 (Strongly Connected Graph):任意两个顶点之间都有路径相连的有向图。

对于下面这个无向图不连通图,顶点 \(1\) 的度数为 \(1\);顶点 \(2\) 的度数为 \(2\);顶点 \(3\) 的度数为 \(1\);顶点 \(4\) 的度数为 \(0\)。同时,由于 \(4\) 号顶点没有度数,所以该顶点没有办法到达任何一个其他的顶点,所以这个图是一个不连通图:

image

如下图,就是一个有向不强连通图。其中,顶点 \(1\) 的入度为 \(0\),出度为 \(2\);顶点 \(2\) 的入度为 \(1\),出度也为 \(1\);顶点 \(3\) 的入度为 \(2\),但出度为 \(0\)。由于顶点 \(1\) 和顶点 \(2\) 可以走到顶点 \(3\),但顶点 \(3\) 没有办法走到顶点 \(1\) 或顶点 \(2\),因此下面的图不是一个强连通图:

image

对于下图来说,\(1\to 2\to 3\to 4\) 是一条从顶点 \(1\) 到顶点 \(4\)的路径。\(2\to 3\to 4 \to 2\to 3\) 就不是一个路径,因为相同的边 \((2, 3)\) 被多次走到了。\(1\to 2\to 3\to 1\) 就是一个回路,因为这个路径的起点和终点相同:

image

图的遍历

图通常采用 深度优先搜索/ 广度优先搜索 这两个算法来遍历。其中深度优先算法是最常见的遍历算法。

对于一个用 邻接矩阵 保存的图,其深度优先搜索遍历的 C++ 代码如下:

int vis[105], map[105][105];

void dfs(int node){
    if (vis[node]) return ;
    vis[node] = 1;
    cout << node << endl;
    for (int i=1; i<=n; i++)
        if (map[node][i] != -0x7f7f7f7f)
            dfs(i);
    return ;
}

// 函数调用:dfs(1); 表示从1号顶点开始遍历。

对于一个用 邻接表 保存的图,其深度优先搜索遍历的 C++ 代码如下:

#include <vector>
vector<int> G[105];
int vis[105];

void dfs(int node){
    if (vis[node]) return ;
    vis[node] = 1;
    cout << node << endl;
    for (int to : G[node])
        dfs(to);
    return ;
}

// 函数调用:dfs(1); 表示从1号顶点开始遍历。

广度优先搜索的方式也类似:

#include <queue>
vector<int> G[105];
int vis[105];

void bfs(int node){
    queue<int> que;
    que.push(node);
    while(!que.empty()){
        int t = que.front();
        cout << t << endl;
        que.pop();
        for (int to : G[node]){
        	if (!vis[to]) {
                vis[to] = 1;
                que.push(to);
            }
        }
    }
    return ;
}

// 函数调用:bfs(1); 表示从1号顶点开始遍历。

对于判断无向图的连通性,我们只需要从任意一个点开始跑一遍深搜或者广搜就行了。如果所有顶点的 vis 都被标记了,则证明图是联通的,否则图就是不连通的。

例题讲解

P3916 图的遍历

模板题目,从每一个顶点开始用深搜遍历一遍就可以了。但从每一个点考虑能走到的最大点比较麻烦,一个更优的解决办法是反向建边,从最大的点开始遍历,这样子就可以一次性计算出多个结果。

#include <iostream>
#include <algorithm>
#include <vector>
#include <cstring>
using namespace std;

const int N = 10005;
int n, m, ans, vis[N];
vector<int> G[N];

void dfs(int node, int d){
    if (vis[node]) return ;
    vis[node] = d;
    ans = max(node, ans);
    for (int to : G[node]) 
        dfs(to, d);
    return ;
}

int main(){
    cin >> n >> m;
    for (int i=0, u, v; i<m; i++){
		cin >> u >> v;
        G[v].push_back(u);  // 反向建边。
    }
    for (int i=n; i>=1; i--) dfs(i, i);
    for (int i=1; i<=n; i++) 
        cout << vis[i] << ' ';
    return 0;
}

P5318 【深基18.例3】查找文献

也是一道模板题目,正常遍历即可。

#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
using namespace std;
const int MAXN = 100005;

int n, m;
int vis1[MAXN], vis2[MAXN];
queue<int> que;
vector<int> G[MAXN];

void dfs(int node, int current){
    vis1[node] = 1;
    cout << node << ' ';
    if (current == n) return ;
    for (int i=0; i<G[node].size(); i++){
        if (vis1[G[node][i]]) continue;
        dfs(G[node][i], current+1);
    }
    return ;
}

void dfs(int node){
    vis2[node] = 1;
    que.push(node);
    while(que.size()){
        int t = que.front();
        cout << t << " ";
        for (int i=0; i<G[t].size(); i++){
            if (vis2[G[t][i]]) continue;
            vis2[G[t][i]] = 1;
            que.push(G[t][i]);
        }
        que.pop();
    }
    return ;
}

int main(){
    cin >> n >> m;
    for (int i=0; i<m; i++){
        int t1, t2;
        cin >> t1 >> t2;
        G[t1].push_back(t2);
    }
    for (int i=1; i<=n; i++) 
        sort(G[i].begin(), G[i].end());
    dfs(1, 0), cout << endl, dfs(1);
    return 0;
}

番外 - 图的常见算法

更多关于图论的算法,请持续关注后续更新。

  1. 深度优先搜索 (DFS):适用于遍历图和检测图中的回路。
  2. 广度优先搜索 (BFS):适用于寻找最短路径(无权图)。
  3. Dijkstra 算法:适用于加权图中寻找单源最短路径。
  4. Bellman-Ford 算法:适用于有负权边的图中寻找单源最短路径。
  5. Floyd-Warshall 算法:适用于寻找所有顶点对之间的最短路径。
  6. Kruskal 算法:用于求解最小生成树 (MST - Minimum Spanning Tree)。
  7. Prim 算法:另一种求解最小生成树的方法。
  8. 拓扑排序 (Topological Sorting):适用于有向无环图 (DAG),用于任务调度等应用。
  9. Tarjan 算法:用于求解图中的强连通分量、割点、桥。

与【知识点】图与图论入门相似的内容:

【知识点】图与图论入门

两三个星期没有发布新文章了,今天再来讲一个新的数据结构:图。 何为图论 见名知意,图论 (Graph Theory) 就是研究 图 (Graph) 的数学理论和方法。图是一种抽象的数据结构,由 节点 (Node) 和 连接这些节点的 边 (Edge) 组成。图论在计算机科学、网络分析、物流、社会网络

[转帖]Redis核心技术与实战

https://www.cnblogs.com/strick/p/14851429.html 最近在读一篇关于Redis的专栏,叫做《Redis核心技术与实战》,作者在Redis方面研究颇深,读后非常受益,特在此做记录。 一、Redis基础 1)知识图和问题画像图 Redis知识全景图都包括“两大维

12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?

本文已收录到 GitHub · AndroidFamily,有 Android 进阶知识体系,欢迎 Star。技术和职场问题,请关注公众号 [彭旭锐] 进 Android 面试交流群。 前言 大家好,我是小彭。 在上一篇文章里,我们聊到了 CPU 的三级缓存结构,提到 CPU 缓存就一定会聊到 CP

Langchain-Chatchat项目:1-整体介绍

基于Langchain与ChatGLM等语言模型的本地知识库问答应用实现。项目中默认LLM模型改为THUDM/chatglm2-6b[2],默认Embedding模型改为moka-ai/m3e-base[3]。 一.项目介绍 1.实现原理 本项目实现原理如下图所示,过程包括加载文件->读取文本->文

京东APP百亿级商品与车关系数据检索实践

本文主要讲解了京东百亿级商品车型适配数据存储结构设计以及怎样实现适配接口的高性能查询。通过京东百亿级数据缓存架构设计实践案例,简单剖析了jimdb的位图(bitmap)函数和lua脚本应用在高性能场景。希望通过本文,读者可以对缓存的内部结构知识有一定了解,并且能够以最小的内存使用代价将位图(bitmap)灵活应用到各个高性能实际场景。

原来Stable Diffusion是这样工作的

stable diffusion是一种潜在扩散模型,可以从文本生成人工智能图像。为什么叫做潜在扩散模型呢?这是因为与在高维图像空间中操作不同,它首先将图像压缩到潜在空间中,然后再进行操作。 在这篇文章中,我们将深入了解它到底是如何工作的,还能够知道文生图的工作方式与图生图的的工作方式有何不同?CFG

[转帖]《Linux性能优化实战》笔记(23)—— 内核线程 CPU 利用率过高,perf 与 火焰图

在排查网络问题时,我们还经常碰到的一个问题,就是内核线程的 CPU 使用率很高。比如,在高并发的场景中,内核线程 ksoftirqd 的 CPU 使用率通常就会比较高。回顾一下前面学过的 CPU 和网络模块,你应该知道,这是网络收发的软中断导致的。 要分析 ksoftirqd 这类 CPU 使用率比

《软件性能测试分析与调优实践之路》第二版-手稿节选-Mysql数据库性能定位与分析

在做MySQL数据的性能定位前,需要先知道MySQL查询时数据库内部的执行过程。只有弄清SQL的执行过程,才能对执行过程中的每一步的性能做定位分析。如图6-2-1所示。 图6-2-1 从图中可以看到,当查询出数据以后,会将数据先返回给执行器,此时执行器先将结果写到查询缓存里面,这样在下次查询相同的数

算法学习笔记(3): 倍增与ST算法

倍增 目录倍增查找 洛谷P2249重点变式练习快速幂ST表扩展 - 运算扩展 - 区间变式答案倍增更多的用法优化矩形查询优化建图优化 DP作者有话说 倍增,字面意思即”成倍增长“ 他与二分十分类似,都是基于”2“的划分思想 那么具体是怎么样,我们以一个例子来看 查找 洛谷P2249 依据题面,我们知

Python从零到壹丨图像增强及运算:图像掩膜直方图和HS直方图

摘要:本章主要讲解图像直方图相关知识点,包括掩膜直方图和HS直方图,并通过直方图判断黑夜与白天,通过案例分享直方图的实际应用。 本文分享自华为云社区《[Python从零到壹] 五十二.图像增强及运算篇之图像掩膜直方图和HS直方图》,作者: eastmount。 一.图像掩膜直方图 如果要统计图像的某