图论基础------存储与查询

本文介绍了图论的基础知识,包括无向图、有向图、连通图等概念,以及图的存储方式——邻接矩阵和邻接表。接着讨论了图的遍历方法,如深度优先搜索(DFS)和广度优先搜索(BFS),并提供了有向图BFS的题目及解题思路。文章以实例解释了邻接矩阵和邻接表的优缺点,并强调了在非连通图中遍历的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图论 (graph) ,其实就是把很多个相同的事物抽象为一个个点,把它们之间的关系抽象为一条边,从而组成一张图。比如我们熟悉的树,他就是一张图。

一. 图的相关定义

图可以分为无向图有向图,他们分定义分别如下:

  1. 无向图:
    两个点之间的一条边没有规定方向,比如: A 点与 B 点之间有一条路径,那么可以从 A 点到 B 点,也可以从 B 点到 A 点。也就是说这条路是双行道。
  2. 有向图:
    两个点之间的边可能有多条,并且规定了方向。A 到 B 有一条路径,A 可以从这条路到 B ,但是从 B 不可以到 A 。也就是像生活中的汽车单行道,是不可以反向过去的。
    图论中的专业术语:

图又分为带权图和无权图。
其实带权图就是在一张图中的任意一条路径都有一个值(题目给出)。而无权图其实就是在两点之间的路径上没有值,(通常把路径上的值置为 1),在储存的时候差别其实也不是很大。

在图中的一些术语:

  • 路径:(无向图)相邻顶点的边
  • 权值:一条路径上的值
  • 连通图:可以从一个任意一个点经过某些路径可以到达任意一个点的图
  • 非连通图:与上文相反
  • 边:两点之间的一条连线(可能两点之间有多条)
  • 有向路径:在有向图中指明了起点和终点的路径
  • 有向环:一条至少含有一条边且起点和终点相同的有向路径(当然只存在于有向图中)
  • 度 :(有向图)
    • 入度:以顶点为起点的边的数目称为这个点的入度
    • 出度:以顶点为终点的边的数目称为这个点的出度
  • 度:(无向图)连接这个顶点的边的条数称为这个点的度
  • 有向无环图:没有环的有向图
  • 圈:(无向图)起点与终点重合的一条路径
  • 树:(无向图)没有圈的连通图
  • 森林:没有圈的非连通图(树和森林其实也是图的一种)

二. 图的储存
对于一张图,我们储存的方式一般有两种:

  • 邻接矩阵,也就是用一个矩阵来表示两条边是否相邻,存在路径则为 1 ,不存在则为 0(无向图)(有向图那么矩阵里面存的就应该是权值)
  • 邻接表,就是用一张表来储存两个点之间的关系(下面会用代码细讲)

邻接矩阵实现(无向图 && 无权值):

#include <cstdio>
#include <cmath>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

const int MAXN = 1005;//具体开多大据题目描述,随便开的
int w[MAXN][MAXN];//矩阵存储信息 

int main() {
	int n;
	scanf("%d", &n);//下面要输入的信息行数 
	for (int i = 1; i <= n; i++) {
		int a, b; 
		scanf("%d %d", &a, &b);//表示 a , b 两点之间存在路径 
		w[a][b] = 1; 
		w[b][a] = 1;//因为是双向的,所以还要反着来存一遍 
		//假如是有向图就删掉后面一个
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			printf("%d ", w[i][j]);
		}
		printf("\n");//输出矩阵 
	}
	return 0;
} 

按照上面的代码输出之后,我们会发现,这个矩阵是沿着一条对角线来看,是对称的,请读者们自己思考为什么是对称的呢?(不给读者留问题自己思考的博客不是好博客~)

邻接表实现(无向图 && 无边权)

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

const int MAXN = 1005;//具体开多大据题目描述,随便开的
vector <int> w[MAXN];

int main() {
	int n, m;
	scanf("%d %d", &m, &n);//下面要输入的信息行数 
	for (int i = 1; i <= n; i++) {
		int a, b; 
		scanf("%d %d", &a, &b);//表示 a , b 两点之间存在路径 
		w[a].push_back(b);
		w[b].push_back(a);//存储 
		//假如是邮箱图就删掉后面一个
	}//
	for (int i = 1; i <= m; i++) {
		printf("%d :  ", i);
		for (int j = 0; j < w[i].size(); j++) {
			printf("%d ", w[i][j]);
		} 
		printf("\n");
	}//输出表中内容 
	return 0;
} 

现在我们可以来分析一下邻接矩阵和邻接表的优点和缺点:

  • 邻接矩阵的存储和调用比较方便,与用起来也更加简单;但是他查询元素时效率低下,只有依次遍历,且会浪费大量的空间
  • 邻接表的查找元素十分高效,时间复杂度度远非邻接矩阵可比,并且他不会浪费太多的空间;但他使用起来比邻接矩阵要麻烦一点,操作难度系数要高一点(但是能运用自如肯定要比邻接矩阵要好得多)

三. 图的遍历
从图中的某个顶点出发(在非连通图中可能不止一个),按某种方法对全图进行遍历。
遍历方法一般有两种:

  • DFS
  • BFS

