图的基本概念
图的定义
图G是由顶点集V和边集E组成,记为G = (V, E),其中V(G)表⽰图G中顶点的有限⾮空集;E(G)表⽰图G中顶点之间的关系(边)集合。若V={v1,v2,…,vn}V = \left\{ v_{1},v_{2},\dots,v_{n} \right\}V={v1,v2,…,vn},则⽤∣V∣|V|∣V∣表
⽰图G中顶点的个数,也称图G的阶,E=(u,v)∣u∈V,v∈VE = {(u,v)|u \in V, v \in V}E=(u,v)∣u∈V,v∈V,⽤∣E∣|E|∣E∣表⽰图G中边的条数。
图是较线性表和树更为复杂的数据结构。
- 线性表中,除第⼀个和最后⼀个元素外,每个元素只有⼀个唯⼀的前驱和唯⼀的后继结点,元素和元素之间是⼀对⼀的关系;
- 在树形结构中,数据元素之间有着明显的层次关系,每个元素有唯⼀的双亲,但可能有多个孩⼦,元素和元素之间是⼀对多的关系;
- ⽽图形结构中,元素和元素之间的关系更加复杂,结点和结点之间的关系是任意的,任意两个结点之间都可能相关,图中元素和元素之间是多对多的关系
![![[Pasted image 20250411210928.png]]](https://i-blog.csdnimg.cn/direct/9de5377be3c94518856a537d0072339c.png)
有向图和⽆向图
图根据边的类型,可以分为⽆向图和有向图
![![[Pasted image 20250411211513.png]]](https://i-blog.csdnimg.cn/direct/e378942babaf40709ffee7c92312da68.png)
在图相关的算法中,我们可以将⽆向图中的边看成两条⽅向相反的有向边,从⽽将⽆向图转化为有向图
![![[Pasted image 20250411211622.png]]](https://i-blog.csdnimg.cn/direct/af286ca3d0c841c1bc920ea91a26080f.png)
简单图与多重图
⾃环:⾃⼰指向⾃⼰的⼀条边
![![[Pasted image 20250411211640.png]]](https://i-blog.csdnimg.cn/direct/308659a9db9347c2b66d6f4b63f19c5d.png)
重边:图中存在两个或两个以上完全相同的边
![![[Pasted image 20250411211811.png]]](https://i-blog.csdnimg.cn/direct/b8afcd550cf64bdfbdc120b5a7fed69e.png)
简单图:若图中没有重边和⾃环,为简单图。
多重图:若图中存在重边或⾃环,为多重图。
![![[Pasted image 20250411211832.png]]](https://i-blog.csdnimg.cn/direct/1b07faafb3f3409dbb1cefce3f6d63e3.png)
稠密图和稀疏图
有很少条边(如e < nlog2 n )的图称为稀疏图,反之称为稠密图
![![[Pasted image 20250411211857.png]]](https://i-blog.csdnimg.cn/direct/9cb34f8f6969488eac5b753e1092b0a9.png)
顶点的度
顶点v的度是指与它相关联的边的条数,记作deg(v)。由该顶点发出的边称为顶点的出度,到达该顶点的边称为顶点的⼊度。
- ⽆向图中,顶点的度等于该顶点的⼊度(indev)和出度(outdev),即deg(v)=indeg(v)=outdeg(v)。
- 有向图中,顶点的度等于该顶点的⼊度与出度之和,其中顶点v的⼊度indeg(v)是以v为终点的有向边的条数,顶点v的出度outdeg(v)是以v为起始点的有向边的条数,deg(v)=indeg(v)+outdeg(v)
![![[Pasted image 20250411212011.png]]](https://i-blog.csdnimg.cn/direct/9eaaf94fc0f84dfe83ac624ec119e28f.png)
路径
在图G=(V,E)中,若从顶点viv_{i}vi出发,沿⼀些边经过某些顶点vp1,vp2,…,vpmv_{p1},v_{p2},\dots,v_{pm}vp1,vp2,…,vpm,到达顶点vjv_{j}vj。则称顶点序列(vi,vp1,vp2,…,vpm,vj)(v_{i},v_{p1},v_{p2},\dots,v_{pm},v_{j})(vi,vp1,vp2,…,vpm,vj)为从顶点viv_{i}vi到顶点vjv_{j}vj的路径。
注意:两个顶点间的路径可能不唯⼀
![![[Pasted image 20250411212248.png]]](https://i-blog.csdnimg.cn/direct/c7f20d62cb0842d8a51c129446e08193.png)
简单路径与回路
若路径上各顶点v1,v2,…,vmv_{1},v_{2},\dots,v_{m}v1,v2,…,vm均不重复,则称这样的路径为简单路径。若路径上第⼀个顶点v1v_{1}v1和最后⼀个顶点vmv_{m}vm相同,则称这样的路径为回路或环
![![[Pasted image 20250411212403.png]]](https://i-blog.csdnimg.cn/direct/6b0962006aed4c2e908ffc43c666f7b1.png)
路径⻓度和带权路径⻓度
某些图的边具有与它相关的数值,称其为该边的权值。这些权值可以表⽰两个顶点间的距离、花费的代价、所需的时间等。⼀般将该种带权图称为⽹络
![![[Pasted image 20250411212509.png]]](https://i-blog.csdnimg.cn/direct/7bce4a2595484cfc86f204bddaeefdac.png)
对于不带权的图,⼀条路径的路径⻓度是指该路径上的边的条数。
对于带权的图,⼀条路径的路径⻓度是指该路径上各个边权值的总和。
![![[Pasted image 20250411212521.png]]](https://i-blog.csdnimg.cn/direct/760b0a55b0894fd49f7c0a842ccf5eac.png)
⼦图
设图G={V,E}G = \left\{ V, E\right\}G={V,E}和图G′={V′,E′}G' = \left\{ V',E' \right\}G′={V′,E′},若V′∈VV'\in VV′∈V 且E′∈EE'\in EE′∈E,则称G′G'G′是GGG的⼦图。若有V(G′)=V(G)V(G')=V(G)V(G′)=V(G)的⼦图G′G'G′,则称G′G'G′为GGG的⽣成⼦图。
相当于就是在原来图的基础上,拿出来⼀些顶点和边,组成⼀个新的图。但是要注意,拿出来的点和边要能构成⼀个图才⾏
![![[Pasted image 20250411212748.png]]](https://i-blog.csdnimg.cn/direct/a0e225dd0ac249439c9870f556e68b66.png)
G1_1和G1_2为⽆向图G1的⼦图,G1_1为G1的⽣成⼦图。
G2_1和G2_2为有向图G2的⼦图,G2_1为G2的⽣成⼦图。
连通图与连通分量
在⽆向图中,若从顶点v1v_{1}v1到顶点v2v_{2}v2有路径,则称顶点v1v_{1}v1与顶点v2v_{2}v2是连通的。如果图G中任意⼀对顶点都是连通的,则图G称为连通图,否则称为⾮连通图。
- 假设⼀个图有n个顶点,如果边数⼩于n-1,那么此图⼀定是⾮连通图。
- 极⼤联通⼦图:⽆向图中,拿出⼀个⼦图,这个⼦图包含尽可能多的点和边。
- 连通分量:⽆向图中的极⼤连通⼦图称为连通分量
![![[Pasted image 20250411212932.png]]](https://i-blog.csdnimg.cn/direct/121f99cbb7f64160a094563a7398d522.png)
⽣成树
连通图的⽣成树是包含图中全部顶点的⼀个极⼩连通⼦图。若图中顶点数为n,则它的⽣成树含有n-1条边。对⽣成树⽽⾔,若砍去⼀条边,则会变成⾮连通图,若加上⼀条边则会形成⼀个回路
![![[Pasted image 20250411213241.png]]](https://i-blog.csdnimg.cn/direct/aeec37dd1e97426c80268a1080070e00.png)
图的存储和遍历
图的存储有两种:邻接矩阵和邻接表:
- 其中,邻接表的存储⽅式与树的孩⼦表⽰法完全⼀样。因此,⽤vector数组以及链式前向星就能实现。
- ⽽邻接矩阵就是⽤⼀个⼆维数组,其中
edges[i][j]存储顶点 i 与顶点 j 之间,边的信息。
图的遍历分两种:DFS和BFS,和树的遍历⽅式以及实现⽅式完全⼀样。因此,可以仿照树这个数据结构来学习
邻接矩阵
邻接矩阵,指⽤⼀个矩阵(即⼆维数组)存储图中边的信息(即各个顶点之间的邻接关系),存储顶点之间邻接关系的矩阵称为邻接矩阵。
对于带权图⽽⾔,若顶点viv_{i}vi和vjv_{j}vj之间有边相连,则邻接矩阵中对应项存放着该边对应的权值,若顶点viv_{i}vi和vjv_{j}vj不相连,则⽤∞\infty∞来代表这两个顶点之间不存在边。
对于不带权的图,可以创建⼀个⼆维的bool类型的数组,来标记顶点vi 和vj 之间有边相连
![![[Pasted image 20250411214010.png]]](https://i-blog.csdnimg.cn/direct/de7fc6638d7049759f48ec3bddca9a54.png)
矩阵中元素个数为nxn,即空间复杂度为O(n^2) ,n为顶点个数,和实际边的条数⽆关,适合存储稠密图
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n, m;
int edges[N][N];
int main()
{
memset(edges, -1, sizeof edges);
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a - b 有⼀条边,权值为 c
edges[a][b] = c;
// 如果是⽆向边,需要反过来再存⼀下
edges[b][a] = c;
}
return 0;
}
vector数组
和树的存储⼀模⼀样,只不过如果存在边权的话,我们的vector数组⾥⾯放⼀个结构体或者是pair即可。
#include <iostream>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
int n, m;
vector<PII> edges[N];
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
edges[a].push_back({b, c});
// 如果是⽆向边,需要反过来再存⼀下
edges[b].push_back({a, c});
}
return 0;
}
链式前向星
和树的存储⼀模⼀样,只不过如果存在边权的话,我们多创建⼀维数组,⽤来存储边的权值即可
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 链式前向星
int h[N], e[N * 2], ne[N * 2], w[N * 2], id;
int n, m;
// 其实就是把 b 头插到 a 所在的链表后⾯
void add(int a, int b, int c)
{
id++;
e[id] = b;
w[id] = c; // 多存⼀个权值信息
ne[id] = h[a];
h[a] = id;
}
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
add(a, b, c); add(b, a, c);
}
return 0;
}
DFS
和树的遍历⽅式⼀模⼀样,⼀条路⾛到⿊
- ⽤邻接矩阵的⽅式存储
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1010;
int n, m;
int edges[N][N];
bool st[N]; // 标记哪些点已经访问过
void dfs(int u)
{
cout << u << endl;
st[u] = true;
// 遍历所有孩⼦
for(int v = 1; v <= n; v++)
{
// 如果存在 u->v 的边,并且没有遍历过
if(edges[u][v] != -1 && !st[v])
{
dfs(v);
}
}
}
int main()
{
memset(edges, -1, sizeof edges);
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a - b 有⼀条边,权值为 c
edges[a][b] = c;
// 如果是⽆向边,需要反过来再存⼀下
edges[b][a] = c;
}
return 0;
}
- ⽤vector数组的⽅式存储
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
int n, m;
vector<PII> edges[N];
bool st[N]; // 标记哪些点已经访问过
void dfs(int u)
{
cout << u << endl;
st[u] = true;
// 遍历所有孩⼦
for(auto& t : edges[u])
{
// u->v 的⼀条边,权值为 w
int v = t.first, w = t.second;
if(!st[v])
{
dfs(v);
}
}
}
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
edges[a].push_back({b, c});
// 如果是⽆向边,需要反过来再存⼀下
edges[b].push_back({a, c});
}
return 0;
}
- ⽤链式前向星的⽅式存储
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
// 链式前向星
int h[N], e[N * 2], ne[N * 2], w[N * 2], id;
int n, m;
// 其实就是把 b 头插到 a 所在的链表后⾯
void add(int a, int b, int c)
{
id++;
e[id] = b;
w[id] = c; // 多存⼀个权值信息
ne[id] = h[a];
h[a] = id;
}
bool st[N];
void dfs(int u)
{
cout << u << endl;
st[u] = true;
// 遍历所有的孩⼦
for(int i = h[u]; i; i = ne[i])
{
// u->v 的⼀条边
int v = e[i];
if(!st[v])
{
dfs(v);
}
}
}
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
add(a, b, c); add(b, a, c);
}
return 0;
}
BFS
- ⽤邻接矩阵的⽅式存储
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 1010;
int n, m;
int edges[N][N];
bool st[N]; // 标记哪些点已经访问过
void bfs(int u)
{
queue<int> q;
q.push(u);
st[u] = true;
while(q.size())
{
auto a = q.front(); q.pop();
cout << a << endl;
for(int b = 1; b <= n; b++)
{
if(edges[a][b] != -1 && !st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
int main()
{
memset(edges, -1, sizeof edges);
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a - b 有⼀条边,权值为 c
edges[a][b] = c;
// 如果是⽆向边,需要反过来再存⼀下
edges[b][a] = c;
}
return 0;
}
- ⽤vector数组的⽅式存储
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 1e5 + 10;
int n, m;
vector<PII> edges[N];
bool st[N]; // 标记哪些点已经访问过
void bfs(int u)
{
queue<int> q;
q.push(u);
st[u] = true;
while(q.size())
{
auto a = q.front(); q.pop();
cout << a << endl;
for(auto& t : edges[a])
{
int b = t.first, c = t.second;
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
edges[a].push_back({b, c});
// 如果是⽆向边,需要反过来再存⼀下
edges[b].push_back({a, c});
}
return 0;
}
- ⽤链式前向星的⽅式存储
#include <iostream>
#include <queue>
using namespace std;
const int N = 1e5 + 10;
// 链式前向星
int h[N], e[N * 2], ne[N * 2], w[N * 2], id;
int n, m;
// 其实就是把 b 头插到 a 所在的链表后⾯
void add(int a, int b, int c)
{
id++;
e[id] = b;
w[id] = c; // 多存⼀个权值信息
ne[id] = h[a];
h[a] = id;
}
bool st[N];
void bfs(int u)
{
queue<int> q;
q.push(u);
st[u] = true;
while(q.size()
{
auto a = q.front(); q.pop();
cout << a << endl;
for(int i = h[a]; i; i = ne[i])
{
int b = e[i], c = w[i];
if(!st[b])
{
q.push(b);
st[b] = true;
}
}
}
}
int main()
{
cin >> n >> m; // 读⼊结点个数以及边的个数
for(int i = 1; i <= m; i++)
{
int a, b, c; cin >> a >> b >> c;
// a 和 b 之间有⼀条边,权值为 c
add(a, b, c); add(b, a, c);
}
return 0;
}

被折叠的 条评论
为什么被折叠?



