图
图(Graph)是由顶点的有穷非空集台和顶点之间边的集台组成的,通常表示为G(V,E) ,其中, G表示—个图,V是图G中顶点的集台,E是图G中边的集台。
注意:
- 线性表中我们把数据元素叫元素,树中将数据元素叫结点,在图中数据元素,我们则称之为顶点(Vertex)。
- 线性表中,相邻的数据元素之间具有线性关系,树结构中,相邻两层的结点具有层次关系,而图中,任意两个顶点之间都可能有关系,顶点之间的逻辑关系用边来表示,边集可以是空的。
- 在线性表中,元素个数可以为零,称为空表;
在树中,结点个数可以为零,称为空树;
在图中,顶点个数不能为零,但可以没有边。
图的相关术语
无向边:若顶点
v
i
v_{i}
vi到
v
j
v_{j}
vj之间的边没有方向.用无序
偶对(
v
i
v_{i}
vi,
v
j
v_{j}
vj)来表示。
有向边:若顶点
v
i
v_{i}
vi到
v
j
v_{j}
vj之间的边有方向。用有序偶 <
v
i
v_{i}
vi,
v
j
v_{j}
vj> 来表示。
无向图:在一个图中,每条边都没有方向。
有向图:在一个图中,每条边都有方向。
无向完全图:在无向图中,任意两个顶点之间都存在边。含有n个顶点的无向完全图有 n x (n-1)/ 2条边。
有向完全图:在一个有向图中,任意两个顶点之间都存在方向相反的两条弧.。含有n个顶点的有向完全图有n×(n-1)条弧.。
注意无向边用小括号“()”表示,而有向边则是用尖括号“<>”表示。
顶点的度:在无向图中,顶点v的度是指依附于该顶点的边数,通常记为TD(v)。
入度:在有向图中,顶点v的入度是指以该顶点为弧头的弧的数目,记为ID(v)。
出度:在有向图中,顶点v的出度是指以该顶点为弧尾的弧的数目,记为OD(v)。
权:有些图的边或弧具有与它相关的数字,这种与图的边或弧相关的数叫做权。(即指对边赋予的有意义的数值量。)
网:边上带权的图,也称网图
路径:在无向图G=(V,E)中,从顶点
v
p
v_{p}
vp到顶点
v
q
v_{q}
vq之间的路径是一个顶点序列(
v
p
v_{p}
vp=KaTeX parse error: Expected 'EOF', got '}' at position 6: v_i0p}̲ ,
v
i
1
v_{i1}
vi1,
v
i
2
v_{i2}
vi2,…,
v
i
m
v_{im}
vim=
v
q
v_{q}
vq其中,(
v
i
j
−
1
v_{ij-1}
vij−1,
v
i
j
v_{ij}
vij)∈E( 1≤ j ≤ m )若G是有向图,则路径也是有方向的,顶点序列满足<
v
i
j
−
1
v_{ij-1}
vij−1,
v
i
j
v_{ij}
vij>∈E。
路径长度:非带权图——路径上边的个数。
带权图——路径上各边的权之和。
回路(环):第一个顶点和最后一个顶点相同的路径。
简单路径:序列中顶点不重复出现的路径。
简单回路(简单环):除了第一个顶点和最后一个顶点外,其余顶点不重复出现的回路。
连通图:在无向图中,如果从一个顶点
v
i
v_{i}
vi到另一个顶点
v
j
v_{j}
vj(i≠j)有路径,则称顶点
v
i
v_{i}
vi和
v
j
v_{j}
vj是连通的。如果图中 任意两个顶点都是连通的,则称该图是连通图。
连通分量:非连通图的极大连通子图称为连通分量。
生成树:n个顶点的连通图G的生成树是包含G中全部顶点的一个极小连通子图。
图的基本操作
- CreateGraph(G):输入图G的顶点和边,建立图G的存储。
- DFS(G,v):在图G中,从顶点v出发深度优先遍历图G。
- BFS(G,v):在图G中,从顶点v出发广度优先遍历图G。
图的存储结构
图的邻接矩阵
图的邻接矩阵(Adjacency Matrix)存储方式是用两个数组来表示图。一个一维数组存储图中顶点信息,一个二维数组(称为邻接矩阵)存储图中的边或弧的信息。
设图G有n个顶点,则邻接矩阵是一个n×n的方阵,定义为:
arc[i][j] = 1 ,若( v i v_{i} vi, v j v_{j} vj)∈E或< v i v_{i} vi, v j v_{j} vj>∈E
arc[i][j] = 0 ,其他
无向图的邻接矩阵是对称的,
有向图的邻接矩阵则不对称。
可以使用邻接矩阵存储一张图:记 v[i][j] 表示从 i 到 j 的边权。如果不通可以设置为 0 或者 inf一个很大的数字)。
/*
对于一个 n 个点 m 条边的图,在使用邻接矩阵时,需要开一个n×n 的数组,即空间复杂度 O(n^2),代码如下:
*/
cin >> n;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
cin >> v[i][j]; // 存入每一对点之间的边权
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (v[i][j] > 0)
cout << "edge from point " << i << " to point " << j
<< " with length " << v[i][j] << '\n';
可采用 vector 代替二维数组,使用 vector 存储第二维,从而减少空间占用。这种做法被称为邻接表。为了同时存储边的终点与边权,可以采用结构体。
#include <iostream>
#include <vector>
#define MAXN 1005
using namespace std;
struct edge {
// 记录边的终点,边权的结构体
int to, cost;
};
int n, m; // 图有 n 个点 m 条边
vector <edge> p[MAXN]; // 邻接表
int v[MAXN][MAXN]; // 邻接矩阵
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v, l;
cin >> u >> v >> l;
p[u].push_back((edge) {v, l});
// p[v].push_back((edge){u, l});
// 无向图邻接表要加一条反方向的边
}
}
// 遍历邻接表,把邻接表转换为邻接矩阵
for (int i = 1; i <= n; i++)
for (int j = 0; j < p[i].size(); j++)
v[i][p[i][j].to] = p[i][j].cost;
邻接表的优缺点:
- 优点:总的空间复杂度是O(m) 的,且遍历某个点相邻的节点的时间复杂度为 O§ ,其中 p 为该点的出度。
- 缺点:如果需要指定查询或修改边<i,j>的边权,需要的时间复杂度为 O§,不如邻接矩阵的 O(1)。
- 邻接矩阵:适用于点较少(几百)、边较多(稠密图)的情况;
邻接表:适用于点较多、或者可能出现重边的情况。
题目详情: 洛谷P5318深基18.例3】查找文献
小K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。
假设洛谷博客里面一共有 n ( n ≤ 1 0 5 ) n(n\le10^5) n(n≤105) 篇文章(编号为 1 到 n n n)以及 m ( m ≤ 1 0 6 ) m(m\le10^6) m(m≤106) 条参考文献引用关系。目前小 K 已经打开了编号为 1 的一篇文章,请帮助小 K 设计一种方法,使小 K 可以不重复、不遗漏的看完所有他能看到的文章。
这边是已经整理好的参考文献关系图,其中,文献 X → Y 表示文章 X 有参考文献 Y。不保证编号为 1 的文章没有被其他文章引用。
请对这个图分别进行 DFS 和 BFS,并输出遍历结果。如果有很多篇文章可以参阅,请先看编号较小的那篇(因此你可能需要先排序)。
法一:DFS
void solve(int x) {
for (int i = 0; i < p[x].size(); i++)
if (!u[p[x][i]]) {
u[p[x][i]] = true;
solve(p[x][i]);
}
}
法二:BFS
while (!q.empty()) {
int x = q.front();
q.pop();
cout << x << ' ';
for (int i = 0; i < p[x].size(); i++)
if (!u[p[x][i]]) {
u[p[x][i]] = true;
q.push(p[x][i]);
}
}