图的基本概念(多对多的逻辑结构)
- 假设ABCDEFG是7个电话,之间的连线表示修有通信线路
- 电话就是图的顶点 vi∈V,通信线路是边 ei∈E,G = {V,E}就是一个图
- 只要两个电话间有线路,就可以相互通话 => 无向图
- 电话(顶点)连接的线路(边)数量:度
- ABCDE和GF之间消息无法传递:不连通
- ABCDE和GF是两个连通分量
- 假设ABCDE是五个电话,之间的连线表示修有通信线路,数字表示该线路的电话费
- 不同通信线路上的电话费不同:加权图
- 假设ABCDE是五个城市,带箭头连线表示该方向上有航班运营、
- 例如航班A→D只能支持A飞往D,边是单向的 => 有向图
- 飞来某地的航班数量:入度
- 从某地起飞的航班数量: 出度
- 图并非只能表示地理数据,只要数据元素间满足多对多关系即可
- 可表示几个学生之间的朋友关系 => 无向无权图
- 可表示社交媒体的关注 / 被关注关系 => 有向无权图
- 可表示多个耗时不同的任务之间的依赖关系 => 有向加权图
- …
Key 1:在一个无向图中,所有顶点的度数之和为边数数量的2倍
Key 2:在一个有向图中,所有顶点的出度之和 == 所有顶点的入度之和
图的存储结构:邻接矩阵和邻接表
邻接矩阵
- 设|V| = n,图可以用一个n * n的方阵表示
- 即一个二维数组AdjMat[n][n] => Adjacent Matrix
- AdjMat[i][j]表示vi和vj的连接情况
无向无权图:
- AdjMat[i][j]为1表示有边相连,为0表示无边
- AdjMat是对称的
有向加权图:
- AdjMat[i][j]表示vi → vj的权重
- AdjMat[i][j]为+∞表示不通
邻接表
- 每个顶点用一个链表存下自己的邻居
- |V| = n,有n个链表,即图可用一个链表的数组AdjList[n]存储
- AdjList[i]表示顶点vi的链表(头)
- 从AdjList[i]开始可以遍历所有以vi的邻居
邻接矩阵和邻接表的比较
-
设G = {V,E},|V| = n.
-
邻接矩阵无论如何都需要一个二维数组[n][n],而邻接表中每条链表长度取决于它有多少邻居
-
邻接矩阵访问AdjMat[i][j]是O(1)的,但邻接表访问特定边需要顺着起点的链表向后查找
-
邻接表的优点:
- 在边较少 (邻居少即边较少) 时节省许多空间 => 适用于稀疏图
-
邻接表的缺点:
- 无法直接获得某条边信息,需要vi链表进行从头顺序存取,最坏情况下O(n)
图DFS和BFS遍历
DFS(Depth First Search):深度优先遍历
- 遇到新的邻居就进去…直到没有可以进的邻居了再返回
- 优先进入后来遇到的邻居 => 递归 / 栈
DFS代码实现
#include <stdio.h>
#include <stdlib.h>
typedef struct EdgeNode //边表结点
{
int adjvex; //存储顶点对应的下标 存储的是一个位置,而非具体元素,为了以后改变数据方便操作
struct EdgeNode *next; //链域指向下一个邻接点
int weight; //权值(问题中有权值再用)
} EdgeNode;
typedef struct VertexNode //顶点表结点
{
char data; //存放顶点信息
EdgeNode *firstedge; //指向边表中第一个结点
} VertexNode;
typedef struct
{
VertexNode adjlist[20];
int n, e;
} GraphAdjlist; //声明图的邻接表类型
int visited[10]; //访问标志数组(访问过赋值为1,反之为0)
void create(GraphAdjlist *G) //创建邻接表
{
int i, j, k;
EdgeNode *e;
printf("ding dian shu he bian shu:");
scanf("%d %d", &G->n, &G->e);
getchar(); //清除缓冲
printf("diang dian bian hao:\n");
for (i = 0; i < G->n; i++)
{
scanf("%c", &G->adjlist[i].data); //输入顶点编号
G->adjlist[i].firstedge = NULL; //将边表置空
getchar();
}
for (k = 0; k < G->e; k++)
{
printf("shu ru bian(vi, vj)shang de ding dian xu hao:\n");
scanf("%d %d", &i, &j); //头插法方便,快速 如果用尾插法需要指针遍历到尾部,太慢
/*使用头插法加入边表结点*/
e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = j;
e->next = G->adjlist[i].firstedge;
G->adjlist[i].firstedge = e;
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //因为是无向图,一条边对应两个顶点
e->adjvex = i;
e->next = G->adjlist[j].firstedge;
G->adjlist[j].firstedge = e;
}
printf("\n");
}
void DFS(GraphAdjlist *G, int i)
{
EdgeNode *p;
visited[i] = 1;
printf("%c ", G->adjlist[i].data);
p = G->adjlist[i].firstedge;
while (p != NULL)
{
if (visited[p->adjvex] == 0)
{
DFS(G, p->adjvex);
}
p = p->next;
}
}
void DFSTraverse(GraphAdjlist *G)
{
int i;
for (i = 0; i < G->n; i++)
{
visited[i] = 0;
}
for (i = 0; i < G->n; i++)
{
if (visited[i] == 0)
{
DFS(G, i);
}
}
}
int main() //A B C D E
{
GraphAdjlist G;
create(&G);
printf("DFS:");
DFSTraverse(&G);
return 0;
}
BFS(Breath First Search):广度优先遍历
- 先把当前结点的邻居都遍历完,再按先来后到遍历邻居的邻居们,逐层向外扩张
- 优先进入先访问的邻居的邻居 => 队列
BFS代码实现
#include <stdio.h>
#include <stdlib.h>
#define max 20
typedef struct EdgeNode //边表结点
{
int adjvex; //存储顶点对应的下标 存储的是一个位置,而非具体元素,为了以后改变数据方便操作
struct EdgeNode *next; //链域指向下一个邻接点
int weight; //权值(问题中有权值再用)
} EdgeNode;
typedef struct VertexNode //顶点表结点
{
char data; //存放顶点信息
EdgeNode *firstedge; //指向边表中第一个结点
} VertexNode;
typedef struct
{
VertexNode adjlist[max];
int n, e;
} GraphAdjlist; //声明图的邻接表类型
int visited[max]; //访问标志数组(访问过赋值为1,反之为0)
void create(GraphAdjlist *G) //创建邻接表
{
int i, j, k;
EdgeNode *e;
printf("ding dian shu he bian shu:");
scanf("%d %d", &G->n, &G->e);
getchar(); //清除缓冲
printf("diang dian bian hao:\n");
for (i = 0; i < G->n; i++)
{
scanf("%c", &G->adjlist[i].data); //输入顶点编号
G->adjlist[i].firstedge = NULL; //将边表置空
getchar();
}
for (k = 0; k < G->e; k++)
{
printf("shu ru bian(vi, vj)shang de ding dian xu hao:\n");
scanf("%d %d", &i, &j); //头插法方便,快速 如果用尾插法需要指针遍历到尾部,太慢
/*使用头插法加入边表结点*/
e = (EdgeNode *)malloc(sizeof(EdgeNode));
e->adjvex = j;
e->next = G->adjlist[i].firstedge;
G->adjlist[i].firstedge = e;
e = (EdgeNode *)malloc(sizeof(EdgeNode)); //因为是无向图,一条边对应两个顶点
e->adjvex = i;
e->next = G->adjlist[j].firstedge;
G->adjlist[j].firstedge = e;
}
printf("\n");
}
void BFS(GraphAdjlist *G, int v)
{
EdgeNode *p;
int queue[max], front = 0, rear = 0; //定义循环队列并初始化
int w, i;
for (i = 0; i < G->n; i++) //标志数组初始化
visited[i] = 0;
printf("%2c", G->adjlist[v].data);
visited[v] = 1;
rear = (rear + 1) % max;
queue[rear] = v;
while (front != rear)
{
front = (front + 1) % max;
w = queue[front];
p = G->adjlist[w].firstedge;
while (p != NULL)
{
if (visited[p->adjvex] == 0)
{
printf("%2c", G->adjlist[p->adjvex].data);
visited[p->adjvex] = 1;
rear = (rear + 1) % max;
queue[rear] = p->adjvex;
}
p = p->next;
}
}
printf("\n");
}
int main()
{
GraphAdjlist G;
create(&G);
printf("BFS:");
BFS(&G, 0);
return 0;
}
Key 1:DFS每步操作:进入当前结点的下一个未访问邻居,如无则返回
Key 2:BFS每步操作:进入当前队首结点并让其出队,将其未访问邻居入队
最小生成树(Prim算法)
- 对于含n个结点的一个无向连通图,其边数最多为 n(n-1)/2 条,最少为n - 1条
- 保持连通性的情况下,选 n - 1条边出来,剔除其他边,它就变成了一棵树
- 生成树里没有环
- n结点 + n - 1条边 + 连通 => 生成树
最小生成树 MST(Minimum Spanning Tree)
- 在加权图中选出n - 1条边来构成其生成树,且这些边的权值之和最小
- MST不唯一
- 如果结点是电话,权值是线路造价
- MST的意义是什么?
求最小生成树:Prim算法 “加点法”
- 在加权图中选出n - 1条边来构成一颗生成树,且它们权值之和最小,用什么策略选?
- 每次在连接已完成结点和未完成结点的边中,选一条权值最小的,重复n - 1遍
- 算法利用了贪心思想:选择局部最优
Key 1:Prim算法是 加点法,逐步增加n - 1个点来形成MST
Key 2:Prim算法每次加点满足 1)这个点所属边的权值最小 2)加点不会形成环
迪杰斯特拉算法
单源点最短路径:Dijkstra算法
- 加权图中求从一个顶点s出发到图上其他各点的最短距离
- |V| = n,算法循环n - 1次,每次循环中:
- 找到未完成结点中,s -> t距离最短的t,将t标注为已完成
- 以t为中转更新s至t的邻居们的距离
AOV图求拓扑排序
-
假设以下有向图中的顶点表示不同课程,边表示课程之间的依赖关系,请问能否顺利毕业?
-
能,因为顺着箭头走,不会产生环 => 有向无环图(DAG)
-
一个可行的选课顺序:1,2,5,4,3,6,7,8,9
-
上面这种网络就叫做AOV网络:
- Activity on vertices
- 图中的结点表示活动
-
如何求拓扑排序?
-
简而言之,把当前能上 (没有前序依赖, 入度为0) 的课上了,然后将此与之有关的依赖关系删掉
-
如果当前还有剩余未上的课,然而没有能上的课了 => 无法拓扑排序 => 有向图存在环
步骤:
最后得到一个可行的选课顺序:1,2,5,4,3,6,7,8,9
AOE网络求解关键路径
AOE网络、关键活动、关键路径
- 设计师、架构师,前端、后端和测试五人共同开发一个网站:
- 设计师设计好前端才能开工,架构师搭好框架后端才能开工,前后端都完成了测试才能开工
- 设计师需要三天,架构师需要五天,前端需要四天,后端需要八天,测试需要两天
- 至少需要多少天?哪些人比较关键?
- 至少需要15天
- 架构师、后端和测试很关键,如果他们拖延了,整个项目必定延期,他们 必须在前序工作完成后立即开始
- 设计师和前端就算拖延了也不一定导致项目延期,可以先摸摸鱼再开工
- 因此架构师、后端和测试就称为关键活动s -> 2 -> 3 -> e就称为关键路径
- AOE网络:
- Activity on edges
- 边表示活动
- 结点表示状态
- 有一个起点和一个终点
- 考试会给一个AOE网络,求关键活动和关键路径
- 方法:求每个结点的 最早 / 最晚发生时间 vE和vL,以及每个活动的 最早 / 最晚开始时间 eE和eL
- 按照拓扑排序,求出所有结点的vE
- 令终点的vL = vE,按照拓扑排序的逆序,求出所有结点的vL
- 所有活动(边)的eE等于其起点的vE (一个活动最早在起点状态达成后即可开始)
- 所有活动(边)的eL等于其终点的vL - wi(也可以拖到最后一刻再开始)