基础算法–图论
基本概念
一个图是由顶点集 V V V和边集 E E E组成的。其中, V V V和 E E E都是非空集合, V V V中的元素称为顶点, E E E中的元素称为边。边是顶点的有序对,即 E = { ( v i , v j ) ∣ v i , v j ∈ V } E=\{(v_i,v_j)|v_i,v_j\in V\} E={(vi,vj)∣vi,vj∈V}
-
顶点的属性
- 度数:与该顶点相关联的总边数(一个图
G
G
G的总度数
d
(
V
)
d(V)
d(V)等于总边数
2
2
2倍,即
2
E
2E
2E)。当图的边具有方向时(即有向图),一个顶点又分为出度和入度
- 出度:是以该顶点为起点的边数
- 入度:以该顶点为终点的边数
- 稀疏图:每个顶点的度数较小的图
- 稠密图:每个顶点的度数较大的图,完全图是稠密图。
- 阶数:图 G G G中顶点集 V V V的大小为 G G G的阶数
- 度数:与该顶点相关联的总边数(一个图
G
G
G的总度数
d
(
V
)
d(V)
d(V)等于总边数
2
2
2倍,即
2
E
2E
2E)。当图的边具有方向时(即有向图),一个顶点又分为出度和入度
-
边(又称为线或弧,边 ( u , v ) (u, v) (u,v)中表示 u u u和 v v v邻接, ( u , v ) ∈ E (u, v) \in E (u,v)∈E)的属性
- 有/无向图:一条边是一个顶点对 ( u , v ) (u,v) (u,v), u , v ∈ V u, v \in V u,v∈V,按照图的顶点对是否有序,顶点对有序的图称为有向图,此时边 ( u , v ) (u,v) (u,v)和 ( v , u ) (v, u) (v,u)是两条不同的边,顶点对无序的图称为无向图,此时边 ( u , v ) (u, v) (u,v)和 ( v , u ) (v, u) (v,u)是两条相同的边,无向图可看作一个特殊的有向图
- 具有成分的边:依据图的中边是否包含权值,可将图分为有权图和无权图,无权图可以看作是所有边权值相同的有权图
- 路径:一条路径是一个顶点序列
u
1
,
u
2
,
u
3
,
…
,
u
n
,
(
u
i
,
u
(
i
+
1
)
)
∈
E
,
1
<
=
i
<
n
u_1, u_2, u_3, …, u_n,(u_i, u_{(i + 1)}) \in E,1 <= i< n
u1,u2,u3,…,un,(ui,u(i+1))∈E,1<=i<n。路径长等于路径的边数(即
n
−
1
n - 1
n−1),不包含边的路径长为
0
0
0
- 简单路径:路径上所有顶点互异,起点和终点可以相同
- 环:此时
u
1
=
u
n
u_1 = u_n
u1=un,而且路径长至少为
1
1
1,有向图
(
u
,
v
)
(u, v)
(u,v)和
(
v
,
u
)
(v, u)
(v,u)是一个圈,无向图
(
u
,
v
)
(u, v)
(u,v)和
(
v
,
u
)
(v, u)
(v,u)通常不被认为是一个圈
- 自环边:两个顶点都相同的边
- 重边(平行边):连接两个顶点的边数超过一条,又称为多重边
- 连通图:无向图中每个顶点到任意顶点都存在一条路径的图称为连通图。其中连通的有向图称为强连通,非连通的有向图,去掉方向其基础图是连通的,则成为弱连通的
- 完全图:图中任意一对顶点都存在一条边
图的表示
图的表示常见有两种方式,但是具体使用哪个表示方式视情况而定,这两个方式分别是邻接矩阵和邻接表
邻接矩阵表示
对于邻接矩阵,一般是针对稠密图,比如完全图,它使用一个二维数组表示,此时空间需求为
V
2
V^2
V2,如下图是一个邻接矩阵:
对于无向图
A
[
u
]
[
v
]
=
1
A[u][v] = 1
A[u][v]=1表示顶点
u
u
u和
v
v
v有一条边连接,同时
A
[
v
]
[
u
]
=
1
A[v][u] = 1
A[v][u]=1。对于有向图,
A
[
v
]
[
u
]
=
1
A[v][u] = 1
A[v][u]=1表示以
u
u
u为起点,
v
v
v为终点的边,对于有权图,
A
[
v
]
[
u
]
A[v][u]
A[v][u]为权值,可以使用很大或很小的权值表示不存在的边。邻接矩阵删除和插入边时间为
O
(
1
)
O(1)
O(1)。下面给出一个简单的无向无权图的邻接矩阵表示法
#include <iostream>
#include <vector>
struct Node {
int data;
Node(int v) {
data = v;
}
};
struct Graph {
size_t v; // 顶点数
std::vector<std::vector<int>> matrix; // 邻接矩阵
Graph(size_t _v) {
v = _v;
matrix = std::vector<std::vector<int>>(v, std::vector<int>(v, 0));
}
};
邻接表
图的另一种表示方式是邻接表,一般来说针对稀疏图,针对完全图那和上面的邻接矩阵一样了。该方式使用一个邻接表数组 l i s t [ V ] list[V] list[V]表示图,也就是对于每个顶点,使用一个表存放与该顶点邻接的所有顶点,一共有 V V V个顶点,空间需求为 V + E V+E V+E,邻接表的实例如下图
如果边有权值,可以在表中每一个单元额外存储权值信息,可以使用一个Pair结构体表示
图
G
G
G实际是一个邻接表数组,横向存储的顶点是与一个顶点邻接的,该组合为一个邻接表,整体上可以使用一个数组,邻接表根据使用不用的语言实现可以有不同的选择:
- 数组或链表(
vector
、list
),此时允许平行边或重边和自环边,查询时间为 O ( V ) O(V) O(V),插入时间为 O ( 1 ) O(1) O(1) - 树,不允许平行边,查询时间和插入时间都为 O ( log V ) O(\log V) O(logV)`
- 散列表(
hashtable
),无序,不允许平行边,插入查询时间为 O ( 1 ) O(1) O(1)
以上要注意的主要是针对有权图的设计,特别是有权然后还有其它相关属性的时候,那么这时可以使用一个结构体类型表示一个单元
图的遍历
图的遍历算法其实我们早在搜索算法里面中已经提到,里面有对广度优先和深度优先算法的详细介绍。图论遍历算法主要有两种:广度优先遍历(BFS)和深度优先遍历(DFS),这两个遍历算法是图论算法的基本形式。我们这里就直接给出代码,不在详细叙述。
void bfs(Graph *graph, std::vector<Node *> &nodes) {
if (nodes.empty() || graph == nullptr || graph->v != nodes.size()) return;
std::queue<Node *> queue;
std::vector<bool> visited(nodes.size(), false);
queue.push(nodes[0]);
visited[0] = true;
size_t idx = 0;
while (!queue.empty()) {
Node *n = queue.front();
std::cout << n->data << " ";
for (int i = 0; i < graph->v; ++i) {
if (graph->matrix[idx][i] && !visited[i]) {
visited[i] = true;
queue.push(nodes[i]);
}
}
queue.pop();
++idx;
}
std::cout << std::endl;
}
void dfs(Graph *graph, std::vector<Node *> &nodes) {
if (nodes.empty() || graph == nullptr || graph->v != nodes.size()) return;
std::list<std::pair<Node *, int>> list;
std::vector<bool> visited(nodes.size(), false);
list.push_front(std::make_pair(nodes[0], 0));
visited[0] = true;
while (!list.empty()) {
std::pair<Node *, int> pair = list.front();
list.pop_front();
std::cout << pair.first->data << " ";
for (int i = graph->v - 1; i >= 0; --i) {
if (graph->matrix[pair.second][i] && !visited[i]) {
visited[i] = true;
list.push_front(std::make_pair(nodes[i], i));
}
}
}
std::cout << std::endl;
}
int main() {
std::vector<Node *> nodes;
for (int i = 0; i < 5; ++i) {
nodes.push_back(new Node(i));
}
Graph *graph = new Graph(5);
graph_add_edge(graph, 0, 1);
graph_add_edge(graph, 0, 2);
graph_add_edge(graph, 0, 3);
graph_add_edge(graph, 1, 3);
graph_add_edge(graph, 1, 4);
graph_add_edge(graph, 2, 3);
graph_add_edge(graph, 3, 4);
bfs(graph, nodes);
dfs(graph, nodes);
}