导读

信息学能够有助于孩子未来工作发展,提升孩子的综合能力。


前面一节课,我们学习了图论的基本理论,讲解了图论的存储结构,这一节课,我们通过C++来实现一下,在信息学复赛中,如何编写图结构代码,以及如何输入图结构数据。


1 预备知识讲解

我们先首先回顾一下图的整个体系,然后讲解一个我们今天会用到,以后也会经常用到的函数。

1 图论体系

图论我们主要讲了四个部分:


图的概念
图的存储
图的遍历
图的应用


在图的概念中,我们讲解了如下这些概念:


1、图的分类
(1)有向图与无向图
(2)简单图、多重图、完全图
(3)稠密图和稀疏图
2、图的连通性
(1)连通与强连通
(2)连通分量与强连通分量
3、部分图
(1)子图
(2)生成子图
(3)生成树与生成森林
4、图的度与权
(1)度、出度、入度
(2)权、网
5、路径
(1)路径、简单路径
(2)回路、简单回路
(3)距离


我们简单学习了图的存储,有如下四种存储方式:


1、邻接矩阵
2、邻接表与逆邻接表
3、十字链表


我们简单学习了图的遍历:


深度优先遍历
广度优先遍历


然后我们简单了解了图的一些应用。

2 memset函数

我们在信息学中,经常需要对数据进行初始化,很多情况下,我们都是将数据初始化为同一个值。


我们可以自己使用for循环实现这个初始化过程,也可以直接使用C++中自带的函数。


memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值。memset需要头文件:


# include<string.h>


函数原型如下:


void *memset(void *s, int c, unsigned long n);


其中:


第一个参数是指要给哪一块内存赋值;
第二个参数是指要给这个内存赋值的具体的值;
第三个参数是要赋值的个数。一般就是内存的全部空间。


举个例子,下面这个操作是给数组a中所有元素赋值为1:


int a[100];
memset(a, 1, sizeof(a)); //sizeof获取a的内存大小。


2 图的存储

上一节课,我们简单学习了几种存储结构,这一节课,我们来详细讲解!

1 邻接矩阵

首先我们来看邻接矩阵。


邻接矩阵是用二维数组(矩阵)存放一个图。


1、无向图的邻接矩阵存储


例如,我们有如下这个图,会得到的邻接矩阵如右图:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_数据


我们左边的图有四个顶点,所以我们要构造一个4×4的矩阵。然后:


第一行和第一列,我们可以代表和顶点A相连的其他顶点;
第二行和第二列,我们可以代表和顶点B相连的其他顶点;
第三行和第三列,我们可以代表和顶点C相连的其他顶点;
第四行和第四列,我们可以代表和顶点D相连的其他顶点;


对于无向图,矩阵中存的值为0或1:


0表示这两个顶点没有连边
1表示这两个顶点有连边


例如上面右图中第二行第一列表示B(第二行)和A(第一列)有连边:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_数据_02


这是对于无向图,我们可以通过图中的0和1来表示任意两个顶点之间是否有连边。


2、无向图的邻接矩阵压缩存储


大家会发现,无向图的邻接矩阵关于从左上到右下的对角线对称,所以我们只需要存一半即可。又因为简单图不存在从自己到自己的连边,所以从左上到右下的对角线上的为0。这就说明对角线的数据是不需要存的。


所以我们只需要存下半部分:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_数据_03


我们想要存行索引为i,列索引为j的数据,存到一维数组中,索引为i的行前面有i-1个完整的阶梯行。从第一行到第i-1行,依次递增1,那么一共有(1+i-1)×(i-1)/2个数据,然后第i行,每个元素的索引在矩阵中为j。我们可以直接把这个列索引加到行索引上去,所以转化为一维数组的索引为:


i*(i-1)/2+j


因为我们选择的是左下角的部分,这部分数据行索引>列索引。当我们访问的数据为a[i][j]的时候,就要用如下方式去计算:


a[i][j] = a[j][i] = b[j*(j-1)/2+i]; // (i<j)
a[i][j] = b[i*(i-1)/2+j]; // (i>j)
a[i][j] = 0; // (i=j)


这样我们就能减少一半以上的空间。


3、无向图邻接矩阵的实现


首先我们先明确输入和输出:


【输入】
第一行:n和m,分别表示无向图的顶点个数和边的个数
下面m行:每一行表示有连边的两个顶点。
【输出】
输出n行n列的邻接矩阵
【要求】
邻接矩阵要使用压缩存储


例如我们上面的那个图,对应的输入如下:


4 4
0 1
0 2
1 3
2 3


如果我们不用压缩存储,非常简单:


