邻接矩阵是数学和计算机科学中常用的一种表示方式,用来表述 有向图 或 无向图。一张图由一组顶点(或节点)和一组边组成,用邻接矩阵就能表示这些顶点间存在的边的关系。
一、图的概念
对于图而言,是数据结构中最复杂的结构了,而实际刷题过程中,不需要去理解太多课本的内容,最大的难点在于 广搜 和 深搜 的过程。所以我这里只把最简单的几个概念列出来,大家能够看懂就可以开始刷题了。
图从两个维度划分,可以分为:有向图和无向图、无权图和带权图。
1、有向图和无向图
无向图(Undirected Graph):在无向图中,边没有方向,表示的是双向关系。换句话说,如果两个顶点(或节点)之间存在边,那么这两个顶点就互相连接。
例如,如果你正在建模一个社交网络,你可能会使用无向图,因为友谊是双向的:如果 1 是 2 的朋友,那么 2 也是 1 的朋友。如图所示:
有向图(Directed Graph):与无向图相反,有向图中的边有方向,表示单向关系。
在这种类型的图中,如果存在从 1 到 2 的边,那不一定存在从 2 到 1 的边。有向图广泛应用于物理科学和工程领域。例如,如果你正在建模一个网站链接的结构,你可能会使用有向图,因为一个网页链接到其他网页并不意味着那个网页也链接返回。
2、无权图和带权图
在图论中,图可以是无权的(Unweighted)也可以是带权的(Weighted),这主要取决于边是否具有与其相关联的值(权重)。
无权图(Unweighted Graph):在无权图中,边没有权重,或者说所有边的权重都是相同的。你只关心两个节点(顶点)之间是否存在边,而不关心边的长度或者成本。比如,社交网络的人际关系就可以用无权图来表示,如果两个人是朋友,就有一条边连接他们,所有的边都被视为相等。
带权图(Weighted Graph):与无权图相对,带权图中的边有各自的权重。这个权重可以表示很多意义,如距离、时间、成本等等,取决于你要解决的问题。比如,在导航应用中,每个节点可以代表一个地点,边的权重就可以代表两个地点之间的距离或者行驶时间。在这种情况下,你不仅关心节点之间是否存在边,还关心这个边的权重是多少。
二、邻接矩阵的概念
注意:对于邻接矩阵而言,不需要去考虑是有向的还是无向的,统一都可以理解成有向的,因为有向图可以兼容无向图,对于无向图而言,只不过这个矩阵按照主对角线对称的,因为 A 到 B 有边,则必然 B 到 A 有边。
1、无权图的邻接矩阵
在这样一个矩阵里:
1)矩阵的行和列都对应图中的一个顶点。
2)如果顶点 A 到 顶点 B 有一条边(这里是单向的),则对应矩阵单元为 1。
3)如果顶点 A 到 顶点 B 没有边,则对应的矩阵单元就为 0。
例如,对于一个有四个节点的无向图,其邻接矩阵可能如下:
从这个矩阵中我们可以看出,A节点能够到 B、D 节点,B节点能够到 A、C节点,C节点能够到 B、D节点,D节点能够到 A、C节点。如图所示:
2、带权图的邻接矩阵
在带权图的邻接矩阵中,每个矩阵元素表示一个有向边的权值。如果不存在从一个节点到另一个节点的边,则通常将其表示为特殊的值,如0,-1或无穷。
假设有一个有向带权图,它有4个顶点(A, B, C, D),边及其权重如下:
- 边 A->B 的权重是3
- 边 A->C 的权重是7
- 边 B->A 的权重是4
- 边 B->D 的权重是1
- 边 C->D 的权重是2
- 边 D->A 的权重是1
我们可以将这个有向带权图表示为以下的邻接矩阵:
A B C D
A 0 3 7 0
B 4 0 0 1
C 0 0 0 2
D 1 0 0 0
在这个矩阵中,行表示起始顶点,列表示目标顶点。矩阵元素的值代表起始顶点到目标顶点的边的权重。如果没有边存在,我们用0来表示。例如,第一行表示从A到各点的边的权重,可以看出有从A到B的边,权重为3,有从A到C的边,权重为7,没有从A出发到达D的边,所以为0。
三、邻接矩阵的应用
1、问题描述
有 n(n ≤ 200) 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。返回矩阵中 省份 的数量。
2、算法分析
根据这个问题的描述,很明显这是一个无向图,并且是不带权重的。
我们可以遍历每个顶点,然后将它标记掉,并且利用 深度优先搜索 遍历和它相邻的其它未被标记的点,从而统计连通的图的数量。
3、源码剖析
首先,实现一个遍历函数,这个函数的作用就是遍历所有没有被访问的点,并且将和它相邻的所有点都标记成同一种颜色。然后重复计算这个过程。
int hash[210];
int findCircleNum(int ** mat, int n, int * _) {
int color = 0; // (1)
memset(hash, 0, sizeof(hash)); // (2)
for (int i = 0; i < n; ++i) {
if (!hash[i]) { // (3)
++color; // (4)
dfs(mat, n, i, color); // (5)
}
}
return color;
}
(1) 将颜色进行初始化;
(2) 将所有节点的颜色都初始化为 0,即未访问;
(3) 遍历访问所有没有被标记颜色的结点;
(4) 将颜色值 + 1;
(5) 遍历所有和 i 相连的结点;
然后,用一个深度优先搜索,将所有相邻的结点都标记成 color 这种颜色。
void dfs(int ** mat, int n, int u, int color) {
if (hash[u]) {
return; // (1)
}
hash[u] = color; // (2)
for (int i = 0; i < n; ++i) {
if (mat[u][i]) { // (3)
dfs(mat, n, i, color); // (4)
}
}
}
(1) 非 0 代表已经被标记颜色,直接返回即可;
(2) 将 u 这个结点的颜色标记为 color;
(3 - 4) 如果 u 到 i 有边,则继续递归计算从 i 结点开始标记的情况;
四、邻接矩阵的优点
1)简单直观:邻接矩阵是一个二维顺序表,通过矩阵中的元素值可以直接表示顶点之间的连接关系,非常直观和易于理解。
2)存储效率高:对于小型图,邻接矩阵的存储效率较高,因为它可以一次性存储所有顶点之间的连接关系,不需要额外的空间来存储边的信息。
3)算法实现简单:许多图算法可以通过邻接矩阵进行简单而高效的实现,例如 遍历图、检测连通性等。
五、邻接矩阵的缺点
1)空间复杂度高:对于大型图,邻接矩阵的空间复杂度较高,因为它需要存储一个 n × n 的矩阵,这可能导致存储空间的浪费和效率问题。
2)不适合稀疏图:邻接矩阵对于稀疏图(即图中大部分顶点之间没有连接)的表示效率较低,因为它会浪费大量的存储空间来存储零元素。
一、邻接表的概念
邻接表是一种表示图的数据结构。邻接表的主要概念是:对于图中的每个顶点,维护一个由与其相邻的顶点组成的列表。这个列表可以用数组、链表或其他数据结构来实现。
实际上,邻接表可以用于 有向图、无向图、带权图、无权图。这里只考虑无权图的情况,带权图只需要多存储一个数据就可以了,大家可以举一反三,触类旁通。
二、邻接表的顺序表存储
在C语言的静态数组中,如果要实现邻接表,一般图中的点的数量控制在 1000 左右的量级,是比较合适的,如果在大一点,存储会产生问题。
在C++中,有 vector 这种柔性数组,所以可以支持百万的量级。当然,也可以用C语言的静态数组来模拟实现一个C++中的柔性数组。
这里不讨论柔性数组的情况,只考虑 1000 量级的情况,如下:
#define maxn 1010
int adjSize[maxn];
int adj[maxn][maxn];
其中 adjSize[i] 代表 从 i 出发,能够直接到达的点的数量;
而 adj[i][j] 代表 从 i 出发,能够到达的第 j 个顶点;
在一个 n 个顶点的图上,由于任何一个顶点最多都有 n-1 个顶点相连,所以在C语言中,定义时必然要定义成二维数组,空间复杂度就是 O(n^2),对于一个稀疏图来说,数组实现,浪费空间严重,建议采用链表实现。
三、邻接表的链表存储
用链表来实现邻接表,实际上就是对于每个顶点,它能够到达的顶点,都被存储在以它为头结点的链表上。
对于如上的图,存储的就是四个链表:
0 -> 3 -> 2 -> NULL
1 -> 0
2 -> NULL
3 -> 1
这就是用链表表示的邻接表。注意:这里实际上每个链表的头结点是存储在一个顺序表中的,所以严格意义上来说是 顺序表 + 链表 的实现。
有向图的十字链表表示法
十字链表(Orthogonal List)是有向图的另外一种链式存储结构,它是邻接表和逆邻接表的结合。
在十字链表中,对应于有向图中的每一条弧有一个结点,每个顶点也有一个结点。
结点结构如下所示:
建立十字链表的时间复杂度与建立邻接表相同(算法与建立邻接表算法类似)。 容易找到以vi为尾的弧,也容易找到以vi为头的弧。
无向图的邻接多重表表示法
无向图的邻接表中每条边被存储了2次,这给某些图的操作带来不便。
邻接多重表(Adjacency Multilist)是无向图的另外一种链式存储结构,每一条边用一个结点表示,每个顶点也用一个结点表示,与十字链表类似。
四、邻接表的应用
邻接表一般应用在图的遍历算法,比如 深度优先搜索、广度优先搜索。更加具体的,应用在最短路上,比如 Dijkstra、Bellman-Ford、SPFA;以及最小生成树,比如 Kruskal、Prim; 还有 拓扑排序、强连通分量、网络流、二分图最大匹配 等等问题。
五、邻接表的优点
邻接表表示法的优点主要有 空间效率、遍历效率。
1)空间利用率高:邻接表通常比邻接矩阵更节省空间,尤其是对于稀疏图。因为邻接表仅需要存储实际存在的边,而邻接矩阵需要存储所有的边。
2)遍历速度:邻接表表示法在遍历与某个顶点相邻的所有顶点时,时间复杂度与顶点的度成正比。对于稀疏图,这比邻接矩阵表示法的时间复杂度要低。
六、邻接表的缺点
1)不适合存储稠密图:对于稠密图(即图中边的数量接近于 n^2),导致每个顶点的边列表过长,从而降低存储和访问效率。
2)代码复杂:相比于邻接矩阵,实现代码会更加复杂一些。
一、邻接矩阵的类定义
#include <iostream>
using namespace std;
#define inf - 1
class Graph { // (1)
private:
int vertices; // (2)
int ** edges; // (3)
public:
Graph(int vertices); // (4)
~Graph(); // (5)
void addEdge(int u, int v, int w); // (6)
void printGraph(); // (7)
};
(1) 这是一个类声明,定义了一个名为 Graph 的类。类的作用是表示一个图。
(2) 这是一个私有成员变量,用于存储图中顶点的数量。
(3) 这是一个私有成员变量,用于存储图中边的信息。它是一个二维指针数组,每个元素都是一个指向整数的指针,表示从一个顶点到另一个顶点的边的权重。
(4) 这是一个构造函数,用于初始化图的顶点数量,并为边的数组分配内存。
(5) 这是一个析构函数,用于释放动态分配的内存。
(6) 这是一个公共成员函数,用于向图中添加一条边。它接受三个整数参数,表示边的起始顶点 u 和目标顶点 v 以及边上的权重 w。
(7) 这是一个公共成员函数,用于打印图的邻接矩阵表示。
二、邻接矩阵的创建
Graph::Graph(int vertices) {
this -> vertices = vertices; // (1)
edges = new int * [vertices]; // (2)
for (int i = 0; i < vertices; i++) {
edges[i] = new int[vertices];
for (int j = 0; j < vertices; j++) {
edges[i][j] = inf; // (3)
}
}
}
(1) 表示这是一个 vertices 个顶点的图。
(2) 创建一个指针数组,实际上就是一个二级指针。edges[i] 代表由第 i 个顶点作为起始顶点的边的权重数组。
(3) 遍历所有的边,并且赋值为 inf,代表初始情况下,所有顶点之间都没有边。
三、邻接矩阵的销毁
Graph::~Graph() {
for (int i = 0; i < vertices; i++) {
delete[] edges[i]; // (1)
}
delete[] edges; // (2)
}
(1) 对于每个顶点,清理它的边内存。
(2) 清理所有顶点的边内存。
四、邻接矩阵的边添加
void Graph::addEdge(int u, int v, int w) {
edges[u][v] = w; // (1)
}
(1) edges[u][v] 代表的是有向边 (u -> v) 的权重。
五、邻接矩阵的打印
void Graph::printGraph() {
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
cout << edges[i][j] << " "; // (1)
}
cout << endl;
}
}
(1) 打印(i -> j) 这条边的权重。
六、邻接矩阵的完整源码
#include <iostream>
using namespace std;
#define inf - 1
class Graph {
private:
int vertices;
int ** edges;
public:
Graph(int vertices);
~Graph();
void addEdge(int u, int v, int w);
void printGraph();
};
Graph::Graph(int vertices) {
this -> vertices = vertices;
edges = new int * [vertices];
for (int i = 0; i < vertices; i++) {
edges[i] = new int[vertices];
for (int j = 0; j < vertices; j++) {
edges[i][j] = inf;
}
}
}
Graph::~Graph() {
for (int i = 0; i < vertices; i++) {
delete[] edges[i];
}
delete[] edges;
}
void Graph::addEdge(int u, int v, int w) {
edges[u][v] = w;
}
void Graph::printGraph() {
for (int i = 0; i < vertices; i++) {
for (int j = 0; j < vertices; j++) {
cout << edges[i][j] << " ";
}
cout << endl;
}
}
int main() {
int vertices = 5;
Graph graph(vertices);
graph.addEdge(0, 1, 1);
graph.addEdge(0, 2, 3);
graph.addEdge(1, 2, 2);
graph.addEdge(2, 3, 7);
graph.addEdge(3, 4, 9);
graph.addEdge(4, 0, 4);
graph.addEdge(4, 2, 5);
cout << "邻接矩阵表示的图:" << endl;
graph.printGraph();
return 0;
}
一、邻接表的类声明
class Graph {
private:
struct EdgeNode { // (1)
int vertex;
int weight;
EdgeNode * next;
};
struct VertexNode { // (2)
int vertex;
EdgeNode * firstEdge;
};
int vertices;
VertexNode * nodes; // (3)
public:
Graph(int vertices);
~Graph();
void addEdge(int u, int v, int w);
void printGraph();
};
(1) 结构体 EdgeNode 表示图中的边结点,包含顶点 vertex 、权重 weight 和指向下一个边结点的指针 next。
(2) 结构体 VertexNode 表示图中的顶点,包含顶点 vertex 和指向第一个边结点的指针 firstEdge。
(3) 成员变量 nodes 是一个指向 VertexNode 结构体的指针数组,用于存储图中所有的顶点。
二、邻接表的创建
Graph::Graph(int vertices) {
this -> vertices = vertices;
this -> nodes = new VertexNode[vertices];
for (int i = 0; i < vertices; i++) {
nodes[i].vertex = i;
nodes[i].firstEdge = NULL;
}
}
邻接表的创建,其实就是创建一个数组,这个数组的每个元素是一个顶点,它包含顶点编号 vertex 以及和这个顶点邻接的边的链表,firstEdge 指向的就是链表头,由于一开始没有顶点都是孤立的,没有和它相邻的边,所以初始的链表头指向空。
三、邻接表的销毁
Graph::~Graph() {
for (int i = 0; i < vertices; i++) {
EdgeNode * curr = nodes[i].firstEdge;
while (curr) {
EdgeNode * temp = curr;
curr = curr -> next;
delete temp;
}
}
delete[] nodes;
}
邻接表的销毁,就是获取每个顶点的邻接边的链表头,并且遍历链表作删除操作。
四、邻接表的边添加
void Graph::addEdge(int u, int v, int w) {
EdgeNode * newNode = new EdgeNode;
newNode -> vertex = v;
newNode -> weight = w;
newNode -> next = nodes[u].firstEdge;
nodes[u].firstEdge = newNode;
}
邻接表的边添加,就是添加一条 (u -> v),权重为 w 的有向边,采用的是链表的头插法。这样边插入的时间复杂度是 O(1) 的。
五、邻接表的打印
void Graph::printGraph() {
for (int i = 0; i < vertices; i++) {
EdgeNode * curr = nodes[i].firstEdge;
cout << "Vertex " << i << ": ";
while (curr) {
cout << curr -> vertex << " (" << curr -> weight << ") ";
curr = curr -> next;
}
cout << endl;
}
}
打印就是遍历每个顶点,然后再遍历它的邻接边链表,把 邻接顶点 和 邻接边权重 打印出来。
六、邻接表的完整源码
#include <iostream>
using namespace std;
class Graph {
private:
struct EdgeNode {
int vertex;
int weight;
EdgeNode * next;
};
struct VertexNode {
int vertex;
EdgeNode * firstEdge;
};
int vertices;
VertexNode * nodes;
public:
Graph(int vertices);
~Graph();
void addEdge(int u, int v, int w);
void printGraph();
};
Graph::Graph(int vertices) {
this -> vertices = vertices;
this -> nodes = new VertexNode[vertices];
for (int i = 0; i < vertices; i++) {
nodes[i].vertex = i;
nodes[i].firstEdge = NULL;
}
}
Graph::~Graph() {
for (int i = 0; i < vertices; i++) {
EdgeNode * curr = nodes[i].firstEdge;
while (curr) {
EdgeNode * temp = curr;
curr = curr -> next;
delete temp;
}
}
delete[] nodes;
}
void Graph::addEdge(int u, int v, int w) {
EdgeNode * newNode = new EdgeNode;
newNode -> vertex = v;
newNode -> weight = w;
newNode -> next = nodes[u].firstEdge;
nodes[u].firstEdge = newNode;
}
void Graph::printGraph() {
for (int i = 0; i < vertices; i++) {
EdgeNode * curr = nodes[i].firstEdge;
cout << "Vertex " << i << ": ";
while (curr) {
cout << curr -> vertex << " (" << curr -> weight << ") ";
curr = curr -> next;
}
cout << endl;
}
}
int main() {
Graph graph(5);
graph.addEdge(0, 1, 4);
graph.addEdge(0, 2, 2);
graph.addEdge(1, 2, 3);
graph.addEdge(2, 3, 4);
graph.addEdge(3, 4, 2);
graph.printGraph();
return 0;
}
深度优先搜索(DFS——DepthFirstSearch)
基本思想:仿树的先序遍历过程。从顶点v出发进行深度优先遍历的方法是:
(1)从图中某个顶点v出发,访问v
(2)依次从v的各个未被访问的邻接点出发进行深度优先遍历,直至图中所有和v有路径相通的顶点都被访问到;
(3)如果此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点做起始点,重复上述过程,直至图中所有顶点都被访问到为止。
深度优先遍历是一个递归的过程
基于邻接矩阵的DFS
基于邻接表的DFS
用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为O(n2)。
用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问 n个头结点的时间,时间复杂度为O(n+e)。
结论:
稠密图适于在邻接矩阵上进行深度遍历;
稀疏图适于在邻接表上进行深度遍历。
广度优先遍历
——仿树的层次遍历过程
⑴ 访问顶点 v
⑵ 依次访问 v 的各个未被访问的邻接点 v1 , v2 , …, vk
⑶ 分别从 v1 , v2 , …, vk 出发依次访问它们未被访问的邻接点,直至访问所有与顶点 v 有路径相通的顶点
“先被访问顶点的邻接点”先于“后被访问顶点的邻接点”
广度优先搜索不是一个递归的过程,其算法也不是递归的。
从图中某个顶点v出发,访问v,并置visited[v]的值为1,然后将v进队。
只要队列不空,则重复下述处理。
① 队头顶点u出队 ② 依次检查u的所有邻接点w,如果visited[w]的值为0,则访问w,并置visited[w]的值为1,然后将w进队。
DFS
const int MAXN = 7;
void dfs(int u) {
if(visit[u]) { // 1
return ;
}
visit[u] = true; // 2
dfs_add(u); // 3
for(int i = 0; i < MAXN; ++i) {
int v = i;
if(adj[u][v]) { // 4
dfs(v); // 5
}
}
}
void dfs_add(int u) {
ans[ansSize++] = u;
}
1、visit[MAXN] 数组是一个bool数组,用于标记某个节点是否已访问,初始化都为 false;这里对已访问结点执行回溯;
2、visit[u] = true; 对未访问结点 u 标记为已访问状态;
3、dfs_add(u); 用来将 u 存储到的访问序列中,实际函数实现如下:我会在以后的文章阐述
BFS
class BFSGraph {
public:
int bfs(BFSState startState);
private:
void bfs_extendstate(const BFSState& fromState);
void bfs_initialize(BFSState startState);
private:
Queue queue_;
};
其中 bfs 作为一个框架接口供外部调用,基本是不变的,实现如下:
const int inf = -1;
int BFSGraph::bfs(BFSState startState) {
bfs_initialize(startState); // 1)
while (!queue_.empty()) {
BFSState bs = queue_.pop();
if (bs.isFinalState()) { // 2)
return bs.getStep();
}
bfs_extendstate(bs); // 3)
}
return inf;
}
1)初始化整个广搜的路径图,确保每个状态都是未访问状态;
2)如果队列不为空,则不断弹出队列中的首元素,如果是结束状态则直接返回状态对应的步数;
3)如果不是结束状态,对它进行状态扩展,扩展方式调用接口 ```bfs_extendstate```,不同问题的扩展方式不同,下文会对不同问题的状态扩展进行讲解。
我会在以后的文章阐述
最小生成树(Minimum Cost Spanning Tree,MST)
生成树的代价:在无向连通网中,生成树上各边的权值之和
最小生成树:保证连通的情况下,成本最小问题
Prim算法归并顶点(加点法),与边数无关,适于稠密网。
Kruskal算法归并边(加边法),适于稀疏网。这些在离散数学有所记载。
PRIM
(1)初始化U={u}。u到其他顶点的所有边为候选边;
(2)重复以下步骤n-1次,使得其他n-1个顶点被加入到U中:
从候选边中挑选权值最小的边输出,设该边在V-U中的顶点是k,将k加入U中;
考察当前V-U中的所有顶点j,修改候选边:若(k,j)的权值小于原来和顶点j关联的候选边,则用(k,j)取代后者作为候选边。
n方的复杂度
#define INF 32767 //INF表示∞
void Prim(MGraph G,int u) //从初始顶点编号u开始构造图G的最小生成树
{ int lowcost[MAX_Vexnum], closest[MAX_Vexnum];
int mincost , i, j, k;
for (i=0;i<G.vexnum;i++) //给lowcost[]和closest[]置初值
{ lowcost[i]=G.arcs[u][i];
closest[i]=u;
}
lowcost[u]=0; //标记u已经加入U
for (i=1;i< G.vexnum;i++) //输出(n-1)条边
{ mincost=INF;
for (j=0;j<G.vexnum;j++) //在(V-U)中找出离U最近的顶点k
if (lowcost[j]!=0 && lowcost[j]<mincost)
{ mincost=lowcost[j];
k=j; //k记录最近顶点编号
}
printf(" 边(%d,%d)权为:%d\n",closest[k],k,mincost);
lowcost[k]=0; //标记k已经加入U
for (j=0;j<G.vexnum;j++) //修改数组lowcost和closest
if (lowcost[j]!=0 && G.arcs[k][j]<lowcost[j])
{ lowcost[j]=G.arcs[k][j];
closest[j]=k;
}
}
}
Kruskal
按边大小递增排序,选取了n-1条边,Kruskal算法的时间复杂度为O(elog2e)。就是所有线段排序,一个一个从小到大加边即可。
#define MaxV 100 //最大顶点数
//定义辅助数组EdgeList
struct {
VertexType head; //边的始点
VertexType tail; //边的终点
Arctype weight; //边上的权值
}EdgeList[(MaxV*(MaxV-1))/2];
int VexSet[Max_Vexnum];//辅助数组VexSet定义各顶点属于的连通分量编号
void MiniSpanTree_Kruskal(MGraph G)
{ //无向网G以邻接矩阵形式存储,构造G的最小生成树T,输出T的各条边
int i , j , v1 , v2 , vs1 , vs2;
Sort(EdgeList); //将数组EdgeList中的元素按权值从小到大排序
for(i = 0; i < G.vexnum; ++i) //辅助数组,表示各顶点自成一个连通分量
VexSet[i] = i;
for(i = 0; i < G.arcnum; ++i)
{//依次查看排好序的数组EdgeList中的边是否在同一连通分量上
v1 =LocateVex(G, EdgeList[i].head); //v1为边的始点head的下标
v2 =LocateVex(G, EdgeList[i].tail); //v2为边的终点tail的下标
vs1 = VexSet[v1]; //获取边EdgeList[i]的始点所在的连通分量vs1
vs2 = VexSet[v2]; //获取边EdgeList[i]的终点所在的连通分量vs2
if(vs1 != vs2) //边的两个顶点分属不同的连通分量
{ cout << EdgeList[i].head << "-->" << EdgeList[i].tail << endl;//输出此边
for(j = 0; j < G.vexnum; ++j) //合并vs1和vs2两个分量,即两个集合统一编号
if(VexSet[j] == vs2) VexSet[j] = vs1; //集合编号为vs2的都改为vs1
}//if
}//for
}//MiniSpanTree_Kruskal
void main()
{
MGraph G;
CreateUDN(G);
cout <<endl;
cout << "*****无向网G创建完成!*****" << endl;
cout <<endl;
MiniSpanTree_Kruskal(G);
}///main
两种常见的最短路径问题:
邻接矩阵存储 存储dist(v, vi)待定路径表(当前的最短路径)
整型数组dist[n]:存储当前最短路径的长度
字符串数组path[n]:存储当前的最短路径,即顶点序列
void Dijkstra(MGraph G, int v) {
int dist[MAX_Vexnum], path[MAX_Vexnum];
bool S[MAX_Vexnum];
int mindis, i, j, u;
for (i = 0; i < G.vexnum; i++) //dist和path数组初始化
{
dist[i] = G.arcs[v][i]; //距离初始化
S[i] = false; //S[]置false
if (G.arcs[v][i] < INF) //路径初始化
path[i] = v; //顶点v到i有边时
else
path[i] = -1; //顶点v到i没边时
}
S[v] = true; //源点v放入S中
dist[v] = 0; //源点到源点的距离为0
for (i = 0; i < G.vexnum; i++) //循环n-1次
{
mindis = INF;
for (j = 0; j < G.vexnum; j++) //找最小路径长度顶点u
if (S[j] == false && dist[j] < mindis) {
u = j;
mindis = dist[j];
}
S[u] = true; //顶点u加入S中
for (j = 0; j < G.vexnum; j++) //修改v0到不在S中的顶点的距离 //调整
if (S[j] == 0)
if (G.arcs[u][j] < INF && dist[u] + G.arcs[u][j] < dist[j]) {
dist[j] = dist[u] + G.arcs[u][j];
path[j] = u;
}
}
DisPath(dist, path, S, G.vexnum, v); //回溯,输出最短路径
}
迪杰斯特拉算法的时间复杂度为O(n2)
Floyd算法
void Floyd(MGraph G) //求每对顶点之间的最短路径
{
int D[MAX_Vexnum][MAX_Vexnum]; //建立A数组
int P[MAX_Vexnum][MAX_Vexnum]; //建立P数组
int i, j, k;
for (i = 0; i < G.vexnum; i++) //D和P数组初始化的二重循环
for (j = 0; j < G.vexnum; j++) {
D[i][j] = G.arcs[i][j];
if (i != j && G.arcs[i][j] < INF)
P[i][j] = i; //i和j顶点之间有一条边时
else //i和j顶点之间没有一条边时
P[i][j] = -1;
}
for (k = 0; k < G.vexnum; k++) //求Dk[i][j]
{
for (i = 0; i < G.vexnum; i++)
for (j = 0; j < G.vexnum; j++)
if (D[i][j] > D[i][k] + D[k][j]) //找到更短路径
{
D[i][j] = D[i][k] + D[k][j]; //修改路径长度
P[i][j] = P[k][j]; //修改最短路径为经过顶点k
}
}
}
AOV网与拓扑排序
教学计划的制定:计算机专业的学生必须完成一系列规定的基础课和专业课才能毕业,假设这些课程的名称与相应代号有如下关系:对学生选课工程图进行拓扑排序,来制定教学计划
课程代号 | 课程名称 | 先修课程 |
C1 | 高等数学 | |
C2 | 程序设计基础 | |
C3 | 离散数学 | C1, C2 |
C4 | 数据结构 | C3, C2 |
C5 | 高级语言程序设计 | C2 |
C6 | 编译方法 | C5, C4 |
C7 | 操作系统 | C4, C9 |
C8 | 普通物理 | C1 |
C9 | 计算机原理 | C8 |
找入度为0的顶点,输出该顶点,删除从它出发的所有出边,返回,直到剩余的图中不再存在没有前驱的顶点为止
算法基于有向图的邻接表存储表示
为了便于查找入度, 将邻接表定义中的VNode类型修改如下:
typedef struct //表头结点类型
{
VertexType data; //顶点信息
int count; //存放顶点入度 用于找入度为0的顶点
ArcNode * firstarc; //指向第一条边
}
VNode;
void TopSort(ALGraph G) //拓扑排序算法
{
int i, j;
ArcNode * p;
SqStack * st;
InitStack(st);
for (i = 0; i < G.vexnum; i++) //入度置初值0
G.adjlist[i].count = 0;
for (i = 0; i < G.vexnum; i++) //求所有顶点的入度
{
p = G.adjlist[i].firstarc;
while (p != NULL) {
G.adjlist[p -> adjvex].count++;
p = p -> nextarc;
}
}
for (i = 0; i < G.vexnum; i++) //将入度为0的顶点进栈
if (G.adjlist[i].count == 0) Push(st, i);
while (!EmptyStack(st)) //栈不空循环排序
{
Pop(st, i) //出栈一个顶点i
printf("%d ", i); //输出该顶点
p = G.adjlist[i].firstarc; //找第一个邻接点
while (p != NULL) //将顶点i的出边邻接点的入度减1
{
j = p -> adjvex;
G.adjlist[j].count--;
if (G.adjlist[j].count == 0) //将入度为0的邻接点进栈
Push(st, j);
p = p -> nextarc; //找下一个邻接点
}
}
}
时间复杂度为O(n+e)
AOE网与关键路径
。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。
今天的如果没有前置 链表、哈希表、深度广度优先搜索 的基础,做起来会比较吃力。
所以如果不会,也不要灰心丧气。
邻接矩阵
省份数量(DFS)
使网格图至少有一条有效路径的最小代价(BFS)
冗余连接(DFS)
喧闹和富有(DFS)
LCP 07. 传递信息 (DP)
矩阵中的最长递增路径(DP)
访问所有节点的最短路径 (状态压缩DP)
邻接表
钥匙和房间(DFS)
面试题 04.01. 节点间通路(BFS)
寻找图中是否存在路径(DFS)
克隆图(DFS)
所有可能的路径(DFS)
找到最终的安全状态(拓扑排序)
朴素边表示法
前向星
剑指 Offer II 113. 课程顺序(拓扑排序)
课程表 II(拓扑排序)
有向图
钥匙和房间(DFS)
面试题 04.01. 节点间通路(BFS)
有向无环图
所有可能的路径(DFS)
喧闹和富有(DFS)
课程表(拓扑排序)
找到最终的安全状态(拓扑排序)
剑指 Offer II 113. 课程顺序(拓扑排序)
课程表 II(拓扑排序)
剑指 Offer II 114. 外星文字典(拓扑排序)
并行课程 III(拓扑排序 + DP)
有向图中最大颜色值(拓扑排序 + SPFA)
无向图
寻找图中是否存在路径(DFS)
省份数量(DFS)
由斜杠划分区域 (并查集)
等式方程的可满足性(并查集)
连接所有点的最小费用(并查集)
连通网络的操作次数(并查集)
找到最小生成树里的关键边和伪关键边(并查集)
可能的二分法(并查集)
处理含限制条件的好友请求(并查集)
带权图
使网格图至少有一条有效路径的最小代价(BFS)
最大化一张图中的路径价值(DFS)
到达目的地的第二短时间(BFS)
二分图
剑指 Offer II 106. 二分图(DFS)
判断二分图(DFS)