稀疏图:有很少边或者弧的图(e < n * log(2, n))
弧:带有方向的边也叫弧
稠密图:有较多的边或者弧的图
网:边或者弧带有权值的图
邻接:两个顶点被边或者弧相连称为邻接,无向图中使用(vi, vj)表示vi与vj互为邻接点;有向图中使用
关联(依附):边或者弧与顶点之间的关系,说白了就是这条边或者弧就是某两个顶点连一起得到的,这个关系就是关联
顶点的度:与顶点关联的边的数目;在有向图中,顶点的度等于顶点的出度和入度之和
入度:以当前顶点为终点的有向边的条数,图二中A的入度为2
出度:以当前顶点为起点的有向边的条数,图二中A的出度为1
有向树:仅有一个顶点入度为0,其余顶点的入度为1的有向图被称为有向树
路径:接续的边(不间断的边边相连)构成的顶点序列
路径长度:网的某路径的权值之和
回路(环):第一个顶点和最后一个顶点具有相同的路径
简单路径:除了起点和终点是一样的,其他顶点均不同的路径
简单回路(简单环):除了路径的起点和终点一样外,其余的顶点均不相同的路径
连通图(强连通图):在无(有)向图G = (V, {E})中,若对任何两个顶点v、都存在从v到u的路径,则称G是连通图,图2就不是强连通图,因为A到D有路径,但是不存在D到A的路径
权:就是网的边上的权值
子图:G = (V, {E}),G1 = (V1, {E1}),若V1∈V,E1∈E,则称G1是G的子图
连通分量(强连通分量):无向图G的极大连通子图称为G的连通分量
极大连通子图:该子图是G的连通子图,将G的任何不在该子图中的顶点加入,子图不在连通
强连通分量:有向图G的极大连通子图称为G的强连通分量
极大强连通子图:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不在是强连通
极小连通子图:该子图是G的连通子图,在该子图中删除任何一条边就不再连通
生成树:包含无向图G所有顶点的极小连通子图
生成森林:对非连通图,由各个连通分量的生成树的集合
CreateGraph(&G, V, VR)
初始条件:V是图的顶点集,VR是图中弧的集合
操作结果:按V和VR的定义构造图G
DFSTraverse(G)
初始条件:图存在
操作结果:对图进行深度优先遍历
BFSTraverse(G)
初始条件:图G存在
操作结果:对图进行广度优先遍历
上图的邻接矩阵为
A B C D E A 0 1 0 1 0 B 1 0 1 0 1 C 0 1 0 0 1 D 1 0 0 0 1 E 0 1 1 1 0
上图的邻接矩阵为
A B C D A 0 1 0 1 B 0 0 0 0 C 1 0 0 0 D 0 0 1 0
V1 V2 V3 V4 V5 V6 V1 0 5 0 7 0 0 V2 0 0 4 0 0 0 V3 8 0 0 0 0 9 V4 0 0 5 0 0 6 V5 0 0 0 5 0 0 V6 3 0 0 0 1 0
// 邻接矩阵存储法的思想是基于两个数组分别存储顶点(一维)和邻接矩阵(二维)
typedef char VertexType; // 顶点类型
typedef int ArcType; // 边的权值的类型
#define MAXSIZE 10 // 最大顶点数目
typedef struct {
VertexType vertex[MAXSIZE];
ArcType arcs[MAXSIZE][MAXSIZE];
// 记录当前顶点数、边数
int vernum, arcnum;
} AMGraph;
// 用于表示存在关联的两个顶点以权值
typedef struct {
VertexType vertex1;
VertexType vertex2;
ArcType weight;
} Linked;
// 辅助函数,用于获取顶点在顶点集的索引
int getIndex(AMGraph *graph, VertexType name);
// 建立无向网
// 参数依次是边的顶点个数、边的个数、网、顶点集、关联数组集
void create(int _vernum, int _arcnum, AMGraph *graph, VertexType vertexes[_vernum], Linked linked[_arcnum]);
// 直观的打印出邻接矩阵
void printMatrix(AMGraph *graph);
//
// Created by 20281 on 2023/6/11.
//
#include "AdjacencyMatrix.h"
#include "stdio.h"
int getIndex(AMGraph *graph, VertexType name){
for (int i=0;i<graph->vernum;i++){
if (graph->vertex[i] == name) return i;
else continue;
}
return -1;
}
void create(int _vernum, int _arcnum, AMGraph *graph, VertexType vertexes[_vernum], Linked linked[_arcnum]){
graph->vernum = _vernum;
graph->arcnum = _arcnum;
for (int i=0;i<_vernum;i++){
graph->vertex[i] = vertexes[i];
}
for (int i=0;i<_vernum;i++){
for (int j=0;j<_vernum;j++){
graph->arcs[i][j] = 0;
}
}
for (int i=0;i<_arcnum;i++){
int ver1 = getIndex(graph, linked[i].vertex1);
int ver2 = getIndex(graph, linked[i].vertex2);
ArcType weight = linked[i].weight;
graph->arcs[ver1][ver2] = weight;
graph->arcs[ver2][ver1] = weight;
}
}
void printMatrix(AMGraph *graph){
printf("\t");
for (int i=0;i<graph->vernum;i++){
printf("%c\t", graph->vertex[i]);
}
printf("\n");
for (int i=0;i<graph->vernum;i++){
printf("%c\t", graph->vertex[i]);
for (int j=0;j<graph->vernum;j++){
printf("%d\t", graph->arcs[i][j]);
}
printf("\n");
}
}
对上面网的测试
//
// Created by 20281 on 2023/6/11.
//
#include "AdjacencyMatrix.h"
int main(){
// 创建img8中的网
Linked linkeds[6] = {
{'A', 'B', 4},
{'A', 'D', 6},
{'B', 'C', 3},
{'B', 'F', 10},
{'C', 'E', 7},
{'D', 'E', 11},
};
VertexType vertexes[6] = {'A', 'B', 'C', 'D', 'E', 'F'};
AMGraph amGraph;
create(6, 6, &amGraph, vertexes, linkeds);
// 打印
printMatrix(&amGraph);
return 0;
}
输出结果
A B C D E F
A 0 4 0 6 0 0
B 4 0 3 0 0 10
C 0 3 0 0 7 0
D 6 0 0 0 11 0
E 0 0 7 11 0 0
F 0 10 0 0 0 0
直观展示无向图的邻接表存储法,空间复杂度O(n+2e),其中n是顶点数,e是边数,下同
typedef char VertexType; // 顶点类型
typedef int ArcType; // 边的权值的类型
#define MAXSIZE 10 // 最大顶点数目
// 被链接的顶点结构
typedef struct ArcNode{
// 当前顶点的索引
int index;
// 被链接的顶点的所在的顶点结构
struct ArcNode *next;
// 权值
ArcType weight;
}ArcNode ;
// 顶点集内的每个结点
typedef struct{
// 顶点数据
VertexType vert;
// 指向的第一个与之链接的顶点
ArcNode *firstArc;
} VNode, AdjList[MAXSIZE];
// 网的表示
typedef struct {
// 顶点集组成的一维数组 AdjList vertexes 相当于 VNode vnode[MAXSIZE]
AdjList vertexes;
int vernum, arcnum;
}ALGraph;
// 用于表示存在关联的两个顶点以权值
typedef struct {
VertexType vertex1;
VertexType vertex2;
ArcType weight;
} Linked2;
// 辅助函数,用于获取顶点在顶点集的索引
int getIndex2(ALGraph *graph, VertexType name);
// 初始化
// 传入参数依次是网本身、顶点个数、边数、顶点集、关联集
// 方法一是改变指针位置来更新链接更新
void createALG1(ALGraph *graph, int _vernum, int _arcnum, VertexType vertexes[_vernum], Linked2 linked[_arcnum]);
// 方法二是在链表尾部不断追加来更新链接更新
// 简单来说,createALG1 是将新节点直接插入链表的开头,而 createALG2 是将新节点插入链表的末尾。
void createALG2(ALGraph *graph, int _vernum, int _arcnum, VertexType vertexes[_vernum], Linked2 linked[_arcnum]);
// 直观的打印出邻接链表
void printLinkedList(ALGraph *graph);
// 释放链表
void freeLinkedList(ALGraph * graph);
//
// Created by 20281 on 2023/6/12.
//
#include "AdjacentLinkedList.h"
#include "stdio.h"
#include "stdlib.h"
int getIndex2(ALGraph *graph, VertexType name){
for (int i=0;i<graph->vernum;i++){
if (graph->vertexes[i].vert == name){
return i;
} else continue;
}
return -1;
}
void createALG1(ALGraph *graph, int _vernum, int _arcnum, VertexType vertexes[_vernum], Linked2 linked[_arcnum]){
graph->arcnum = _arcnum;
graph->vernum = _vernum;
for (int i=0;i<_vernum;i++){
graph->vertexes[i].vert = vertexes[i];
graph->vertexes[i].firstArc = NULL;
}
for (int i=0;i<_arcnum;i++){
int index1 = getIndex2(graph, linked[i].vertex1);
int index2 = getIndex2(graph, linked[i].vertex2);
int weight = linked[i].weight;
ArcNode * newNode = (ArcNode* ) malloc(sizeof(ArcNode));
newNode->index = index2;
newNode->weight = weight;
newNode->next = graph->vertexes[index1].firstArc;
graph->vertexes[index1].firstArc = newNode;
ArcNode * newNode2 = (ArcNode* ) malloc(sizeof(ArcNode));
newNode2->index = index1;
newNode2->weight = weight;
newNode2->next = graph->vertexes[index2].firstArc;
graph->vertexes[index2].firstArc = newNode2;
}
}
void createALG2(ALGraph* graph, int _vernum, int _arcnum, VertexType vertexes[_vernum], Linked2 linked[_arcnum]) {
graph->arcnum = _arcnum;
graph->vernum = _vernum;
// 初始化顶点集
for (int i = 0; i < _vernum; i++) {
graph->vertexes[i].vert = vertexes[i];
graph->vertexes[i].firstArc = NULL;
}
// 构建邻接链表
for (int i = 0; i < _arcnum; i++) {
int index1 = getIndex2(graph, linked[i].vertex1);
int index2 = getIndex2(graph, linked[i].vertex2);
int weight = linked[i].weight;
// 创建新节点1
ArcNode* newNode = (ArcNode*)malloc(sizeof(ArcNode));
newNode->index = index2;
newNode->weight = weight;
newNode->next = NULL;
// 将新节点1插入链表1的末尾
if (graph->vertexes[index1].firstArc == NULL) {
graph->vertexes[index1].firstArc = newNode;
} else {
ArcNode* currentFirstNode = graph->vertexes[index1].firstArc;
while (currentFirstNode->next != NULL) {
currentFirstNode = currentFirstNode->next;
}
currentFirstNode->next = newNode;
}
// 创建新节点2
ArcNode* newNode2 = (ArcNode*)malloc(sizeof(ArcNode));
newNode2->index = index1;
newNode2->weight = weight;
newNode2->next = NULL;
// 将新节点2插入链表2的末尾
if (graph->vertexes[index2].firstArc == NULL) {
graph->vertexes[index2].firstArc = newNode2;
} else {
ArcNode* currentFirstNode2 = graph->vertexes[index2].firstArc;
while (currentFirstNode2->next != NULL) {
currentFirstNode2 = currentFirstNode2->next;
}
currentFirstNode2->next = newNode2;
}
}
}
void printLinkedList(ALGraph *graph){
for (int i=0;i<graph->vernum;i++){
printf("%c-->", graph->vertexes[i].vert);
ArcNode * firstLinkedNode = graph->vertexes[i].firstArc;
while (firstLinkedNode != NULL){
printf("(%d, %d)-->", firstLinkedNode->weight, firstLinkedNode->index);
firstLinkedNode = firstLinkedNode->next;
}
printf("\n");
}
}
void freeLinkedList(ALGraph * graph){
for (int i=0;i<graph->vernum;i++){
ArcNode *currentNode = graph->vertexes[i].firstArc;
while (currentNode !=NULL){
ArcNode * temp = currentNode;
currentNode = currentNode->next;
free(temp);
}
}
}
对上面网的测试
// 邻接表
ALGraph alGraph;
Linked2 linkeds_2[6] = {
{'A', 'B', 4},
{'A', 'D', 6},
{'B', 'C', 3},
{'B', 'F', 10},
{'C', 'E', 7},
{'D', 'E', 11},
};
ALGraph alGraph2;
Linked2 linkeds_3[6] = {
{'A', 'B', 4},
{'A', 'D', 6},
{'B', 'C', 3},
{'B', 'F', 10},
{'C', 'E', 7},
{'D', 'E', 11},
};
createALG1( &alGraph, 6, 6, vertexes, linkeds_2);
createALG2( &alGraph2, 6, 6, vertexes, linkeds_3);
// 直观打印
printLinkedList(&alGraph);
printLinkedList(&alGraph2);
// 释放链表内存
freeLinkedList(&alGraph);
freeLinkedList(&alGraph2);
输出结果
A-->(6, 3)-->(4, 1)-->
B-->(10, 5)-->(3, 2)-->(4, 0)-->
C-->(7, 4)-->(3, 1)-->
D-->(11, 4)-->(6, 0)-->
E-->(11, 3)-->(7, 2)-->
F-->(10, 1)-->
A-->(4, 1)-->(6, 3)-->
B-->(4, 0)-->(3, 2)-->(10, 5)-->
C-->(3, 1)-->(7, 4)-->
D-->(6, 0)-->(11, 4)-->
E-->(7, 2)-->(11, 3)-->
F-->(10, 1)-->
最直观的例子就是“走迷宫”。假设你站在迷宫的某个岔路口,然后想找到出口。你随意选择一个岔路口来走,走着走着发现走不通的时候,你就回退到上一个岔路口,重新选择一条路继续走,直到最终找到出口(这个出口可以理解为最后一个被遍历的顶点)。这种走法就是一种深度优先搜索策略。
// DFS算法的实现
// index是开始起点的索引、visited是辅助数组、_c是计数器,我特地设定的,用于统计递归了多少次
void _DFS(AMGraph *graph, int index, int visited[], int *_c);
void DFS(AMGraph *graph, int index);
void _DFS(AMGraph *graph, int index, int visited[], int *_c){
*_c += 1;
printf("%c", graph->vertex[index]);
visited[index] = 1;
for (int i=0;i<graph->vernum;i++){
// 当前的顶点与邻近的顶点之间有关联并且邻近顶点未被访问情况下
if (graph->arcs[index][i] != 0 && visited[i] == 0){
_DFS(graph, i, visited, _c);
}
}
}
// _DFS才是核心,DFS只是辅助作用和直观打印过程
void DFS(AMGraph *graph, int index){
// 辅助数组,用于记录每个点是否被访问
int visitedArray[graph->vernum];
for (int i = 0;i<graph->vernum;i++){
visitedArray[i] = 0;
}
int _c = 0;
printf("开始遍历……\n");
_DFS(graph, index, visitedArray, &_c);
printf("\n");
printf("函数触发了%d次……\n", _c);
}
// 对img11进行DFS
// 用1表示边边关联,而不是权重
Linked linked_img11[6] = {
{'1', '2', 1},
{'1', '3', 1},
{'1', '4', 1},
{'2', '5', 1},
{'3', '5', 1},
{'4', '6', 1}
};
VertexType vertexes_img11[6] = {'1', '2', '3', '4', '5', '6'};
AMGraph amGraph_img11;
create(6, 6, &amGraph_img11, vertexes_img11, linked_img11);
printMatrix(&amGraph_img11);
// DFS
DFS(&amGraph_img11, 1);
// 结果
// 开始遍历……
// 213546
// 函数触发了6次……
// BFS算法的实现
// index 表示从哪个顶点作为开始访问
void _BFS(ALGraph *graph, int index, int vertexArray[], Queue *queue);
void BFS(ALGraph *graph, int index);
void _BFS(ALGraph *graph, int index, int vertexArray[], Queue *queue){
push(queue, index);
printf("%c ", graph->vertexes[index].vert);
vertexArray[index] = 1;
while (isEmpty(queue) != 1){
int outIndex = pop(queue);
// 找到与出队顶点相关的且没被访问的顶点
ArcNode * currentNode = graph->vertexes[outIndex].firstArc;
// 找到相关点
while (currentNode != NULL){
// 找到没被访问的点
if (vertexArray[currentNode->index] == 0){
push(queue, currentNode->index);
printf("%c ", graph->vertexes[currentNode->index].vert);
vertexArray[currentNode->index] = 1;
} else {}
currentNode = currentNode->next;
}
}
}
void BFS(ALGraph *graph, int index){
int vertexArray[graph->vernum];
for (int i=0;i<graph->vernum;i++){
vertexArray[i] = 0;
}
printf("Begin Traversal ......\n");
Queue queue;
createQueue(&queue);
_BFS(graph, index, vertexArray, &queue);
printf("\n");
}
// 对img12实现邻接链表的BFS演示
Linked2 linked_img12[9] = {
{'1', '2', 1},
{'1', '3', 1},
{'2', '4', 1},
{'2', '5', 1},
{'3', '6', 1},
{'3', '6', 1},
{'4', '8', 1},
{'5', '8', 1},
{'6', '7', 1},
};
VertexType img12_vec[8] = {'1', '2', '3', '4', '5', '6', '7', '8'};
ALGraph alGraph_img12;
createALG2(&alGraph_img12, 8, 9, img12_vec, linked_img12);
printLinkedList(&alGraph_img12);
BFS(&alGraph_img12, 2);
// 释放内存
freeLinkedList(&alGraph_img12);
1-->(1, 1)-->(1, 2)-->
2-->(1, 0)-->(1, 3)-->(1, 4)-->
3-->(1, 0)-->(1, 5)-->(1, 5)-->
4-->(1, 1)-->(1, 7)-->
5-->(1, 1)-->(1, 7)-->
6-->(1, 2)-->(1, 2)-->(1, 6)-->
7-->(1, 5)-->
8-->(1, 3)-->(1, 4)-->
Begin Traversal ......
3 1 6 2 7 4 5 8
ALGraph Prim(AlGraph *graph){
ALGraph mst;
// 初始
U = {mst.vertexes[0].vert};
V_U = {除了mst.vertexes[0].vert外的所有的点};
// 这里面TE就是Linked2类型的
Linked2 links[graph->vernum - 1];
// links当前添加元素的计数
_counter = 0;
// 直到U = V为止,也就是说U长度为V为止
while(getLength(U) != graph->vernum){
// 在所有u∈U,v∈V-U的边(u, v)∈E中,找一条权值最小的边
// 将(u。, v,)并入集合TE中,同时v。加入U中
link = {};
link.weight = 0;
for (u in U){
for (v in V_U){
// 如果两点成关联
if (islinked(u, v)){
// 每次都比较,获取最小权值的关联
if (getWeight(u, v) <= link.weight){
link.vertex1 = u;
link.vertex2 = v;
}
}
}
}
// 此时拿到了最小权值边
append(&U, link.vertex1);
remove(&V_U, link.vertex2);
links[_counter] = link;
_couter++;
}
// 最后获取这棵树
createALG2(&mst, _counter+1, _counter, U, links);
return mst;
}
ALGraph Kruskal(AlGraph *graph){
ALGraph mst;
// 初始化只有独立的顶点,没有关联的网
createALG2(&mst, graph->vernum, 0, graph->vertexes, NULL);
// 获取graph的有关联的边点,得到Linked2 links
getLinks(graph);
// 先从links找到权值最小边mlink添加到mst,并从links删除此组合
getMinCompose(links);
remove(&links, mlink);
append(&mst, mlink);
// 计数,成功了graph->vernum-2次停止
_counter = 1;
while (_counter < graph->vernum-2){
// 从links找到权值最小且不形成的link添加到mst,并删除这样的link
// 那如果这个边权值最小,但形成闭环,是不是要从考虑的边中删除
getMinCompose(links); // 设为temp_link;
// 形成闭环
if (isLoop(mst, temp_link)){}
else{
append(&mst, temp_link);
_counter++;
}
remove(&links, temp_link);
}
return mst;
}
算法名字 | Prim算法 | Kruskal算法 |
---|---|---|
算法思想 | 选择点 | 选择边 |
时间复杂度 | O(n^2) | O(ElogE) |
适用范围 | 稠密网 | 稀疏网 |
对上图进行算法分析
备注
- 使用0表示此处无路径(很多教程或者视频使用无穷大)
- 使用‘-’符号表示此处此点已经找到最短路径
点名 | 从v0到各点的最短路径 | |||||
---|---|---|---|---|---|---|
v1 | 13 | 13 | 13 | - | - | - |
v2 | 8 | - | - | - | - | - |
v3 | 0 | 13 | - | - | - | - |
v4 | 30 | 30 | 19 | 19 | - | - |
v5 | 0 | 0 | 0 | 22 | 21 | 21 |
v6 | 32 | 32 | 32 | 20 | 20 | - |
当前最短点 | v2 | v3 | v1 | v4 | v6 | v5 |
路径长度 | 8 | 13 | 13 | 19 | 20 | 21 |
该图中左图就是一个有向无环图,右侧不是
对上面AOE网进行关键路径查找
顶点 | ve | vl |
---|---|---|
v1 | 0 | 0 |
v2 | 6 | 6 |
v3 | 4 | 6 |
v4 | 5 | 8 |
v5 | 7 | 7 |
v6 | 7 | 10 |
v7 | 16 | 16 |
v8 | 14 | 14 |
v9 | 18 | 18 |
活动 | e | l | l-e |
---|---|---|---|
a1 | 0 | 0 | 0 |
a2 | 0 | 2 | 2 |
a3 | 0 | 3 | 3 |
a4 | 6 | 6 | 0 |
a5 | 4 | 6 | 2 |
a6 | 5 | 8 | 3 |
a7 | 7 | 7 | 0 |
a8 | 7 | 7 | 0 |
a9 | 7 | 10 | 3 |
a10 | 16 | 16 | 0 |
a11 | 14 | 14 | 0 |
得到关键活动是:a1 a4 a7 a8 a10 a11
Warshall算法是一种用于解决传递闭包(transitive closure)问题的算法。传递闭包是指在有向图中,如果存在一条路径从节点i到节点j,那么节点i可以达到节点j。Warshall算法的目标是计算出图中所有节点对之间的可达性。
具体而言,Warshall算法通过一个二维矩阵(通常称为邻接矩阵)来表示有向图的连接关系。矩阵中的元素A[i][j]表示从节点i到节点j是否存在一条边。初始时,如果有一条边直接连接节点i和节点j,则A[i][j]为1,否则为0。
Warshall算法的主要思想是通过迭代更新邻接矩阵,以逐步计算出所有节点对之间的可达性。算法的步骤如下:
初始化邻接矩阵,将直接连接的边设置为1,没有直接连接的边设置为0。
对于每个节点k,遍历所有节点对(i, j),如果存在一条路径从节点i经过节点k到节点j,即A[i][k]为1且A[k][j]为1,则将A[i][j]设置为1。
重复步骤2,直到对每个节点都进行了一次遍历。最终的邻接矩阵将表示图中所有节点对之间的可达性。
通过Warshall算法,最终得到的邻接矩阵能够表明在图中从一个节点到另一个节点是否存在一条路径。如果A[i][j]为1,则表示节点i可以达到节点j;如果A[i][j]为0,则表示节点i无法达到节点j。
总结来说,Warshall算法的实质是通过迭代更新邻接矩阵,来计算出图中所有节点对之间的可达性,从而解决传递闭包问题。
声明:
部分图片源于互联网、部分更通俗易懂的概念源于搜索
教学看的是青岛大学王卓老师的
源代码下载:https://gitee.com/PythonnotJava/Data-structure-and-algorithm-collection/tree/master/ReGraph