「数据结构详解·九」图的初步


注意:本章主要讲解图的存储图的遍历

1. 图的定义、构成和术语

图(Graph),我们可以把它定义成一个二元组 G = ( V , E ) G=(V,E) G=(V,E)。其中 V V V 代表 Vertices Set \text{Vertices Set} Vertices Set,即顶集,顶集中的元素称为顶点(Vertex),写作 V ( G ) V(G) V(G) E E E 代表 Edges Set \text{Edges Set} Edges Set,即边集,边集中的元素称为边(Edge),写作 E ( G ) E(G) E(G) V , E V,E V,E 无交集 E E E 中的元素也是二元组,为 ( x , y ) (x,y) (x,y),且 x , y ∈ V x,y\in V x,yV

图还有一种三元组的定义,这里不再赘述,感兴趣的读者可以自行查阅。

以上定义可能较为形式化,较难理解。读者一会可以通过下面的例子来理解。

图还分为有向图无向图——也就是说,有向图的边有方向,即有向边,无向图的边无方向,即无向边。特殊地,如果一个图的边带有权值,则称为带权图
以下分别展示了有向图、无向图、无向带权图。同时我会用形式语言、通俗语言和举例来帮助大家更好理解。
有向图
无向图
无向带权图

  • G G G点集 V V V 的大小,称作图 G G G阶(Order)
    一个图中顶点的个数,就是一个图的阶。
    1 ∼ 3 1\sim 3 13 的阶都是 6 6 6
  • 当存在图 G ′ = ( V ′ , E ′ ) G'=(V',E') G=(V,E)其中 V ′ ∈ V , E ′ ∈ E V'\in V,E'\in E VVEE,则 G ′ G' G 称作图 G = ( V , E ) G=(V,E) G=(V,E)子图(Sub-Graph)
    从一个图中“抠”下来的图(即两个图具有包含关系),则被包含的图是另一个图的子图。
    1 ∼ 3 1\sim 3 13 分别都是各自的子图。
  • 当存在图 G ′ G' G 是图 G G G子图,且 V ( G ′ ) = V ( G ) V(G')=V(G) V(G)=V(G),则称为图 G ′ G' G 是图 G G G生成子图(Spanning Sub-Graph)
    一个图是另一个图的子图,且这两个图顶点数相同,则这个子图是另一个图的生成子图。
    1 1 1 中,若去掉了 1 → 5 , 1 → 3 1\to 5,1\to 3 15,13,则所产生的图就是原图的生成子图。
  • 与一个顶点 V V V 相关联的边的条数,称为 V V V度(Degree),记作 d ( V ) d(V) d(V)
    在一个图中的一个顶点,和它相连的边的条数,就是它的度。
    2 2 2 中, 5 5 5 的度为 4 4 4 2 2 2 的度为 2 2 2 1 1 1 的度为 3 3 3
  • 对于有向图,与一个顶点 V V V 相关联的边中,以 V V V终点的边的个数,称为 V V V入度(In-Degree),这些边称为 入边(In-Edge);与一个顶点 V V V 相关联的边中,以 V V V起点的边的个数,称为 V V V出度(Out-Degree),这些边称为出边(Out-Edge)
    有向图中的一个顶点,所有指向它的边的个数,就是它的入度,这些边是入边;除了这些边以外和它相连的边的条数,就是它的出度,这些边是出边。
    1 1 1 中, 5 5 5 的入度为 3 3 3,出度为 1 1 1 6 6 6 的入度为 3 3 3,出度为 0 0 0
  • 若一条边的两个顶点相同,则这条边是一个自环(Loop)(并查集的初始化其实就是自环)。
  • 图中的一条闭合的路径,称为环(Circuit)

以上是一些基本的概念,其他大多数概念可以通过逐步的学习理解。
另外注意:树是一种特殊的图。

2. 图的存储