#include<iostream>
using namespace std;
int main(){
int G[100][100];
int n,m,x,y;
cin>>n>>m;
for(int i = 0;i<m;i++){
cin>>x>>y;
G[x][y] = 1;
G[y][x] = 1;
}
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
cout<<G[i][j]<<" ";
}
cout<<endl;
}

return 0;
}


如果使用压缩存储,我们就要考虑对于行索引为i,列索引为j的数据存在哪里了。


我们只用存行索引大于等于列索引的数据。输入的时候,行列索引的大小不确定。我们要判断,然后把行索引变成大的那个


for(int i = 0;i<m;i++){
cin>>x>>y;
if(x<y) {
t = x;
x = y;
y = t;
}
G[x*(x-1)/2+y] = 1;
}

 

输出的时候,我们要分情况输出:


for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
if(i<j) cout<<G[j*(j-1)/2+i]<<" ";
else if(i>j) cout<<G[i*(i-1)/2+j]<<" ";
else cout<<0<<" ";
}
cout<<endl;
}


全部代码如下



#include<iostream>
using namespace std;
int G[100];
int main(){
int n,m,x,y,t;
cin>>n>>m;
for(int i = 0;i<m;i++){
cin>>x>>y;
if(x<y) {
t = x;
x = y;
y = t;
}
G[x*(x-1)/2+y] = 1;
}
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
if(i<j) cout<<G[j*(j-1)/2+i]<<" ";
else if(i>j) cout<<G[i*(i-1)/2+j]<<" ";
else cout<<0<<" ";
}
cout<<endl;
}

return 0;
}


执行结果如下:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_c++_04


4、有向带权图及邻接矩阵实现


有向图就只能使用完整存储了,有向图我们要考虑的是箭头的指向。因为前面的无向图我们考虑了无权的情况,那么有向图我们也同时考虑一下有权的情况。


我们分两部分说:


第一部分是有向:有向就是如果是i j,那么我们只能确定有从i到j的边。
第二部分是带权:带权就是
(1)如果两个顶点之间只有边存在,那么邻接矩阵存这条边的权重,
(2)否则就存无穷。表示顶点之间没有边。


对于下面的有向带权图,得到的邻接矩阵:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_c++_05


在计算机中,我们不可能实现真正的无穷,要么我们就使用字符定义,如果使用数值定义,我们就可以定义一个不在我们数据范围内的数,认为这个数代表的含义是无穷。例如,我们上面的有向带权图的权重是个位数,我们可以让10来代表无穷,或者用负数来代表无穷。在具体的代码中,我们要看题目中给出的数据的取值范围。


对于上面的带权图,我们输入格式如下:


4 4 //有4个顶点,4条边
1 0 5 //从顶点1(B)到顶点0(A)的边,权重为5
2 0 8
1 3 9
3 2 3


代码如下:


#define wq 100 
#include<iostream>
#include<string.h>
using namespace std;
int G[100][100];
int n,m,x,y,w;

int main(){
cin>>n>>m;
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++) {
G[i][j] = wq; //初始化为无穷
}
}
for(int i = 0;i<m;i++){
cin>>x>>y>>w;
G[x][y] = w;
}
for(int i = 0;i<n;i++){
for(int j = 0;j<n;j++){
if(G[i][j] == wq) cout<<"- ";
else cout<<G[i][j]<<" ";
}
cout<<endl;
}

return 0;
}


我们可以用一个其他字符来表示无穷(两个顶点没有连边),例如我们示例中的-。


执行结果如下:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_邻接表_06


2 邻接表与逆邻接表

接下来我们看一下邻接表和逆邻接表。


1、邻接表的定义


信息学赛培 | 06 信息学复赛必备的图结构实现方式_邻接表_07


如上图,是一个邻接表。


在竞赛中,如果我们这样存储是不方便的,我们一般都定义一个结构体数组和一个普通数组,结构体数组相当于上图中邻接表的所有节点,普通数组存放的是每个节点的指向的第一个邻接节点,相当于前面的第一列中的指针。如果我们需要将第一列中每一部分都存下来,我们就需要再定义结构体了。在竞赛中,我们一般只需要一个数组存第一个节点就可以了。


首先我们先定义一个结构体,用来存放,结构体需要包含如下几个部分:


边的起点;
边的终点;
边的权重;
邻接表中的下一条边;


所以我们可以定义边的结构体如下:


struct Edge {
int u; //边的起点
int v; //边的终点
int w; //边的权重
int next; //当前边在邻接表中的下一条边
}e[100];


如果我们需要存储起点和终点的数据,我们再添加其他成员变量即可。


在很多时候,我们存邻接表,是从他能去哪,所以我们的边的终点更重要。在一些场景中,边的起点是可以不用存的。


