什么是图

在一个社交网络中,每个帐号和他们之间的关系构成了一张巨大的网络,就像下面这张图:

那么在电脑中,我们要用什么样的数据结构来保存这个网络呢?这个网络需要用一个之前课程里未提到过的数据结构,也就是接下来要讲解的结构来保存。

到底什么是图?图是由一系列顶点和若干连结顶点集合内两个顶点的边组成的数据结构。数学意义上的图,指的是由一系列点与边构成的集合,这里我们只考虑有限集。通常我们用 G=(V,E) 表示一个图结构,其中V表示点集,E表示边集。

在顶点集合所包含的若干个顶点之间,可能存在着某种两两关系——如果某两个点之间的确存在这样的关系的话,我们就在这两个点之间连边,这样就得到了边集的一个成员,也就是一条边。对应到社交网络中,顶点就是网络中的用户,边就是用户之间的好友关系。

如果用边来表示好友关系的话,对于微信这种双向关注的社交网络没有问题,但是对于微博这种单向关注的要如何表示呢?

于是引出了两个新的概念:有向边和无向边。

简而言之,一条有向边必然是从一个点指向另一个点,而相反方向的边在有向图中则不一定存在;而有的时候我们并不在意构成一条边的两个顶点具体谁先谁后,这样得到的一条边就是无向边。就像在微信中,AB的好友,那B也一定是A的好友,而在微博中,A关注B并不意味着B也一定关注A

对于图而言,如果图中所有边都是无向边,则称为无向图,反之称为有向图。

简而言之,无向图中的边是“好友”,而有向图中的边是“关注”。一般而言,我们在数据结构中所讨论的图都是有向图,因为有向图相比无向图更具有代表性。

实际上,无向图可以由有向图来表示。如果AB两个点之间存在无向边的话,那用有向图也可以表示为AB两点之间同时存在ABBA两条有向边。

仍然以社交网络举例:虽然微博中并不存在明确定义的好友关系,但是一般情况下,如果你和另一个 ID 互相关注的话,那么我们也可以近似认为,你和 TA 是好友。

我们来形式化地定义一下图:图是由顶点集合(简称点集)和顶点间的边(简称边集)组成的数据结构,通常用G(V,E)来表示。其中点集用V(G) 来表示,边集用 E(G) 来表示。在无向图中,边连接的两个顶点是无序的,这些边被称为无向边。例如下面这个无向图G,其点集V(G)={1,2,3,5,6},边集为E(G)={(1,2),(2,3),(1,5),(2,6),(5,6)}

而在有向图中,边连接的两个顶点之间是有序的。箭头的方向就表示有向边的方向。

例如下面这张有向图G’

其点集V(G′)={1,2,3,5,6},边集为E(G′)={(1,2),(2,3),(2,6),(6,5),(1,5)}。对于每条边 (u,v) ,我们称其为从u到v的一条有向边,u是这条有向边的起点v 是这条有向边的终点。注意在有向图中,(u,v)(v,u) 是不同的两条有向边。

图的分类



对于一个图,如果以任意一个点为起点,在图上沿着边走都可以到达其他所有点(有向图必须沿有向边的方向),那么这个图就是连通图。显然完全图一定是连通图。

定义

在无向图中,顶点的是指某个顶点连出的边数。例如在下图中,顶点 b 的度数为3,顶点 a 的度数为4。

在有向图中,和度对应的是入度出度这两个概念。顶点的入度是指以该顶点为终点的有向边数量;顶点的出度是指以顶点为起点的有向边数量。需要注意的是,在有向图里,顶点是没有的概念的。例如在下图中,顶点 a 的入度为1,出度为3;顶点 c 的入度为2,出度为2。

度的性质

在无向图或有向图中,顶点的度数总和为边数的两倍,即:

而在有向图中,有一个很明显的性质就是,入度等于出度
|
|
|
|
|
无向图度数统计

#include <iostream>
using namespace std;
int deg[105];
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v;cin >> u >> v;deg[u]++;deg[v]++;}for (int i = 1; i <= n; i++) {cout << deg[i] << " ";}return 0;
}

有向图度数统计

#include <iostream>
using namespace std;
int outdeg[105], indeg[105];
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v;cin >> u >> v;outdeg[u]++;indeg[v]++;}for (int i = 1; i <= n; i++) {cout << outdeg[i] << " " << indeg[i] << endl;}return 0;
}

图的存储(无权值)

图该怎么存呢?

邻接矩阵

基础知识