其实这两个和搜索的时候学的有点像,只是BFS少了方向数组而已。

  1. DFS定义
    假设初始状态是图中所有顶点都未曾被访问,则深度优先搜索可从图中某个顶点v出发,访问此顶点,然后依次从v的未被访问的邻接点出发深度优先遍历图,直至图中所有和v有路径相通的顶点都被访问到;若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

  2. BFS定义
    假设从图中某顶点v出发,在访问v之后依次访问v的各个未被访问过的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到。若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。换句话说,广度优先搜索遍历图的过程是以v为起始点,由近至远,依次访问和v有路径相通且路径长度为1,2,…的顶点。

下面是两道例题:

1.有向图的BFS

内存限制:256 MiB
时间限制:1000 ms

题目描述

给定一个有向图,有N个顶点,M条边,顶点从1..N依次编号,求出字典序最小的深度优先搜索顺序。

输入格式


第1行:2个整数,N(1≤N≤200)和M(2≤M≤5000) 接下来M行,每行2个整数I,J,描述一条边从顶点I指向顶点J

输出格式

仅一行,一个顶点编号序列,表示字典序最小的深度优先搜索序列.顶点之间用一个空格分开 

样例输入

3 3
1 2
1 3
2 3

样例输出

1 2 3

代码及注释如下:

#include <cstdio>
#include <cmath>
#include <queue> 
#include <vector>
#include <algorithm>
#include <iostream> 
using namespace std;

const int MAXN = 205;

vector <int> a[MAXN];//链表实现储存
bool vis[MAXN];//记录每个点是否被访问过

void addEnge (int u, int v) {
	a[u].push_back(v);
}/储存

void search_(int x) {
	queue <int> q;//与搜索里面的BFS一样,用队列实现
	q.push(x);//入队
	printf("%d ", x);//直接输出
	vis[x] = 1;//已经被访问过
	while (!q.empty()) {//只要队列不为空就继续搜索
		int k = q.front();
		q.pop();//取出对头元素
		sort(a[k].begin(), a[k].end());//题目要求按字典序输出
		for (int i = 0; i < a[k].size(); i++) {//查找
			if (!vis[a[k][i]]) {
				vis[a[k][i]] = 1;
				printf("%d ", a[k][i]);
				q.push(a[k][i]); //等一会儿继续查找与他相关的顶点
			}
		}
	}
}

int main() {
	int v, e;
	scanf("%d %d", &v, &e);
	for (int i = 1; i <= e; i++) {
		int u, v;
		scanf("%d %d", &u, &v);
		addEnge(u, v);//储存
	}
	for(int i = 1;i <= v; i++) {
		if(! vis[i]) search_(i);//与上文一样
	}
	return 0;
} 

代码非常简单。但是有些读者可能就要问了:为什么最后还要用一个循环来访问呢?
那是因为假如说这张图不是一个连通图的话,一次访问就不能访问到全图,所以我们还需要再找一个没有访问过的(因为访问过的也一定不会到这张非连通图的另一部分),继续访问,知道图中的每一个点都被访问一遍才结束,下边的DFS搜索也是一样的。

有向图的BFS
内存限制:256 MiB
时间限制:1000 ms
题目描述

给定一个有向图,有N个顶点,M条边,顶点从1..N依次编号,求出字典序最小的宽度优先搜索顺序。

输入格式

第1行:2个整数,N(1≤N≤200)和M(2≤M≤5000) 接下来M行,每行2个整数I,J,描述一条边从顶点I指向顶点J

输出格式

仅一行,一个顶点编号序列,表示字典序最小的宽度优先搜索序列.顶点之间用一个空格分开 

样例输入

3 3
1 2
1 3
2 3

样例输出

1 2 3

这道题只看样例数据好像和上一题差不多,但是这只是因为数据比较水而已 😛
下面是具体实现代码:

#include <cstdio>
#include <cmath>
#include <queue> 
#include <vector>
#include <algorithm>
#include <iostream> 
using namespace std;

const int MAXN = 205;

vector <int> a[MAXN];//用链表实现
bool vis[MAXN];//每一个顶点是否访问过 //visit的缩写

void addEnge (int u, int v) {
	a[u].push_back(v);
}//存储

void search_(int x) {
	vis[x] = 1;//把这个点置为访问过
	printf("%d ", x);
	sort(a[x].begin(), a[x].end());
	for(int i = 0;i < a[x].size(); i++) {
		if(! vis[a[x][i]]) {
			vis[a[x][i]] = 1;//已经被访问过
			search_(a[x][i]);//搜索
		} 
	}	
}

int main() {
	int v, e;
	scanf("%d %d", &v, &e);
	for (int i = 1; i <= e; i++) {
		int u, v;
		scanf("%d %d", &u, &v);
		addEnge(u, v);//出巡
	}
	for(int i = 1;i <= v; i++) {
		if(! vis[i]) {//假如说这个点没有访问过,就继续以他为顶点出发查询
			search_(i);//搜索
		}
	}
	return 0;
} 

好了,这些就是关于图基本的存储与查询了,文章就到这了,如果想了解图的最短路算法,请关注我的另一篇博客哟(^U^)ノ~YO,拜拜!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值