然后我们需要一个节点存放后面邻接的数据。我们通过一个数组来存放,会更加方便。


int vFirst[100]; //每个节点指向的第条边,没有存为-1,邻接表指针


这个数组表达的含义是说我们当前这个顶点指向的第一个节点中存放的边。


然后我们要知道边的序号,我们通过一个变量k来表示:


int k;


2、邻接表输入数据


当我们想输入数据的时候,可以写如下函数:


void e_push_back(int u,int v,int w)
{
e[k].v=v; //输入终点
e[k].w=w; //输入边的权重
e[k].next=vFirst[u]; //以u为起点,指向的next是vFirst[u]。
vFirst[u]=k; //更新u的目前第一个指向的边
k++; //k为边的序号
}


因为边的起点u一般是不常用的,所以我们在这里就不做赋值操作了。


3、邻接表的输出


输入完成之后我们可以输出每个顶点和与这个顶点相邻的边。


我们可以从邻接节点的角度去输出全部的关系:


void print(){
for(int i = 0;i<2*m;i++){
cout<<e[i].u<<"-"<<e[i].v<<": ";
k = i;
while(e[k].next != -1){
k = e[k].next;
cout<<e[k].u<<"-"<<e[k].v<<", ";
}
cout<<endl;
}
}


执行结果如下:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_数据_08


上面是从邻接节点的角度去输出所有的边,如果我们从顶点的角度输出,效果会更好,代码如下:


void print(){
for(int i = 0;i<n;i++){
cout<<e[vFirst[i]].u<<"-"<<e[vFirst[i]].v<<", ";
k = vFirst[i];
while(e[k].next != -1){
k = e[k].next;
cout<<e[k].u<<"-"<<e[k].v<<", ";

}
cout<<endl;
}
}


执行结果如下:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_c++_09


全部代码如下




#include<string.h>
#include<iostream>
using namespace std;

int n,m,k;
int x,y;

struct Edge {
int u; //边的起点
int v; //边的终点
int next; //当前边在邻接表中的下一条边
}e[100];

int vFirst[100]; //每个节点指向的第条边,没有存为-1,邻接表指针

void e_push_back(int u,int v)
{
e[k].u=u;
e[k].v=v; //输入终点
e[k].next=vFirst[u]; //以u为起点,指向的next是vFirst[u]。
vFirst[u]=k; //更新u的目前第一个指向的边
k++; //k为边的序号
}

void print1(){ //从边的角度输出
for(int i = 0;i<2*m;i++){
cout<<e[i].u<<"-"<<e[i].v<<": ";
k = i;
while(e[k].next != -1){
k = e[k].next;
cout<<e[k].u<<"-"<<e[k].v<<", ";
}
cout<<endl;
}
}

void print(){ //从顶点的角度输出
for(int i = 0;i<n;i++){
cout<<e[vFirst[i]].u<<"-"<<e[vFirst[i]].v<<", ";
k = vFirst[i];
while(e[k].next != -1){
k = e[k].next;
cout<<e[k].u<<"-"<<e[k].v<<", ";
}
cout<<endl;
}
}


int main()
{
memset(vFirst,-1,sizeof(vFirst)); //-1表示空
cin>>n>>m;
for(int i=0;i<m;i++) {
cin>>x>>y;
e_push_back(x,y);
e_push_back(y,x); //有向图要存两次
}
print();
}


上面输出的过程中,我们没有使用权重,大家也可以尝试带权重的情况。


4、逆邻接表


逆邻接表原理与邻接表是一致的,只不过存的是入度。在竞赛中很少用到。我们就不详细说明了,代码与邻接表类似。

3 十字链表

在竞赛中,十字链表几乎没人用,因为存储复杂度太大。我们主要能理解十字链表的思想就可以了。




6 作业

本节课的作业,就是复习上面的所有知识,并完成下面两道题目!

1 带权有向图的邻接表存储与输出

使用邻接表实现一个带权有向图的存储,并把这个图输出。


【输入格式】
第一行两个整数n和m,分别表示图的顶点数和边数
后面m行,每行三个数据,x,y,w,分别表示某条边的起点、终点、权重
【输出格式】
至多n行,每行有多个从同一起点开始的元组,每个格式如下:
(起点,终点,权重)


例如:


信息学赛培 | 06 信息学复赛必备的图结构实现方式_数据_10


我们用0表示A,1表示B,2表示C,3表示D。


那输入为(权重任意设置):


4 4
0 1 1
2 0 3
2 3 5
3 1 6


输出为:


(0,1,1) 
(2,0,3) (2,3,5)
(3,1,6)




AI与区块链技术

信息学赛培 | 06 信息学复赛必备的图结构实现方式_邻接表_11

长按二维码关注