简单の暑假总结——图论基础

4.1 图论概念

图论是一个极为重要的知识点,我们需要一些篇幅去介绍它

本篇主要是一些基础知识

图的概念很多,听我细细道来

4.1.1 什么是图

截至目前,我们主要学了 3 3 3 种数据结构(不含图)

  1. 集合(变量应该算吧)

在这里插入图片描述

就是一群散点,元素与元素间没有什么联系

  1. 线性表(数组,队列,栈……)

在这里插入图片描述

如图,它们所组成的结构类似一条线,因此被称为线性结构

  1. 树形结构

在这里插入图片描述

如图,这种结构长的像颗树,因此被称为树形结构

好吧我也不知道为啥叫树形结构

那么,图应该是我们所学的第 4 4 4 种数据结构了

在这里插入图片描述

那么,我们现在要给图下一个定义了

很简单,点用边连起来就叫做图,严格意义上讲,图是一种数据结构,定义为 g r a p h = (   V , E   ) graph=(\ V,E\ ) graph=( V,E ) . V V V 是一个非空优先集合,代表顶点(结点), E E E 代表边的集合。——《一本通》

图(Graph) 描述的是一些个体之间的关系。和线性表和二叉树不同的是:这些个体之间既不是前驱后继的顺序关系,也不是祖先后代的层次关系,而是错综复杂的网状关系。——《算法经典》

其实,我们只需要知道:

  1. 图描述的是各个元素之间的关系(废话)
  2. 图的关系很复杂

特殊的,线性表和树形结构也是一种图

4.1.2 图的分类

图分为三类:无向图,有向图和带权图

4.1.2.1 无向图

顾名思义,一个没方向的图,即边没有指定方向的图

举个栗子:在下图就有一张无向图

在这里插入图片描述

那么,对于无向图,下面有一些术语(一般来讲,xie’e的出题人会直接在题干中使用术语,请务必理解):

在无向图中,如果两个顶点之间有边连接,那么就视为两个顶点相邻

举个栗子:在上图中,我们可以认为 1 1 1 号结点和 2 2 2 号结点相邻,但 1 1 1 号结点和第 3 3 3 号结点不相邻

那么,对于相邻顶点的序列,我们将其称为路径

举个栗子:在上图中,我们可以认为 1 − 2 − 3 − 4 1-2-3-4 1234 是一条路径

特殊的:起点和终点重合的路径叫做(毕竟长的像)

举个栗子:在上图中,我们可以认为 1 − 2 − 3 − 4 − 1 1-2-3-4-1 12341 是一个圈

对于一个单个顶点,该顶点连接的边数叫做该顶点的

举个栗子:在上图中,我们可以认为 1 1 1 号结点的度为 2 2 2 ,而 2 2 2 号结点的度为 3 3 3

对于各种类型的图而言,任意两点之间都有路径连接的图称为连通图,反之,称为非连通图

举个栗子:上图就是连通图,而下图则是非连通图

在这里插入图片描述
这里,教一个装B的小技巧

我们可以将树称为没有圈的连通图,森林称为没有圈的非连通图

结合树的定义,应该好理解吧?

主要是我也不想写

4.1.2.2 有向图

顾名思义,一个有方向的图,即边有指定方向的图

在这里插入图片描述

特殊的:有向图中的边又称为,起点称为弧头,终点称为弧尾

那么,对于有向图,下面有一些术语:

在有向图中,边是单向的:每条边所连接的两个顶点之间的邻接性是单向的。

举个栗子:在上图中,我们可以认为 1 1 1 号结点和 2 2 2 号结点相邻 ,但 2 2 2 号结点不和 1 1 1 号结点相邻

那么,对于相邻顶点的序列,我们将其称为有向路径

举个栗子:在上图中,我们可以认为 1 − 2 − 3 − 4 1-2-3-4 1234 是一条有向路径

特殊的:一条至少含有一条边且起点和终点相同的有向路径叫做有向环

举个栗子:在上图中,我们可以认为 2 − 3 − 4 − 2 2-3-4-2 2342 是一条有向环

注意:为什么定义里会说至少含有一条边呢?路径不应该是两条起步吗?

对于一些聪(hun)明(zhang)出题人,可能会出现这样的图:

在这里插入图片描述

是的,自己连自己

在这种情况下,我们也认为这是一个有向环

特殊的:我们把没有环的有向图称为有向无环图(DAG)

举个栗子:下图就是一个 DAG

在这里插入图片描述

Tips:在题目中,出题人可能会直接写 DAG ,而非有向无环图

在有向图中,度被分为了入度出度

通俗来讲,一个顶点的入度指最终指向该顶点的边的数量,出度指从该顶点指出去的边的数量