什么是邻接矩阵呢?所谓邻接矩阵存储结构就每个顶点用一个一维数组存储边的信息,这样所有点合起来就是用矩阵表示图中各顶点之间的邻接关系。所谓矩阵其实就是二维数组。
对于有n个顶点的图 G=(V,E) 来说,我们可以用一个 n×n 的矩阵 A 来表示 G 中各顶点的相邻关系,如果 vivj之间存在边(或弧),则 A[i][j]=1 ,否则 A[i][j]=0 。下图为有向图 G1 和无向图 G2 对应的邻接矩阵:

一个图的邻接矩阵是唯一的,矩阵的大小只与顶点个数N有关,是一个 N×N 的矩阵。前面我们已经介绍过,在无向图里,如果顶点 vivj 之间有边,则可认为顶点 vivj 有边,顶点 vjvi 也有边。对应到邻接矩阵里,则有 A[i][j]=A[j][i]=1 。因此我们可以发现,无向图的邻接矩阵是一个对称矩阵。

在邻接矩阵上,我们可以直观地看出两个顶点之间是否有边(或弧),并且很容易求出每个顶点的度,入度和出度。

这里我们以 G1 为例,演示下如何利用邻接矩阵计算顶点的入度和出度。顶点的出度,即为邻接矩阵上点对应行上所有值的总和,比如顶点1出度即为0+1+1+1=3;而每个点的入度即为点对应列上所有值的总和,比如顶点3对应的入度即为1+0+0+1=2。

接下来我们就先一起学习构造和使用邻接矩阵的方法。邻接矩阵是一个由1和0构成的矩阵。处于第 i 行、第 j 列上的元素1和0分别代表顶点i到j之间存在或不存在一条又向边。

显然在构造邻接矩阵的时候,我们需要实现一个整型的二维数组。由于当前的图还是空的,因此我们还要把这个二维数组中的每个元素都初始化为0。

在构造好了一个图的结构后,我们需要把图中各边的情况对应在邻接矩阵上。实际上,这一步的实现非常简单,当从顶点x到y存在边时,我们只要把二维数组对应的位置置为1就好了。

用邻接矩阵来构建图需要如下几步,我们可以用二维数组G来表示一个图。

初始化

初始化的过程很简单,只需要把数组初始化为0即可。可以借助memset来快速地将一个数组中的所有元素都初始化为0。(其实定义成全局变量就行了……)

memset(G, 0, sizeof(G));

注意,memset只能用来初始化0和 -1,并且需要加上头文件cstring

上面的代码等价于:

for (int i = 0; i < N1; i++) { // N1 为数组第一维大小for (int j = 0; j < N2; j++) { // N2 为数组第二维大小G[i][j] = 0;}
}

当然我们平常使用邻接矩阵的时候下标只用 1n 或者 0n−1 (这个看题目中点的编号)

插入边

如果插入一条无向边 (u,v) ,只需要

G[u][v] = 1;
G[v][u] = 1;

也可以写成G[u][v] = G[v][u] = 1;
如果插入一条有向边 (u,v) ,只需要G[u][v] = 1;

访问边

如果G[u][v] = 1,说明有一条从 uv 的边,否则没有从 uv 的边。

邻接矩阵的使用

#include <iostream>
using namespace std;
const int maxn = 105;
int G[maxn][maxn];
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v;cin >> u >> v;G[u][v] = G[v][u] = 1;}for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {cout << G[i][j] << " ";}cout << endl;}return 0;
}

邻接表

邻接表的思想是,对于图中的每一个顶点,用一个数组来记录这个点和哪些点相连。由于相邻的点会动态的添加,所以对于每个点,我们需要用vector来记录。

也就是对于每个点,我们都用一个vector来记录这个点和哪些点相连。比如对于一张有 10 个点的图,vector<int> G[11]就可以用来记录这张图了。对于一条从 ab 的有向边,我们通过G[a].push_back(b)就可以把这条边添加进去;如果是无向边,则需要在G[a].push_back(b)的同时G[b].push_back(a)

上图演示了一个图对应的邻接表。每一行的第一列表示的是最外层vector数组的下标。

邻接表的优缺点

优点

  1. 节省空间:当图的顶点数很多、但是边的数量很少时,如果用邻接矩阵,我们就需要开一个很大的二维数组,最后我们需要存储 n*n 个数。但是用邻接表,最后我们存储的数据量只是边数的两倍。
  2. 可以记录重复边:如果两个点之间有多条边,用邻接矩阵只能记录一条,但是用邻接表就能记录多条。虽然重复的边看起来是多余的,但在很多时候对解题来说是必要的。

缺点

当然,有优点就有缺点,用邻接表存图的最大缺点就是随机访问效率低。比如,我们需要询问点 a 是否和点 b 连,我们就要遍历G[a],检查这个vector里是否有 b 。而在邻接矩阵中,只需要根据G[a][b]就能判断。

