知识图谱
1.相关词汇
欧拉通路:走所有边,整张图度为0(出度为负,入度为正)
哈密尔顿通路:走所有点
自相似结构:递归(去掉结点还是图)adjacent
弧头弧尾:有向弧箭头方向为弧头
最小生成树
2.图的两种表达方式
邻接矩阵
当边比较多的时候用矩阵表示
表示:二维数组,十字链表
有向:不一定对称,无向:一定对称
顶点向量:将所有顶点按顺序存到数组中
先看定义
typedef struct ArcNode {//定义边的结构体
AdjType adjvex; //对于无权图,用1或0表示是否相邻;对带权图,则为权值类型
OtherInfo info;
} ArcNode;
//这个类型是在教材基础上增加的,其目的是为了保持与其他图的类型一致性
typedef struct VertexNode {//定义顶点的结构体
VertexData data; //顶点数据,一般为字符标签
} VertexNode;
//定义邻接矩阵表示的图类型
typedef struct Graph {
ArcNode arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵的二维数组
VertexNode vertex[MAX_VERTEX_NUM]; //创建顶点向量
int vexnum, arcnum; //图的顶点数和弧数
GraphKind kind; //图的种类标志
} AdjMatrix, Graph;
data存储如下
1,6,11
ABCDEF
A,B,50
A,E,45
A,C,10
B,E,10
B,C,15
C,A,20
C,D,15
D,B,20
D,E,35
E,D,30
F,D,3
v0 v1 v2 v3 v4 v5
A
void CreateGraph(Graph *G) {
int i, j, k, weight; // 用于循环和临时存储数据的变量
int kind; // 存储图的类型
VertexData v1, v2; // 用于存储单条边的起始和结束顶点
// 读取图的类型、顶点数和边数
scanf("%d,%d,%d", &kind, &G->vexnum, &G->arcnum);
getchar(); // 吸收换行符,准备读取下一个数据
G->kind = kind; // 存储图的类型
// 初始化所有边的权重为无穷大
for (i = 0; i < G->vexnum; i++)
for (j = 0; j < G->vexnum; j++)
G->arcs[i][j].adjvex = INFINITY;
// 读入每个顶点的标识符
for (i = 0; i < G->vexnum; i++) {
scanf("%c", &G->vertex[i].data);//存入顶点数组
}
getchar(); // 吸收换行符
// 读入每条边的起始顶点、结束顶点及权重,并建立相应的弧
for (k = 0; k < G->arcnum; k++) {
scanf("%c,%c,%d", &v1, &v2, &weight); // 输入一条弧的两个顶点及权值
getchar(); // 吸收换行符
i = LocateVertex(G, v1); // 查找起始顶点的索引
j = LocateVertex(G, v2); // 查找结束顶点的索引
G->arcs[i][j].adjvex = weight; // 建立弧
if (G->kind > DN) // 如果图是有向加权图,则同时建立反向弧
G->arcs[j][i].adjvex = weight;
}
}
通过邻接矩阵算入度和出度
入度:这一行的非零总和
出度:这一列的非零总和
通过入度和出度判断欧拉回路:图的出度总和和入度总和做差
邻接表
当定点比较多的时候使用邻接表
链表存储结点的邻接点——头插法(因为头插法是最快的)但是相比逆序
邻接表和邻接向量顶点多
void CreateGraph(Graph *G) {
// 用于循环遍历和临时存储的变量
int i, j, k;
int kind, w; // kind表示图的类型,w表示边的权重
ArcNode *arc; // 指向边结点的指针
VertexData v1, v2; // 用于存储顶点信息
// 读取图的类型、顶点个数和边数
scanf("%d,%d,%d", &kind, &G->vexnum, &G->arcnum);
getchar(); // 吸收换行符
G->kind = kind; // 存储图的类型
// 初始化顶点信息,将边表的第一邻接点置为空
for (i = 0; i < G->vexnum; ++i) {
scanf("%c", &G->vertex[i].data);
G->vertex[i].firstarc = NULL;
}
getchar(); // 吸收换行符
// 输入并创建边的信息
for (k = 0; k < G->arcnum; ++k) {
scanf("%c,%c,%d", &v1, &v2, &w);
getchar(); // 吸收换行符
// 根据顶点信息查找其在顶点数组中的位置
i = LocateVertex(G, v1);
j = LocateVertex(G, v2);
// 创建新的边结点
arc = (ArcNode *)malloc(sizeof(ArcNode));
arc->adjvex = j; // 存储邻接顶点的索引
arc->nextarc = G->vertex[i].firstarc; // 将新边插入到边表的头部
arc->info.weight = w; // 存储边的权重信息
G->vertex[i].firstarc = arc; // 更新顶点的边表头指针
}
}
3.深度优先搜索:
找第一个邻接点:在一行中从左到右找第一个不为0的
相对于上一点的第一个邻接点:基于此位置向右搜索的下一个
void SearchRecur(Graph *G, int v0, CALLBACK visit) {//v0为搜索起始顶点的索引
visit(G->vertex[v0].data);
visited[v0] = true;//设为访问过
Arc w = FirstAdjVertex(G, v0);//用弧的结构体获取邻接顶点
while(w.adj != -1){
if (!visited[w.adj]) {
SearchRecur(G, w.adj, visit);
}
w = NextAdjVertex(G, v0, w);
}
}
3.1.循环+自定义栈消除递归
理解
void SearchStack(Graph *G, int v0, CALLBACK visit) {
Stack s, *S = &s;
Arc w;
int v; //v表示正在处理的顶点的索引
InitStack(&S);
Push(S, v0); //开始的时候要先把v0入栈
while (!IsEmpty(S)) //然后形成闭环
{
Pop(S, &v);
if (!visited[v])
{
visit(G->vertex[v].data);
visited[v] = true;
}
w = FirstAdjVertex(G, v);//用w接收v的第一个邻接点
while (w.adj != -1) //循环遍历邻接点
{
if (!visited[w.adj])
{
Push(S, w.adj);//若没有访问过则入栈
break; //!!!这条语句对于adjmatrix和adjlist有不同的效果
}
w = NextAdjVertex(G, v, w);//更新w为下一个邻接点
}
}
ClearStack(S);
}
递归:先找到第一个邻接点,往下走
如果没有break语句跳出while,则会先在while中遍历邻接点,沿着最后一个邻接点往下走,调用次数为1次
加上break后,在跳出while后会沿着第一个邻接点向下遍历,算法调用次数为3次,正好A有三个邻接点
4.广度优先
void SearchQueue(Graph *G, int v0, CALLBACK visit) {
int v, j; // 用于遍历元素
Arc w;
Queue q, *Q = &q;
visit(G->vertex[v0].data);
visited[v0] = true; // 设置该顶点i已被访问
InitQueue(Q); // 初始化队列
EnterQueue(Q, v0);
while(!IsEmpty(Q))
{
DeleteQueue(Q, &v);//&v获取队列第一个元素,用v接收
w=FirstAdjVertex(G, v);//找v的第一个邻接点,以边的方式记录,即用w接收
while (w.adj!=-1)//循环v通过w连的的邻接点
{
j=w.adj; //j接收w的邻接点预防错误
if(!visited[j])// 如果没有访问过,则依次入队
{
visit(G->vertex[j].data);
visited[j]=true;
EnterQueue(Q, j);
}
w=NextAdjVertex(G, v, w);
}
}
ClearQueue(Q);
}
邻接矩阵和邻接表在寻找first和next顶点时逻辑不同
4.1.在邻接表中寻找第一个邻接点或者下一个邻接点
Arc FirstAdjVertex(struct Graph *G, int v) {
Arc w = {-1, G->vertex[v].firstarc};
if (w.arc != NULL) w.adj = w.arc->adjvex;//若w有邻接边,w的邻接点就是结构体中的邻接点
return w;
}
Arc NextAdjVertex(struct Graph *G, int v, Arc w) {
Arc w2 = {-1, w.arc->nextarc};//w2是下一个邻接边
if (w2.arc != NULL) w2.adj = w2.arc->adjvex;
return w2;
}
其中Arc定义
typedef struct {
int adj; //顶点位序,邻接点在顶点数组中的位序
struct ArcNode *arc; //顶点为弧尾的弧,若为矩阵此成员无用
} Arc;
//存储邻接边的方法:必须使用结构体来记录边和所连顶点
5.简单路径算法
void path(Graph *G, int u, int v) {
int pre[G->vexnum];//前驱结点数组
for (int i = 0; i < G->vexnum; i++)
pre[i] = -1;
pre[u] = -2; //将pre[u]置为非-1,表示第u个顶点已被访问
if (DFS(G, u, v, pre)) //用深度优先搜索找一条从u到v的简单路径。
print_path(G, pre, v); //输出路径
else
printf("There's no path leads from %c to %c\n", G->vertex[u].data, G->vertex[v].data);
}
其中pre数组记录了一路的过程的向量,可以通过回溯找到路径
基于深度优先搜索的寻找简单路径
姑且将j成为当前结点,pre[j.adj]为当前节点对应张量位置的值
bool DFS(Graph *G, int u, int v, int *pre) {//从u到v简单路径
for (Arc j = FirstAdjVertex(G, u); j.adj >= 0; j = NextAdjVertex(G, u, j)) {
// j是u的第一个出度 j变为下一个出度
if (pre[j.adj] == -1) {//如果当前结点未被访问,j.adj为出度所连的结点
pre[j.adj] = u; //u是这一步的前驱结点
if (j.adj == v)
return true;
if (DFS(G, j.adj, v, pre))
return true;
pre[j.adj] = -1; //为了保持pre数组的“干净”,可以有这句。这句去掉不影响算法。
}
}
return false;
}
6.hamilton通路
#include <stdio.h>
#include <stdbool.h>
#include "auxf.h"
#include "graph.h"
bool visited[MAX_VERTEX_NUM];
int path[MAX_VERTEX_NUM];
int n = 0;
void DisplayPath(Graph *G) {
for (int j = 0; j < G->vexnum; ++j)
printf("%3c", G->vertex[path[j]].data);
putchar('\n');
}
bool DepthFirstSearch(Graph *G, int v0) {
visited[v0] = true;
path[n++] = v0;
if (n == G->vexnum) {
DisplayPath(G);
return true;
}
Arc w = FirstAdjVertex(G, v0);
while (w.adj != -1) {
if (!visited[w.adj] && DepthFirstSearch(G, w.adj))
return true;
w = NextAdjVertex(G, v0, w);
}
visited[v0] = false;
--n;
return false;
}
void ResetPath(Graph *G) {
n = 0;
for (int j = 0; j < G->vexnum; ++j) visited[j] = false;
}
void Hamilton(Graph *G) {
for (int i = 0; i < G->vexnum; ++i) {
ResetPath(G);
if (DepthFirstSearch(G, i)) return;
}
printf("Hamilton path doesn't exist.\n");
}
int main(int argc, char *argv[]) {
choose_graph_model("hamilton", argc >= 2, argv, NULL);
graph_figure();
Graph G;
CreateGraph(&G);
Hamilton(&G);
DestroyGraph(&G);
close_model();
return 0;
}
n个顶点的图,有n-1条边,多一条就会形成回路了
7.生成树,最小生成树
7.1.prim算法
画表,第一行为所有顶点,第二行为代价,第三行为起始顶点
7.2.kruskal算法
分成不同数组,先按边找最小边(这条边的两个点为一个数组),找最小边的方法:权值最小且定点属于不同该数组,之后同化为新的数组
8.DAG 有向无环图
-
拓扑排序:先发生的事件排在前面
-
入度为0的结点可以先做,做完后将邻接点的入度-1,入度减为零的时候入栈,
-
复习邻接矩阵和表求出度的方法
出度:矩阵->找第i列;表->数i后面所连的结点个数
(邻接表:用一个数组存储所有结点,数组每个元素后面按顺序连接起所有邻接点)
8.1.拓扑排序
//基于邻接表 栈
bool TopoSort(AdjList *G) {
Stack *S;
int indegree[MAX_VERTEX_NUM];//用数组记录每个结点的入度
int i, count, k;
ArcNode *p;
FindID(G, indegree);
InitStack(&S);
for (i = 0; i < G->vexnum; ++i)
if (indegree[i] == 0)
Push(S, i);//将初始入度为0的结点入栈
count = 0;
while (!IsEmpty(S)) {
Pop(S, &i);
printf("%s ", Vertex2Name(G->vertex[i].data));
++count; /*输出所弹出的i号顶点并计数*/
p = G->vertex[i].firstarc;//p是顶点i的边(在下面会更新),即i邻接点的入度
while (p != NULL) {
k = p->adjvex;//k是p的邻接点
--indegree[k]; /*i号顶点(p)的每个邻接点k的入度减1*/
if (indegree[k] == 0)
Push(S, k); /*若入度减为0,则入栈*/
p = p->nextarc;
}
} /*while*/
ClearStack(S);
return count < G->vexnum ? false : true; /*false意味着该有向图含有回路*/
}
AOV网:以顶点表示活动网络
AOE网:以边表示活动网络(有权图)
注:前一步最大权重的决定下一步开始时间(已做完的分支会等着)
有权图中计算event(结点)最早和最晚开始时间,activity(边)最早和最晚开始时间 看教材
最早开始时间和最晚开始时间不一样,有时间差
最早开始时间和最晚开始时间一样,没有时间差,此边称为关键路径中一条边
8.2.寻找关键路径
两个栈一个用于正向拓扑排序,一个用于存储从第一个栈弹出的结点,即是拓扑的逆序
基于邻接表
问题:为什么邻接点之间时间步可能不同
bool TopoOrder(AdjList *G, Stack *T) {
int count, i, j, k;
ArcNode *p;
int indegree[MAX_VERTEX_NUM]; /*各顶点入度数组*/
Stack *S;
InitStack(&T);
InitStack(&S); /*初始化栈T, S*/
FindID(G, indegree); /*求各个顶点的入度*///id作为数组对应每个顶点id
//将初始id=0的点入栈
for (i = 0; i < G->vexnum; ++i)
if (indegree[i] == 0)
Push(S, i);
//初始化count为0,最早发生时间数组为0
count = 0;
for (i = 0; i < G->vexnum; ++i)
ve[i] = 0; /*初始化最早发生时间*/
//弹出S中初始入度为0的点,并压入栈T,这样循环下去T中元素恰是拓扑排序的逆向顺序
while (!IsEmpty(S)) {
Pop(S, &j);
Push(T, j);
++count;
p = G->vertex[j].firstarc;
while (p != NULL) {
k = p->adjvex;//k为p邻接点
if (--indegree[k] == 0)
Push(S, k); /*若顶点的入度减为0,则入栈S*/
if (ve[j] + p->info.weight > ve[k])//ve最早被初始化为0,而在这一步可以将ve逐步累积
ve[k] = ve[j] + p->info.weight;
p = p->nextarc;
}
}
ClearStack(S);
return count >= G->vexnum;
}
bool CriticalPath(AdjList *G) {//计算最晚开始时间,对比最早最晚时间,判断是否包含于关键路径中
ArcNode *p;
int i, j, k, dut, ei, li;
char tag;
int vl[MAX_VERTEX_NUM]; /*每个顶点的最迟发生时间*/
Stack *T;
InitStack(&T);
if (!TopoOrder(G, T))
return false;
for (i = 0; i < G->vexnum; ++i)
vl[i] = ve[G->vexnum - 1]; /*初始化顶点事件的最迟发生时间*/
while (!IsEmpty(T)) { /*按逆拓扑顺序求各顶点的vl值*/
Pop(T, &j);
p = G->vertex[j].firstarc;
while (p != NULL) {
k = p->adjvex;
dut = p->info.weight;
if (vl[k] - dut < vl[j])
vl[j] = vl[k] - dut;
p = p->nextarc;
}
}
for (j = 0; j < G->vexnum; ++j) { /*求ei,li和关键活动*/
p = G->vertex[j].firstarc;//边
while (p != NULL) {
k = p->adjvex;//接点
dut = p->info.weight;
ei = ve[j];
li = vl[k] - dut;
tag = (ei == li) ? '*' : ' ';//如果包含于关键路径则加星号
printf("%s,%s,%d,%d,%d,%c\n", Vertex2Name(G->vertex[j].data), Vertex2Name(G->vertex[k].data), dut, ei, li, tag);
p = p->nextarc;
}
}
ClearStack(T);
return true;
}
路由router:终端信号发到最近路由器上,路由找下一个最近路由直至到达服务器
8.3.dijkstra算法和贪心寻找
每次找到距离前一个点(或说源顶点)最近的点,然后更新
void ShortestPath_DJS(Graph *G, int v0, WeightType dist[], VertexSet path[]) {
int i;
// 初始化dist[i]和path[i]
for (i = 0; i < G->vexnum; ++i) {
InitList2(&path[i]);//初始化path数组
dist[i] = G->arcs[v0][i].adjvex;//起始顶点v0到i的距离
if (dist[i] < INFINITY) {//如果可达,加入path数组,path的每个元素都是从v0到这的路径,可能是链表或顺序表
AddTail(&path[i], G->vertex[v0].data);
AddTail(&path[i], G->vertex[i].data);
}
}
VertexSet s, *S = &s; // s为已找到最短路径的终点集合
InitList2(S);
AddTail(S, G->vertex[v0].data);
int k, min;
for (int t = 1; t <= G->vexnum - 1; ++t) {
min = INFINITY;
for (k = -1, i = 0; i < G->vexnum; ++i)
if (!Member(G->vertex[i].data, S) && dist[i] < min) {//member用于监测i点是否已经在s中了
k = i;
min = dist[i];
}
if (k == -1) continue;
AddTail(S, G->vertex[k].data);
// 更新距离k更远的顶点的最短路径
for (i = 0; i < G->vexnum; ++i) /*修正dist[i], i∈V-S*/
// 如果顶点i尚未被处理,并且存在从k到i的边,且该路径更短,则更新dist[i]和path[i]
if (!Member(G->vertex[i].data, S) && G->arcs[k][i].adjvex != INFINITY && (dist[k] + G->arcs[k][i].adjvex < dist[i])) {
dist[i] = dist[k] + G->arcs[k][i].adjvex;
path[i] = path[k];
AddTail(&path[i], G->vertex[i].data); /* path[i]=path[k]∪{Vi} */
}
}
}
- 初始化:
- 使用循环遍历所有顶点(
i
),将dist[i]
设置为G->arcs[v0][i].adjvex
的值,表示从v0
到i
的初始路径长度。如果路径长度不是无穷大(INFINITY
),则在path[i]
列表中添加v0
和i
的顶点数据。 - 初始化一个名为
s
的顶点集合S
,用于存储已找到最短路径的顶点,并将起始顶点v0
添加到S
中。
- 使用循环遍历所有顶点(
- Dijkstra算法核心:
- 使用一个外层循环(
t
从1到G->vexnum - 1
),在每次迭代中找到当前未被包含在S
中的顶点中距离v0
最近的一个顶点,记为k
。 - 将找到的最近顶点
k
添加到S
中。 - 再次遍历所有顶点(
i
),检查未被包含在S
中的顶点。如果通过k
到达i
的路径比当前已知的最短路径更短,就更新dist[i]
的值,并更新path[i]
,将k
的路径与i
合并。
- 使用一个外层循环(
- 结束条件:
- 当所有顶点都已被包含在
S
中(即找到所有顶点的最短路径),或者没有未访问顶点时,算法结束。
- 当所有顶点都已被包含在
在函数执行完毕后,dist[]
数组包含了从起始顶点v0
到图中每个顶点的最短路径长度,而path[]
数组则包含了对应的最短路径的顶点顺序。这个算法适用于没有负权重边的图。
外层循环:遍历除了源顶点的每个顶点,
内层循环:在每次外层循环之间遍历图中每个顶点,查找未被包含在集合 S 中且距离源顶点最近的顶点。
8.4.弗洛伊德算法
void ShortestPath_Floyd(AdjMatrix *G,
WeightType dist[][MAX_VERTEX_NUM],
VertexSet path[][MAX_VERTEX_NUM]) {
int i, j, k;
for (i = 0; i < G->vexnum; ++i)
for (j = 0; j < G->vexnum; ++j) {
InitList2(&path[i][j]);
dist[i][j] = G->arcs[i][j].adjvex;
if (dist[i][j] < INFINITY) {
AddTail(&path[i][j], G->vertex[i].data);
AddTail(&path[i][j], G->vertex[j].data);
}
}
for (k = 0; k < G->vexnum; k++)
for (i = 0; i < G->vexnum; i++)
for (j = 0; j < G->vexnum; j++)
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
JoinList(&path[i][k], &path[k][j], &path[i][j]);
}//三重循环,不断找有没有中间结点k使距离更短
}
图的表示法(掌握理解)
图的搜索
最小生成树算法(理解)(克鲁斯卡尔算法和普利姆算法)
拓扑,关键路径(理解)
迪弗算法
嵌入式多接触linux
图的表示复习:
//邻接矩阵
typedef struct Graph {
ArcNode arcs[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; //邻接矩阵的二维数组
VertexNode vertex[MAX_VERTEX_NUM]; //创建顶点向量
int vexnum, arcnum; //图的顶点数和弧数
GraphKind kind; //图的种类标志
} AdjMatrix, Graph;
//邻接表
typedef struct {
int adj;
struct ArcNode *arc;
} Arc;
//邻接表中找到弧,找到j的第一个出度
p = G->vertex[j].firstarc;
p = p->nextarc
//邻接表中找到弧线相连的顶点
k = p->adjvex;