举个栗子:在上图中,我们可以认为 2 2 2 号结点的入度为 1 1 1 ,而 出度为 2 2 2

4.1.2.3 带权图

带权图,指边上带有权值的图(不同问题中,权值意义不同,可以是距离、时间、价格、代价等)

下图就是一张带权图
在这里插入图片描述

关于带权图,暂时不需要介绍术语,掌握无向图和有向图的术语即可

4.2 图的存储方式

图的存储方式常见的有三种:邻接矩阵,邻接表和链式前向星

4.2.1 邻接矩阵

个人认为邻接矩阵是最简单的一种存储图的方式

对于有 n n n 个顶点的图,我们可以采用 f l a g [   n   ] [   n   ] flag[\ n\ ][\ n\ ] flag[ n ][ n ] 这样一个数组来表示它

具体含义:若 f l a g [   i   ] [   j   ] = 1 flag[\ i\ ][\ j\ ]=1 flag[ i ][ j ]=1 说明 i i i j j j 之间有一条连线; 若 f l a g [   i   ] [   j   ] = 0 flag[\ i\ ][\ j\ ]=0 flag[ i ][ j ]=0 说明 i i i j j j 之间没有一条连线

特殊的:在无向图中, f l a g [   i   ] [   j   ] = f l a g [   j   ] [   i   ] flag[\ i\ ][\ j\ ]=flag[\ j\ ][\ i\ ] flag[ i ][ j ]=flag[ j ][ i ]

举个栗子:

在这里插入图片描述

在上图中,如果我们用一张表来表示 f l a g flag flag 数组,应该是如下图所示:

[ NULL ⁡ j = 1 j = 2 j = 3 i = 4   i = 1 0 1 0 1   i = 2 1 0 1 1   i = 3 0 1 0 1   i = 4 1 1 1 0 ] \begin{bmatrix}\operatorname{NULL}&j=1&j=2&j=3&i=4\\\ i=1&0&1&0&1\\\ i=2&1&0&1&1\\\ i=3&0&1&0&1\\\ i=4&1&1&1&0\end{bmatrix} NULL i=1 i=2 i=3 i=4j=10101j=21011j=30101i=41110

同样,有向图和带权图也能用邻接矩阵来表示:

在这里插入图片描述

对于上图的有向图, f l a g flag flag 数组如下图所示:

[ NULL ⁡ j = 1 j = 2 j = 3 j = 4   i = 1 0 1 0 1   i = 2 0 0 1 1   i = 3 0 0 0 1   i = 4 0 0 0 0 ] \begin{bmatrix}\operatorname{NULL}&j=1&j=2&j=3&j=4\\\ i=1&0&1&0&1\\\ i=2&0&0&1&1\\\ i=3&0&0&0&1\\\ i=4&0&0&0&0\end{bmatrix} NULL i=1 i=2 i=3 i=4j=10000j=21000j=30100j=41110

在有向图的邻接矩阵中:顶点 i i i 的出度为:第 i i i 所有非零元素的个数

显然,因为在有向图的邻接矩阵中, f l a g [   i   ] [   j   ] flag[\ i\ ][\ j\ ] flag[ i ][ j ] 表示 i i i 为起点, j j j 为终点,是否存在一条边,则第 i i i 行所有非零元素的个数就代表有多少个点从第 i i i 号点出发的,即 i i i 号点的出度

同理可得:在有向图的邻接矩阵中:顶点 i i i 的入度为:第 i i i 所有非零元素的个数

考虑一个带权图:

在这里插入图片描述

在上图中,如果我们用一张表来表示 f l a g flag flag 数组,应该是如下图所示:

[ NULL ⁡ j = 1 j = 2 j = 3 j = 4   i = 1 ∞ 1 ∞ 4   i = 2 1 ∞ 2 5   i = 3 ∞ 2 ∞ 3   i = 4 4 5 3 ∞ ] \begin{bmatrix}\operatorname{NULL}&j=1&j=2&j=3&j=4\\\ i=1&\infty&1&\infty&4\\\ i=2&1&\infty&2&5\\\ i=3&\infty&2&\infty&3\\\ i=4&4&5&3&\infty\end{bmatrix} NULL i=1 i=2 i=3 i=4j=114j=2125j=323j=4453

其中如果 f l a g [   i   ] [   j   ] = ∞ flag[\ i\ ][\ j\ ]=\infty flag[ i ][ j ]= 就说明 i i i 号顶点和 j j j 号顶点间不存在一条边;反之,说明 i i i 号顶点和 j j j 号顶点间存在一条边,并用 f l a g [   i   ] [   j   ] flag[\ i\ ][\ j\ ] flag[ i ][ j ] 记录其边权

Eg_1 邻接矩阵存储图

这是一道裸题