因此,我们需要对不同的应用情景选择不同的存图方法。如果是稀疏图(顶点很多、边很少),一般用邻接表;如果是稠密图(顶点很少、边很多),一般用邻接矩阵。


当点数较多(多于 5000)时,使用邻接矩阵会超出空间限制,需要使用邻接表。

邻接表的实现

#include <iostream>
#include <vector>
using namespace std;
const int maxn = 105;
vector<int> G[maxn];
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v;cin >> u >> v;G[u].push_back(v);G[v].push_back(u);}for (int i = 1; i <= n; i++) {cout << i << " : ";for (int j = 0; j < G[i].size(); j++) {cout << G[i][j] << " ";}cout << endl;}return 0;
}

图的存储(带权值)

邻接矩阵

在前面,图中的边都只是用来表示两个点之间是否存在关系,而没有体现出两个点之间关系的强弱。比如在社交网络中,不能单纯地用0、1来表示两个人否为朋友。当两个人是朋友时,有可能是很好的朋友,也有可能是一般的朋友,还有可能是不熟悉的朋友。

我们用一个数值来表示两个人之间的朋友关系强弱,两个人的朋友关系越强,对应的值就越大。而这个值就是两个人在图中对应的边的权值,简称边权。对应的图我们称之为带权图

如下就是一个带权图,我们把每条边对应的边权标记在边上:
带权图也分成带权有向图和带权无向图。前面学到的关于图的性质在带权图上同样成立。实际上,我们前面学习的图是一种特殊带权图,只不过图中所有边的权值只有1一种;而在带权图中,边的权值可以是任意的。

用邻接矩阵存储带权图和之前的方法一样,用G[a][b]来表示 ab 之间的边权(我们需要用一个数值来表示边不存在,如0)。同样,对于无向图,这个矩阵依然是对称的。

如上所示,左边的图对应的右边的邻接矩阵

带权图的邻接矩阵

#include <iostream>
#include <cstring>
using namespace std;
const int maxn = 105;
int G[maxn][maxn];
int sum[maxn];
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v, w;cin >> u >> v >> w;G[u][v] = w;}for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {cout << G[i][j] << " ";}cout << endl;}for (int i = 1; i <= n; i++) {for (int j = 1; j <= n; j++) {sum[i] += G[i][j];}}for (int i = 1; i <= n; i++) {cout << sum[i] << " ";}return 0;
}

邻接表

用邻接表存储带权图和之前的实现方式略有区别,我们需要用一个结构体来记录一条边连接的点和这条边的边权,然后用一个vector来存储若干个结构体,实际上就是把一个点所连接的点以及这条边的边权"打包"起来放到邻接表中。

结构体的定义举例如下:

struct node {int v;  // 用来记录连接的点int w;  // 用来记录这条边的边权
};

我们通常把有向图中加入一条边写成一个函数,例如加入一条有向边 (u,v) 、边权为 w ,就可以用如下的函数来实现(我们需要把图定义成全局变量)。

vector<node> G[105];
// 插入有向边
void insert(int u, int v, int w) {node temp;temp.v = v;temp.w = w;G[u].push_back(temp);
}

而插入一条无向边,实际上相当于插入两条方向相反的有向边:

// 插入无向边
void insert2(int u, int v, int w) {insert(u, v, w);insert(v, u, w);
}

带权图邻接表的实现

#include <iostream>
#include <vector>
using namespace std;
const int maxn = 105;
struct node {int v;int w;
};
vector<node> G[maxn];
void insert(int u, int v, int w) {node temp;temp.v = v;temp.w = w;G[u].push_back(temp);
}
int main() {int n, m;cin >> n >> m;for (int i = 0; i < m; i++) {int u, v, w;cin >> u >> v >> w;insert(u, v, w);}for (int i = 1; i <= n; i++) {for (int j = 0; j < G[i].size(); j++) {cout << i << " " << G[i][j].v << " " << G[i][j].w << endl;}}return 0;
}

终于写完了……累死我了……

欢迎大家指出错误或给出建议
欢迎大家指出错误或给出建议
欢迎大家指出错误或给出建议
重要的事说三遍……

