注意:本章主要讲解图的存储和图的遍历。
1. 图的定义、构成和术语
图(Graph),我们可以把它定义成一个二元组 。其中 代表 ,即顶集,顶集中的元素称为顶点(Vertex),写作 ; 代表 ,即边集,边集中的元素称为边(Edge),写作 。 无交集。 中的元素也是二元组,为 ,且 。
图还有一种三元组的定义,这里不再赘述,感兴趣的读者可以自行查阅。
以上定义可能较为形式化,较难理解。读者一会可以通过下面的例子来理解。
图还分为有向图和无向图——也就是说,有向图的边有方向,即有向边,无向图的边无方向,即无向边。特殊地,如果一个图的边带有权值,则称为带权图。
以下分别展示了有向图、无向图、无向带权图。同时我会用形式语言、通俗语言和举例来帮助大家更好理解。
- 中点集 ,称作图 的阶(Order)。
一个图中顶点的个数,就是一个图的阶。
图 的阶都是 。 - 当存在图 ,其中 ,则 称作图 的子图(Sub-Graph)。
从一个图中“抠”下来的图(即两个图具有包含关系),则被包含的图是另一个图的子图。
图 分别都是各自的子图。 - 当存在图 是图 的子图,且 ,则称为图 是图 的生成子图(Spanning Sub-Graph)。
一个图是另一个图的子图,且这两个图顶点数相同,则这个子图是另一个图的生成子图。
图 中,若去掉了 ,则所产生的图就是原图的生成子图。 - 与一个顶点 相关联的边的条数,称为 的度(Degree),记作 。
在一个图中的一个顶点,和它相连的边的条数,就是它的度。
图 中, 的度为 , 的度为 , 的度为 。 - 对于有向图,与一个顶点 相关联的边中,以 为终点的边的个数,称为 的入度(In-Degree),这些边称为 入边(In-Edge);与一个顶点 相关联的边中,以 为起点的边的个数,称为 的出度(Out-Degree),这些边称为出边(Out-Edge)。
有向图中的一个顶点,所有指向它的边的个数,就是它的入度,这些边是入边;除了这些边以外和它相连的边的条数,就是它的出度,这些边是出边。
图 中, 的入度为 ,出度为 ; 的入度为 ,出度为 。 - 若一条边的两个顶点相同,则这条边是一个自环(Loop)(并查集的初始化其实就是自环)。
- 图中的一条闭合的路径,称为环(Circuit)。
以上是一些基本的概念,其他大多数概念可以通过逐步的学习理解。
另外注意:树是一种特殊的图。
2. 图的存储
和 「数据结构详解·一」树的初步 的第二部分相同。
无向图连接 ,就是 g[u][v]=g[v][u]=1;
;有向图就是 g[u][v]=1;
。
但是这里再补充一部分内容。
2-1. 边表
不常用。主要在 Kruskal(一种最小生成树算法,将在以后讲到)等算法中用到。
直接用结构体存,。对于无向图,表示 ;对于有向图,表示 。
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;
}
//...
}
对于寻找与结点 相连的边,需要
2-2. 带权图的存储
邻接矩阵只要将 g[u][v]=1;
变为 g[u][v]=w;
即可,但是缺点是无法存重边(比如,输入中给出了 u=1 v=2 w=3
和 u=1 v=2 w=-2
,意味着 间有两条边)。另外,矩阵初始化要变为 或
邻接表只要用 pair 或结构体来代替 vector 中的内容(对于 有
边表和邻接表类似,修改结构体的内容即可。
3. 图的遍历
所有代码均为邻接表存储。
3-1. 深度优先遍历(DFS)
和 「数据结构详解·一」树的初步 类似,但是由于图的特性,我们要记录
void dfs(int p)//p 为当前节点编号
{
if(f[p]) return;//走过了
cout<<p<<' ';
f[p]=1;
for(auto i:g[p])
{
dfs(i);
}
}
3-2. 广度优先遍历(BFS)
和 「数据结构详解·一」树的初步 类似,但是由于图的特性,我们要记录
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】查找文献
只要将本文章的
4-2. Luogu P3916 图的遍历
如果我们直接暴力对于每个点查找,那是必定超时的。
题目问的是每个点所到达编号最大的点,那我们可以先反向建图,然后编号从大到小搜索,第一次搜索到的点就是答案(因为第二次搜到时编号因为是越来越小,因此不会比之前大)。
那这样的话,每个点只会遍历的到一次,时间复杂度由 变为了 。
参考代码:
#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;
}