#include<cstdio>
#include<algorithm>
using namespace std;
bool a[2005][2005];			//邻接矩阵
int main(){
	int n,m,x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		a[x][y]=a[y][x]=1;			//因为是无向图,所以需要双向存边
	}
	for(int i=1;i<=n;i++){			//一次枚举每个点
		for(int j=1;j<=n;j++){
			if(a[i][j]==1){			//判断是否有边相连
				printf("%d ",j);
			}
		}
		printf("\n");
	}
	return 0;
}

在用邻接矩阵存储图中,我们可以 O ( 1 ) O(1) O(1) 的时间复杂度判断 i i i j j j 之间是否有边相连,但是,如果我们存储的是一个稀疏图(点多边少的图),就十分浪费空间,而且,在查找最短路中,也很浪费时间

所以,我们需要另外的方法存储图

4.2.2 邻接表

邻接表是最常用的存储图的方式,也是链式前向星的基础,相当重要

我们一般通过链表的方式(即使用不定长数组)实现邻接表

当然,由于我对结构体深沉的爱,我一般是通过结构体

以下有关邻接表的做法全部是通过结构体实现的,如果你不想看,Go to here

在结构体中 a [   i   ] a[\ i\ ] a[ i ] 中,可以定义一个 l e n len len 变量和一个 s u m sum sum 数组,其中,用 s u m sum sum 数组来表示与第 i i i 号元素所相邻的元素, l e n len len 表示 s u m sum sum 数组的长度,即有多少个元素与第 i i i 号元素相邻

举个栗子:

在这里插入图片描述

如果我们要通过邻接表来存储上图,那么,实现结果应如下图所示:

a [   1   ] . l e n = 2 , a [   1   ] . s u m [   2   ] = { 2 , 4 } a [   2   ] . l e n = 3 , a [   2   ] . s u m [   3   ] = { 1 , 3 , 4 } a [   3   ] . l e n = 2 , a [   3   ] . s u m [   2   ] = { 2 , 4 } a [   4   ] . l e n = 3 , a [   4   ] . s u m [   3   ] = { 1 , 2 , 3 } a[\ 1\ ].len=2,a[\ 1\ ].sum[\ 2\ ]=\{2,4\}\\a[\ 2\ ].len=3,a[\ 2\ ].sum[\ 3\ ]=\{1,3,4\}\\a[\ 3\ ].len=2,a[\ 3\ ].sum[\ 2\ ]=\{2,4\}\\a[\ 4\ ].len=3,a[\ 4\ ].sum[\ 3\ ]=\{1,2,3\} a[ 1 ].len=2,a[ 1 ].sum[ 2 ]={2,4}a[ 2 ].len=3,a[ 2 ].sum[ 3 ]={1,3,4}a[ 3 ].len=2,a[ 3 ].sum[ 2 ]={2,4}a[ 4 ].len=3,a[ 4 ].sum[ 3 ]={1,2,3}

用邻接表存储有向图的方法与存储无向图的方法大同小异,不再赘述

那么,如果我们要存储一个带权图呢?