「数据结构详解·一」树的初步 的第二部分相同。
无向图连接 u , v u,v u,v,就是 g[u][v]=g[v][u]=1;;有向图就是 g[u][v]=1;
但是这里再补充一部分内容。

2-1. 边表

不常用。主要在 Kruskal(一种最小生成树算法,将在以后讲到)等算法中用到。
直接用结构体存, g i = ( u , v ) g_i=(u,v) gi=(u,v)。对于无向图,表示 u − v u-v uv;对于有向图,表示 u → v u\to v uv

struct node{
	int u,v;
}g[100005];

int main()
{
	int n,m;
	cin>>n>>m;
	for(int i=1;i<=m;i++)
	{
		cin>>a[i].u>>a[i].v;
	}
	//...
}

对于寻找与结点 x x x 相连的边,需要 O ( m ) O(m) O(m) 的时间,显然很慢,故不常用。

2-2. 带权图的存储

邻接矩阵只要将 g[u][v]=1; 变为 g[u][v]=w; 即可,但是缺点是无法存重边(比如,输入中给出了 u=1 v=2 w=3u=1 v=2 w=-2,意味着 1 , 2 1,2 1,2 间有两条边)。另外,矩阵初始化要变为 + ∞ +\infin + − ∞ -\infin 以表示两条边未连接(因为可能有负权边)。

邻接表只要用 pair 或结构体来代替 vector 中的内容(对于 a x a_x ax y , z y,z y,z 分别代表连接的边和权值),无需担心重边。

边表和邻接表类似,修改结构体的内容即可。

3. 图的遍历

所有代码均为邻接表存储。

3-1. 深度优先遍历(DFS)

「数据结构详解·一」树的初步 4 − 1 \mathbf{4-1} 41 类似,但是由于图的特性,我们要记录 f i f_i fi 表示其是否走过。

void dfs(int p)//p 为当前节点编号
{
	if(f[p]) return;//走过了
	cout<<p<<' ';
	f[p]=1;
	for(auto i:g[p])
	{
		dfs(i);
	}
}

3-2. 广度优先遍历(BFS)

「数据结构详解·一」树的初步 4 − 3 \mathbf{4-3} 43 类似,但是由于图的特性,我们要记录 f i f_i fi 表示其是否走过。

queue<int>q;
void bfs()
{
	q.push(root);//root 为遍历的起始节点
	while(!q.empty())
	{
		int x=q.front();
		q.pop();
		if(f[x]) continue;
		f[x]=1;
		cout<<x<<' ';
		for(auto i:g[x])
		{
			q.push(i);
		}
	}
}

4. 例题详解

4-1. Luogu P5318 【深基18.例3】查找文献

只要将本文章的 3 \mathbf{3} 3 的内容套用即可。

4-2. Luogu P3916 图的遍历

如果我们直接暴力对于每个点查找,那是必定超时的。
题目问的是每个点所到达编号最大的点,那我们可以先反向建图,然后编号从大到小搜索,第一次搜索到的点就是答案(因为第二次搜到时编号因为是越来越小,因此不会比之前大)。
那这样的话,每个点只会遍历的到一次,时间复杂度由 O ( ( n + m ) n ) O((n+m)n) O((n+m)n) 变为了 O ( n + m ) O(n+m) O(n+m)
参考代码:

#include<bits/stdc++.h>
using namespace std;

vector<int>g[100005];
int n,m,ans[100005];

void dfs(int fr,int p)
{
	if(ans[p]) return;//搜过了
	ans[p]=fr;
	for(auto i:g[p])
	{
		dfs(fr,i);
	}
}

int main()
{
	cin>>n>>m;
	while(m--)
	{
		int u,v;
		cin>>u>>v;
		g[v].push_back(u);//反向建图
	}
	for(int i=n;i>=1;i--)
	{
		dfs(i,i);
	}
	for(int i=1;i<=n;i++)
	{
		cout<<ans[i]<<' ';
	}
 	return 0;
}

5. 巩固练习

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值