BFS 的数学分析(一)
by Amamiya Fuko
盈月无缺亏似笑,静卧天穹看星灼
引言
Wakuuuu!🌕Fuko参上!(其实这段是想打星星emoji的,但是找不到对应的代码)这篇博客是对我写的上一篇同名博客的重构,上篇博客其实是名副其实的失败品啊(划掉),但是优快云质量分居然有80(奇迹发生了!!!),这使我感到数学分析好像确实十分大概也许还是挺 冷门的 adj.(话说高数不是理科必学么),所以很积极地写了第二篇(指隔了好几天),因为想要尝试更有风格的写法所以会说很多废话(把废话去掉就不剩什么的文章有什么必要单独写一篇么)总之请多多包涵说是(鞠躬)。
目录
- 引入
- 图的三元组表示方法
- 图的二元组表示方法
- 图的邻接链表表示法
- 图的关联矩阵
引入
这里是引入,为了防止你对基础知识一点都不了解就被后面的内容劝退,所以会介绍一些基本的知识,科班出身的小伙伴们倒是应该可以跳过这一部分(不过科班出身的朋友也不至于要到看这篇博文的程度吧,这好像有点不对),毕竟程序员是问题导向的生物(其实是因为自己是这个样子)。
图的数学表示方法有两种,一种是使用二元组,另外一种是使用三元组。
图的三元组表示法
这里先说图的三元组表示法,它由图的顶点集、边集和顶点与边的关联函数组成,符号表示为下,其中G为图,V为顶点集(发出神尾观铃的声音✌️),E为边集,I为关联函数。
G
=
(
V
,
E
,
I
)
G=(V,E,I)
G=(V,E,I)
这看起来是一个十分别扭的表示方法,因为我们大多数时候使用两个顶点的二元组就可以表示一个边了(在邻接链表的表示方法里,我们可以通过一个顶点得到另一个顶点,进而就可以得到一个边),不过还有一种情况是,边本身是有值的,而这也是关联函数的意义。
I
(
v
i
,
v
j
)
=
a
,
(
v
i
,
v
j
∈
V
,
a
∈
E
)
I(v_i,v_j)=a,(v_i,v_j \in V,a \in E)
I(vi,vj)=a,(vi,vj∈V,a∈E)
邻接矩阵可以视作是图的三元组表达,列如我们有邻接矩阵
A
=
A
(
G
)
A=A(G)
A=A(G)
A
=
{
a
1
a
2
a
3
a
4
a
5
a
6
a
7
a
8
a
9
}
A= \begin{Bmatrix} a_1 & a_2 & a_3 \\ a_4 & a_5 & a_6 \\ a_7 & a_8 & a_9 \end{Bmatrix}
A=⎩
⎨
⎧a1a4a7a2a5a8a3a6a9⎭
⎬
⎫
其中邻接矩阵A的i行j列就是连接着
v
i
v_i
vi与
v
j
v_j
vj二节点边的值,同时一般我们规定在图中不存在的边的值为0(所以为什么不存在的边也被存储了啊),这也意味着对于使用邻接矩阵表示的图,其实际边集大小只与顶点数量v有关,恒定为
v
2
v^2
v2,同时使用邻接矩阵表示的图算法也是很~稳定地慢(怎么你不服气.jpg)。
易知
I
(
i
,
j
)
=
A
[
i
,
j
]
I(i,j)=A[i,j]
I(i,j)=A[i,j](何意味
图的二元组表示法
相比于三元组表示法,图的二元组表示法显得十分简洁而优雅(显而易见),就像我们把边叫边,把顶点叫顶点一样自然(一个图里必须有且只有边和点!!)。这里没有什么关联函数,没有什么映射,没有什么难懂的表达,只有顶点,只有边,就好像它们本该如此一样,不会有个关联函数跳出来说:“hey bro,这是我的权力,你们不得私自相互联系”不,这样充满了中世纪封建特权味道的风景不会再出现了,它被禁止了,这就是revlution,真正的革命。
G
=
(
V
,
E
)
G=(V,E)
G=(V,E)
这当然是个很简洁的式子,不过——我们要怎样才能表达它呢?😇
1
^1
1很自然的我们会想要用一个三元组来表达一个边,其中包含了边的两个顶点和它的值,这也意味着我们可以用一个
∣
E
∣
⋅
3
|E| \cdot 3
∣E∣⋅3的矩阵来表示边集,它的第一列为边的起点,第二列为边的终点,第三列为边的值,第i行为
a
i
∈
E
a_i \in E
ai∈E,这种表示方法极大的节省了计算机的存储空间(太棒啦),但是还有没有更加节省空间的表示方法呢,当然还是有的,比如说接下来说的邻接链表法。
[1].这个emoji的文字表示是innocent,第一眼还以为是in no cent(在没钱的时候),结果去查了才知道是in-noc-ent 无辜的 adj.(笑)文盲落泪了说是
图的邻接链表表示法
邻接链表是IT超级传奇经典巨著、如砖块一般的黑暗启蒙之书以及中文译本比原版还便宜的神圣慈爱之书 《算法导论》中所介绍的两种表示图的方法之一,足以证明其不可为凡人所质疑的神性(这里是在开玩笑没有真的表达这玩意至高无上的意思,严正申明我不是邻接链表单推人,、任何数据结构或者算法都要结合实际情况使用,不存在什么万能算法的说法,如有请当做是信徒的狂热话语)。
链表是一种很常见的数据结构,它可以用以下方式表示,其中箭头是一个指针,指向的是链表的下一个元素,一个元素通常是一个二元组(这里也就是说三元组的表示方式同样是存在的),元组中的第一项是元素,而第二项则是下一元素的指针。
a
⟶
b
⟶
c
⟶
a \longrightarrow b \longrightarrow c \longrightarrow \\
a⟶b⟶c⟶
这是一种相当好用简洁的数据结构,当然只使用像python这种原生就支持可变数组的语言的朋友们可能不能够理解为什么我们会钟爱这种简陋粗俗的数据结构,这是因为对于计算机的存储而言,必须要提前声明一段连续的空间才可以,也就是说如果最后分配给我们的空间不够用的情况,就会十分尴尬。因为如果我们要求计算机分配这之后的空间给我们,就会发现这之后的空间可能早就被分配给其他的变量了,像python的可变数组的实现方法就是,申请一段更长的空间,然后复制原来的数组进去,从直接观感来说,这太蠢了,我们会浪费多少时间与资源在维护可变数组上面啊,为什么不在最开始就计算好我们要使用的空间呢?
这种想法当然理所应当,可是为什么可变数组成为了python中唯一的数组类型呢?这是因为它可以从容地处理预料之外的情况,假如你在今天预计这个数组中的数据数量不会超过一千,而假如出现了超过一千的情况,那么只需要简单修改数组的大小就可以了(这种设想在过去那种存储空间紧张的条件下是合理的),但是随着时间的流逝,人员的变迁,这些最开始的构想很可能就会被遗忘,最终成为不知道什么时候暴雷的bug(当然与存储器性能大小与价格的下降,计算机性能的提升也有很大关系)。
而邻接链表是一个二维的链表,其中存储了某一顶点所可以到达的其他顶点,如下
a
:
↓
⟶
b
⟶
c
⟶
a: \downarrow \longrightarrow b \longrightarrow c \longrightarrow
a:↓⟶b⟶c⟶
b
:
↓
⟶
c
⟶
b: \downarrow \longrightarrow c \longrightarrow
b:↓⟶c⟶
c
:
↓
c: \downarrow
c:↓
图的关联矩阵
图的关联矩阵
M
M
M 是一个
n
⋅
m
n \cdot m
n⋅m 大小的矩阵,其中
n
n
n 是顶点的数量,
m
m
m 是边的数量。假设我们有图
G
G
G。
v
1
⟷
v
2
⟶
v
3
⟵
v
4
v_1 \longleftrightarrow v_2 \longrightarrow v_3 \longleftarrow v_4
v1⟷v2⟶v3⟵v4
它的关联矩阵
M
M
M 为
M
(
G
)
=
a
1
a
2
a
3
v
1
1
0
0
v
2
1
1
0
v
3
0
−
1
−
1
v
4
0
0
1
M(G)= \begin{array}{c|rrr} & a_1 & a_2 & a_3 \\ \hline v_1 & 1 & 0 & 0 \\ v_2 & 1 & 1 & 0 \\ v_3 & 0 & -1 & -1 \\ v_4 & 0 & 0 & 1 \end{array}
M(G)=v1v2v3v4a11100a201−10a300−11
我们可以通过计算图的关联矩阵的秩来计算图的连通性(挖坑)。
虽然很想一次性更完,但是这(哈欠)太长了,所以就先发写完的部分(变成系列更新了)
如果你喜欢的话可以打赏(有这个功能么)或者友好评论(我会很开心❤️)
如果你真的想看到后续的话,那最好就照我说的做(威胁(bushi)
二编:更新了图的关联矩阵,以及对不恰当的地方进行了修正