图论基础(自认为很全)相关推荐

  1. Linux常用基础命令(很全)

    Linux常用基础命令 Linux命令格式 Linux 常见用户管理命令 创建新用户 Linux文件及目录管理命令 绝对路径和相对路径 cd命令 ls命令 pwd命令 mkdir命令 touch命令 ...

  2. 很全面的5G技术基础知识PPT(三)

    本文是很全面的5G技术基础知识PPT(二)的接续 参考链接:https://blog.csdn.net/weixin_41486034/article/details/106302233 40 5G网 ...

  3. 很全面的5G技术基础知识PPT(一)

    最近研究5G相关技术,边看边分享,大家要多多关注和点赞哈,这是一份PPT,全面了解5G技术基础知识,什么是5G?有哪些关键技术?应用场景等等,看完这份PPT你就明白了.这份PPT共有100页,我将分3 ...

  4. 【数据结构与算法】图论基础与图存储结构

    前言 由于后续更新好几篇文章都涉及到 图 这种数据结构,因此打算先普及一下 图 的相关理论支持,如果后面的相关内容有些点不太容易理解,可以查阅此篇文章.本文不建议一口气阅读完毕,可以先浏览一遍,在后续 ...

  5. python很全的爬虫入门教程

    python很全的爬虫入门教程 一.爬虫前的准备工作 首先,我们要知道什么是爬虫 1.什么是网络爬虫? 网络爬虫是一种按照一定的规则,自动的抓取万维网信息的程序或者脚本.另外一些不常使用的名字还有蚂蚁 ...

  6. 零基础学python pdf-零基础学Python PDF 全彩影印版

    给大家带来的一篇关于python编程相关的电子书资源,介绍了关于零基础学Python.明日科技方面的内容,本书是由吉林大学出版社出版,格式为PDF,资源大小314 MB,明日科技编写,目前豆瓣.亚马逊 ...

  7. 史上很全的注册表修改大全

    史上很全的注册表修改大全 请使用 Ctrl+F 键来查找你想要的 不想别人使用你电脑的某些功能,或是在网吧想使用某些功能,或是让自己的电脑速度更快 怎么办 如果你有点注册表的知识就OK拉 没有多深奥 ...

  8. [分布式控制] (4) 刚性图论基础和仿射编队

    [分布式控制] (4) 刚性图论基础和仿射编队 [分布式控制] (4) 刚性图论基础和仿射编队 0 概念 1 一般的(Generic) 2 等价与全等(Equivalent and Congruent ...

  9. python编程入门指南明日科技pdf-零基础学Python PDF 全彩影印版

    给大家带来的一篇关于python编程相关的电子书资源,介绍了关于零基础学Python.明日科技方面的内容,本书是由吉林大学出版社出版,格式为PDF,资源大小314 MB,明日科技编写,目前豆瓣.亚马逊 ...

  10. [分布式控制浅述] (1) 图论基础

    [分布式控制浅述] (1) 图论基础 [分布式控制浅述] (1) 图论基础 1 前言 2 概述 3 度矩阵(Degree Matrix) 4 拉普拉斯矩阵(Laplacian Matrix) 5 特殊 ...

最新文章

  1. 网络化楼宇对讲的前景
  2. Android Screen Monitor抓取模拟器与真机界面
  3. 如何在Marketing Cloud里创建extension field扩展字
  4. Windows10+Ubuntu 18.04.2+ROS 安装笔记(SSD单硬盘)下
  5. 如何让apache支持3gp下载
  6. 6月21日武汉见!华为nova 5正式官宣:麒麟980+40W快充
  7. java容易搞混的技术_Java中比较容易混淆的知识点
  8. 常见的几种 RuntimeException
  9. python search函数_python 正则表达式 (search与match函数)
  10. html制作小短片,教你制作微视频 一分钟成为电影大师!
  11. 【计算机网络】第六部分 应用层(26) 远程登录、电子邮件与文件传输
  12. 高等代数 线性映射(第9章)1 概念,运算,核与象
  13. JavaScript:异步简介与Promise实践拓展
  14. android 9.0 app应用安装白名单
  15. MySQL 单个数据库备份还原
  16. python怎么解题_农夫过河问题 Python实现解题
  17. 外包公司面试门槛高吗?软件测试员进外包公司容易吗?
  18. 湘潭大学c语言作业系统学生端,作业收发系统学生端使用说明
  19. 交换机和路由器的区别?
  20. SONY CR13 实战 windows7

热门文章

  1. GAN对抗网络相关资源
  2. JAVA 之 GC 一
  3. c语言noj答案,NOJ答案c++版.docx
  4. 音频服务器未响应怎么修复,win10音频服务未响应怎么办?win10音频服务未响应的修复方法...
  5. 《Elasticsearch:权威指南》Query DSL -- Term-level queries --Terms Query
  6. 西安交大计算机考研软件工程编程题库(十八)
  7. Python统计单词数量
  8. ESP8266-Arduino编程实例-MAX6675冷端补偿K热电偶数字转换器驱动
  9. FB个人账户被封(受限),如何处理?
  10. 网关和路由,路由的分类,route命令