那么,我们就需要将 a a a 数组里的 s u m sum sum 数组定义成结构体数组(恐怕只有我这种天才才想得出结构体套结构体的做法吧?

那么,在 s u m [   i   ] sum[\ i\ ] sum[ i ] 中,我们包含了两个信息: s u m sum sum n u m num num ,其中, n u m num num 表示当前元素的下标, s u m sum sum 表示该元素与第 i i i 号元素的权值

再举一个栗子:

在这里插入图片描述
如果我们要通过邻接表来存储上图,那么,实现结果应如下图所示:

a [   1   ] . l e n = 2 , a [   1   ] . s u m [   2   ] . n u m = { 2 , 4 } ,      a [   1   ] . s u m [   2   ] . s u m = { 1 , 4 } a [   2   ] . l e n = 3 , a [   2   ] . s u m [   3   ] . n u m = { 1 , 3 , 4 } , a [   2   ] . s u m [   3   ] . s u m = { 1 , 2 , 5 } a [   3   ] . l e n = 2 , a [   3   ] . s u m [   2   ] . n u m = { 2 , 4 } ,      a [   3   ] . s u m [   2   ] . s u m = { 2 , 3 } a [   4   ] . l e n = 3 , a [   4   ] . s u m [   3   ] . n u m = { 1 , 2 , 3 } , a [   4   ] . s u m [   3   ] . s u m = { 4 , 5 , 3 } a[\ 1\ ].len=2,a[\ 1\ ].sum[\ 2\ ].num=\{2,4\},\ \ \ \ a[\ 1\ ].sum[\ 2\ ].sum=\{1,4\}\\a[\ 2\ ].len=3,a[\ 2\ ].sum[\ 3\ ].num=\{1,3,4\},a[\ 2\ ].sum[\ 3\ ].sum=\{1,2,5\}\\a[\ 3\ ].len=2,a[\ 3\ ].sum[\ 2\ ].num=\{2,4\},\ \ \ \ a[\ 3\ ].sum[\ 2\ ].sum=\{2,3\}\\a[\ 4\ ].len=3,a[\ 4\ ].sum[\ 3\ ].num=\{1,2,3\},a[\ 4\ ].sum[\ 3\ ].sum=\{4,5,3\} a[ 1 ].len=2,a[ 1 ].sum[ 2 ].num={2,4},    a[ 1 ].sum[ 2 ].sum={1,4}a[ 2 ].len=3,a[ 2 ].sum[ 3 ].num={1,3,4},a[ 2 ].sum[ 3 ].sum={1,2,5}a[ 3 ].len=2,a[ 3 ].sum[ 2 ].num={2,4},    a[ 3 ].sum[ 2 ].sum={2,3}a[ 4 ].len=3,a[ 4 ].sum[ 3 ].num={1,2,3},a[ 4 ].sum[ 3 ].sum={4,5,3}

Eg_2 邻接表存储图

邻接点按照度数由小到大输出,如果度数相等,则按照编号有小到大输出。

右上,我们还需要在 a a a 数组中添加一个信息: s i z e size size ,表示度数

具体详情见代码:

#include<cstdio>
#include<algorithm>
using namespace std;
struct node{
	int sum[1005],len,size;
}a[1005];
bool cmp(int x,int y){			//排序,以度数为第一关键字,数值为第二关键字
	if(a[x].size!=a[y].size){
		return a[x].size<a[y].size;
	}
	return x<y;
}
int main(){
	int n,m,x,y;
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		a[x].len++;
		a[x].sum[a[x].len]=y;			//处理新增的与第 i 号点连接的点
		a[x].size++;			//处理度数
		a[y].len++;
		a[y].sum[a[y].len]=x;
		a[y].size++;			//无向图,双向处理
	}
	for(int i=1;i<=n;i++){
		sort(a[i].sum+1,a[i].sum+1+a[i].len,cmp);			//排序
		for(int j=1;j<=a[i].len;j++){
			printf("%d ",a[i].sum[j]);			//输出
		}
		printf("\n");
	}
	return 0;
}

4.2.3 链式前向星

上文已提,真正的邻接表是由不定长数组实现的,而链式前向星则是由数组

具体详情Go to here

其实我也不太懂链式前向星,不敢瞎BB

4.3 图的遍历

图的遍历有两种:DFS和BFS

如果就这还要详细讲解,你可以回家种田了

这里,在此给大家推荐一个强大的网址:

程序可视化

这是一个强大的网站,大家可以自行探索

我真的不是不想写!

那么,我们来看两道例题:

Eg_3 有向图的DFS

数据范围不大,可以采用邻接矩阵输入,操作起来也方便

注意:这里是有向图,不是无向图

#include<cstdio>
bool a[205][205];
bool flag[205];
int n,m,x,y;
void dfs(int num){
	printf("%d ",num);
	flag[num]=1;			//标记为已遍历
	for(int i=1;i<=n;i++){			//要求字典序最小,所以要按照顺序
		if(a[num][i]==1&&flag[i]==0){			//如果两点之间有边连接且第 i 好点未被遍历
			dfs(i);			//遍历
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		a[x][y]=1;			//建图
	}
	for(int i=1;i<=n;i++){			//要求字典序最小,所以要按照顺序
		if(flag[i]==0){			//注意:这里不一定是连通图,所以,凡是还没有被遍历到的都要在进行一次DFS
			dfs(i);
		}
	}
	return 0;
}

Eg_4 有向图的BFS

#include<queue>
#include<cstdio>
using namespace std;
bool a[205][205];
bool flag[205];
int n,m,x,y;
void bfs(int num){
	printf("%d ",num);			//输出当前元素
	flag[num]=1;
	queue<int> q;
	q.push(num);
	while(!q.empty()){
		int xx=q.front();
		q.pop();			//以上为BFS基本操作
		for(int i=1;i<=n;i++){
			if(a[xx][i]&&!flag[i]){			//如果两点间有边连接且第 i 号点未被遍历
				q.push(i);
				printf("%d ",i);
				flag[i]=1;			//标记,输出,压队列
			}
		}
	}
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&x,&y);
		a[x][y]=1;			//采用邻接矩阵建图
	}
	for(int i=1;i<=n;i++){			//与上题类似
		if(flag[i]==0){			//与上题相类似,要考虑非连通图
			bfs(i);
		}
	}
	return 0;
}

栗子:你在这篇文章中一共举了我 14 14 14 次,你礼貌吗?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值