考纲内容
- 图的基本概念
- 图的存储及基本操作
- 邻接矩阵法
- 邻接表法
- 邻接多重表法
- 十字链表法
- 图的遍历
- 深度优先搜索(重点)
- 广度优先搜索(重点)
- 图的应用
- 最小(代价)生成树
- 最短路径
- 拓扑排序
- 关键路径
1. 基本概念
- 图:图G由顶点集V和边集E组成,记为G=(V, E),其中V(G)表示图G中顶点的有限非空集;E(G)表示图G中顶点的边集合。若V={ v 1 v_1 v1, v 2 v_2 v2,···, v n v_n vn},则用|V|表示G中顶点的个数,也称图G的阶,|E|表示G中边的条数
线性表和树可以为空,但图至少要有一个顶点
-
有向图
- E是有向边(弧)的有限集合,记为<v, w>(从顶点v到w的弧),v为弧尾,w为弧头
- 0 ≤ \le ≤|E| ≤ \le ≤n(n-1)(最多: 2 C n 2 2C_{n}^{2} 2Cn2)
-
无向图
- E是无向边的有限集合,记为(v, w)或(w, v)
- 0 ≤ \le ≤|E| ≤ n ( n − 1 ) 2 \le\frac{n(n-1)}2 ≤2n(n−1)(最多: C n 2 C_{n}^{2} Cn2)
-
简单图
- 不存在重复边
- 不存在顶点到自身的边
-
多重图
- 某两个结点之间的边数多于一条,且允许顶点通过同一条边和自己关联
-
完全图(简单完全图)
- 无向图,任意两个顶点之间都存在一条边
- 有向图,任意两个顶点之间都存在两条方向相反的边
-
子图
- 设有两个图 G=(V, E)和G’(V’, E’),若V’是V的子集,E’是E的子集,则G’是G的子图
- 若V(G’) = V(G),则称其为G的生成子图
-
连通、连通图和连通分量
- 连通:两顶点之间有路径存在
- 连通图:图G中任意两个顶点都是连通的
- 连通图:最少有n-1条边
- 非连通图:最多有 C n − 1 2 C_{n-1}^{2} Cn−12条边
- 连通分量:无向图中的极大连通子图
- 极小连通子图:既要保持图连通又要使得边数最少的子图
-
强连通图、强连通分量
- 强连通:有向图中,顶点v到w和顶点w到v都有路径
- 强连通图:任何一对顶点都是强连通的
- 强连通分量:有向图中的极大强连通子图
-
生成树、生成森林
- 生成树(连通图):包含图中全部顶点的一个极小连通子图(边尽可能少,但要保持连通)
- 若图中顶点数为n,则它的生成树含有n-1条边
- 若砍去一条边,则会变成非连通图;若加上一条边,则会形成一个回路
- 生成森林(非连通图):连通分量的生成树构成
- 生成树(连通图):包含图中全部顶点的一个极小连通子图(边尽可能少,但要保持连通)
-
顶点的度、入度和出度
- 无向图顶点的度:以该顶点引出的边的数目
- 无向图的全部顶点的度的和等于边数的2倍
- 度为奇数的顶点个数之和必为偶数
- 有向图没有度的概念,只有入度和出度
- 入度:以顶点v为终点的有向边的数目
- 出度:以顶点v为起点的有向边的数目
- 有向图的全部顶点的入度之和 = 出度之和 = 边数
- 无向图顶点的度:以该顶点引出的边的数目
-
边的权和网
- 权:边上标上具有某种含义的数值
- 网:边上带有权值的图
-
稠密图和稀疏图
- 稠密图:边数很多的图(|E| > |V|·log |V|)
- 稀疏图:边数很少的图(|E| < |V|·log |V|)
-
路径、路径长度和回路
- 路径:顶点 v p v_p vp到顶点 v q v_q vq的一条路径是指顶点序列 v p v_p vp, v i 1 v_i1 vi1, v i 2 v_i2 vi2,···, v i m v_im vim, v p v_p vp
- 路径长度:路径上边的数目
- 回路:第一个顶点和对后一个顶点相同的路径
-
简单路径、简单回路
- 简单路径:顶点不重复出现的路径
- 简单回路:除第一个和最后一个顶点外,其余顶点不重复出现的回路
-
距离
- 从顶点u出发到顶点v的最短路径长度,不存在则记为无穷
-
有向树
- 一个顶点的入度为0,其余顶点的入度均为1的有向图
2. 图的存储及基本操作
1. 邻接矩阵
-
定义
- 邻接矩阵:用一个一维数组存储图中顶点的信息,用一个二维数组存储图中边的信息(即各顶点之间的邻接关系),存储顶点之间邻接关系的二维数组
- 图
- 带权图(可用int的上限值表示无穷)
-
结构定义
#define MaxVertexNum 100 //顶点数目的最大值 typedef struct { char Vex[MaxVertexNum]; //顶点表 int Edge[MaxVertexNum][MaxVertexNum]; //邻接矩阵,边表 int vexnum, arcnum; //图的当前顶点数和边数/弧数 }MGraph;
-
特点
- A n A^n An的元素 A n A^n An[i][j] = 顶点i到顶点j的长度为n的路径的数目
- 无向图
- 第i个结点的度 = 第i行或列的非零元素个数
- 有向图
- 第i个结点的出度 = 第i行的非零元素个数
- 第i个结点的入度 = 第i列的非零元素个数
- 第i个结点的度 = 第i行、第i列的非零元素个数之和
-
性能分析
- 空间复杂度:O( ∣ V ∣ 2 |V|^2 ∣V∣2)(只和顶点数相关,和实际的边数无关)
- 求顶点的度时间复杂度:O( ∣ V ∣ 2 |V|^2 ∣V∣2)
- 适于存储稠密图
- 无向图的邻接矩阵是对称矩阵,可用压缩存储(只存储上三角区/下三角区)
2. 邻接表
- 定义
- 对图G中的每个顶点 v i v_i vi建立一个单链表,第i个单链表中的结点表示依附于顶点 v i v_i vi的边(对于有向图则是以顶点 v i v_i vi为尾的弧),这个单链表就称为顶点 v i v_i vi的边表
- 边表的头指针和顶点的数据信息采用顺序存储,称为顶点表
- 结构定义
#define MaxVertexNum 100 typedef struct //邻接表存储的图 { VNode vertices[MaxVertexNum]; int vexnum, arcnum; }ALGraph; typedef struct ArcNode //顶点 { int adjvex; //边/弧指向哪个结点 struct ArcNode *next; //指向下一条弧的指针 //InfoType info; //边权值 }ArcNode; typedef struct VNode //边/弧 { VertexType data; //顶点信息 ArcNode *first; //第一条边/弧 }VNode;
3. 十字链表(仅存储有向图)
- 找出边:顺着绿色找
- 找入边:顺着橙色找
4. 邻接多重表(仅存储无向图)
3. 图的遍历
1. 定义
- 从图中的某一顶点出发,按照某种搜索方法沿着图中的边对图中的所有顶点访问一次且仅访问一次
2. 广度优先搜索(BFS)
- 基本思想(需要借助一个辅助队列)
- 访问起始顶点v,接着由v出发,依次访问v的各个未访问过的邻接顶点w1, w2, ···
- 依次访问w1, w2, ···的所有未被访问过的邻接顶点
- 再从这些顶点出发,访问它们所有未被访问过的邻接结点,直至图中所有顶点都被访问过为止
- 若图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作为始点,重复直至所有顶点被访问
- 算法实现(类比树的层次遍历)
void BFS(AGraph *G, int v, int visit[maxSize]) { ArcNode *p; int que[masSize], front = 0, rear = 0; int j; Visit(v); //任意访问顶点v的函数 visit[v] = 1; rear = (rear+1) % maxSize; //当前结点v进队 que[rear] = v; while(front != rear) //队空的时候说明遍历完成 { front = (front+1) % maxSize; //顶点出队 j = que[front]; p = G->adjlist[j].firstarc; //p指向出队顶点j的第一条边 while(p != NULL) { if(visit[p->adjvex] == 0) //当前邻接顶点未被访问,则进队 { Visit(p->adjvex); visit[p->adjvex] = 1; rear = (rear+1) % maxSize; //该顶点进队 que[rear] = p->adjvex; } p = p->nextarc; //p指向j的下一条边 } } } void BFSTraverse(AGraph *g) { int i; for(i = 0; i < g->vexnum; ++i) //初始化访问标记数组 visit[i] = 0; for(i = 0; i < g->vexnum; ++i) //对每个连通分量用一次BFS if(visit[i] == 0) BFS(G, i, visit); }
- 复杂度
- 空间复杂度:O(|V|)
- 时间复杂度:访问结点的时间+访问所有边的时间
- 邻接矩阵:O( ∣ V ∣ 2 |V|^2 ∣V∣2)
- 邻接表:O(|V|+|E|)
- 广度优先生成树
- 由广度优先遍历确定的树
- 邻接表存储的图表示方式不唯一,遍历序列、生成树也不唯一
- 遍历非连通图可得广度优先森林
3. 深度优先搜索(DFS)
- 算法思想
- 访问图中某一起始顶点v,然后由v出发,访问与v邻接且未被访问的任一顶点w1
- 再访问与w1邻接且未被访问的任一顶点w2···重复上述过程
- 当不能再继续向下访问时,依次退回到最近被访问的顶点,若它还有邻接顶点未被访问过,则从该结点开始继续上述搜索过程,知道图中所有顶点均被访问过为止
- 算法实现(类比树的先根遍历)
int visit[maxSize]; void DFS(AGraph *G, int v) { ArcNode *p; visit[v] = 1; //置已访问标记 Visit(v); //函数Visit()代表访问顶点v p = G->adjlist[v].firstarc; //p指向顶点v的第一条边 while(p != NULL) { if(visit[p->adjvex] == 0) //若顶点未访问,则递归访问 DFS(G, p->adjvex); p = p->nextarc; //p指向顶点v的下一条边的终点 } } void DFSTraverse(AGraph *g) { int i; for(i = 0; i < g->vexnum; ++i) //初始化访问标记数组 visit[i] = 0; for(i = 0; i < g->vexnum; ++i) if(visit[i] == 0) DFS(G, i); }
- 复杂度
- 空间复杂度:最坏:O(|V|);最好:0(1)(来自函数调用栈)
- 时间复杂度:访问结点的时间+访问所有边的时间
- 邻接矩阵:O( ∣ V ∣ 2 |V|^2 ∣V∣2)
- 邻接表:O(|V|+|E|)
- 深度优先生成树
- 由深度优先遍历确定的树
- 邻接表存储的图表示方式不唯一,遍历序列、生成树也不唯一
- 遍历非连通图可得深度优先森林
无向图:DFS/BFS函数调用次数 = 连通分量数
有向图:若从起始路径到其他顶点都有路径,则只需调用一次DFS/BFS函数;对于强连通图,从任一顶点出发都只需调用一次DFS/BFS函数
4. 算法应用
1. 试设计一个算法,判断一个无向图G是否为一棵树,若是一棵树,则返回true,否则返回false
- 算法思想:一个无向图G是一棵树的条件是:G必须是无回路的连通图或有n-1条边的连通图。可采用深度优先搜索一次遍历就能访问到n个顶点和n-1条边,则可判定此图是一棵树
- 算法实现
bool isTree(AGraph *G) { for(int i = 1; i <= G.vexnum; ++i) visit[i] = 0; int Vnum = 0, Enum = 0; //记录顶点数和边数 DFS(G, 1, Vnum, Enum, visit); if(Vnum == G.vexnum && Enum == 2*(G.vexnum-1)) return true; //符合树的条件 else return false; //不符合树的条件 } void DFS(AGraph *&G, int v, int &Vnum, int &Enum, int visit[]) { //深度优先遍历图,统计访问过的顶点数和边数,通过Vnum和Enum返回 visit[v] = 1; //作访问标记,顶点计数 ++Vnum; ArcNode *p; p = G->adjlist[v].first arc; //p指向顶点v的第一条边 while(p != NULL) { ++Enum; //边存在,边计数 if(visit[p->adjvex] = 0) //当该邻接顶点未访问过 DFS(G, p->adjvex, Vnum, Enum, visit); p = p->nextarc; //p指向顶点v的下一条边的终点 } }
2. 写出图的DFS的非递归算法(图采用邻接表形式)
void DFS_NoRC(AGraph *G, int v, int visit[maxSize])
{ //连通图
ArcNode *p;
ArcNode *stack[maxSize]; //定义一个栈,用来存储访问结点
int top = -1;
visit[v] = 1; //访问初始结点
Visit[v];
stack[++top] = G->adjlist[v]; //初始结点入栈
while(top != -1) //当栈非空
{
p = stack[top--]; //弹出栈顶元素
for(p = G->adjlist[p->adjvex].firstarc; p != NULL; p = p->nextarc) //遍历p的相邻结点
{
if(visit[p->adjvex] == 0) //若有未访问
{
Visit[p->adjvex]; //访问
stack[++top] = p; //入栈
break; //退出循环,遍历下一条边
}
}
if(visit[p->adjvex] == 1) //若已访问,出栈
--top;
}
}
3. 分别采用基于深度优先遍历和广度优先遍历算法判别以邻接表方式存储的有向图中是否存在由顶点 v i v_i vi到顶点 v j v_j vj(i ≠ \neq =j)的路径。注意,算法中涉及的图的基本操作必须在此存储结构上实现
//深度优先遍历
int visit[maxSize] = {0}; //访问标记数组
int ExitPath_DFS(AGraph *G, int i, int j)
{
ArcNode *p;
if(i == j)
return 1;
else
{
visit[i] = 1; //置访问标记
for(p = G->adjlist[i].firstarc; p != NULL; p = p->nextarc)
if(!visit[p->adjlist] && ExitPath_DFS(G, p->adjvex, j)) //递归检测邻接点
return 1; //i与j之间有路径
}
return 0;
}
//广度优先遍历
int visit[maxSize] = {0}; //访问标记数组
int ExitPath_BFS(AGraph *G, int i, int j)
{
ArcNode *Q[maxSize];
int front = 0; rear = 0;
ArcNode *p;
Q[++rear] = G->adjlist[i];
while(front != rear)
{
p = Q[++front];
visit[p->adjvex] = 1;
for(p = G->adjlist[p->adjvex].firstarc; p != NULL; p = p->nextarc) //检查所有邻接点
{
if(p->adjvex == j) //若p->adjvex == j,则查找成功
return 1;
if(visit[p->adjvex] == 0) //否则,顶点p->adjvex入队
Q[++rear] = G->adjlist[p->adjvex];
}
}
return 0;
}
4. 假设图用邻接表表示,设计一个算法,输出从顶点 v i v_i vi到顶点 v j v_j vj的所有简单路径
void FindPath(AGraph *G, int u, int v, int path[], int d)
{
int w, i;
ArcNode *p;
d++; //表示路径长度,初始为-1
path[d] = u; //将当前顶点添加到路径中
visit[u] = 1;
if(u == v) //找到一条路径则输出
print(path[]);
p = G->adjlist[u].firstarc; //p指向u的第一个相邻点
while(p != NULL)
{
w = p->adjvex; //若顶点w未访问,则递归访问它
if(visit[w] == 0)
FindPath(G, w, v, path, d);
p = p->nextarc; //p指向u的下一个相邻结点
}
visit[u] = 0; //恢复环境,该顶点可重新使用
}
5. 求不带权无向连通图G中离顶点v最远的一个顶点(即到v的路径长度最长)
int BFS(AGraph *G, int v) //修改BFS算法,返回最后 一个结点
{
ArcNode *p;
int que[maxSize], front = 0, rear = 0;
int visit[maxSize]; //声明访问数组
int i, j;
for(i = 0; i < G->vexnum; ++i) //初始化数组
visit[i] = 0;
rear = (rear + 1) % maxSize;
que[rear] = v; //初始结点入队
visit[v] = 1; //访问数组置为1
while(front != rear) //当队列非空
{
front = (front + 1) % maxSize;
j = que[front]; //头结点出队
p = G->adjlist[j].firstarc; //指向结点的第一条邻边
while(p != NULL)
{
if(visit[p->adjvex] == 0); //当遇到未访问结点
{
visit[p->adjvex] = 1; //访问结点
rear = (rear + 1) % maxSize; //入队
que[rear] = p->adjvex;
}
p = p->nextarc;
}
}
return j; //队空时,j保存了遍历过程的最后一个顶点
}
4. 图的应用(重点)
1. 最小生成树
所有权值均不相等,或有相等的边,但是在构造最小生成树的过程中权值相等的边都被并入生成树的图,其最小生成树唯一
1. 概念
- 定义
- 带权图G的生成树中边的权值最小的生成树(MST)
- 性质
- 最小生成树不唯一
- 若G本身是一棵树时,则G的最小生成树是它本身
- 最小生成树的边的权值之和总是唯一的,且最小
- 边数 = 顶点数-1
- 设G=(V, E)是一个带权连通无向图,U是顶点集的一个非空子集。若==(u, v)是一条具有最小权值的边==,其中u ∈ \in ∈U, v ∈ \in ∈V-U,则必存在一棵包含边(u, v)的最小生成树
- 通用算法
generic_MST(G) { T = NULL; while T 未形成一棵生成树; do 找到一条最小代价边(u, v)并且加入T后不会产生回路 T = T 并 (u, v); }
2. Prim算法
- 算法思想
- 从某一个顶点构建生成树,每次将代价最小的新顶点加入生成树,直到所有顶点都加入为止
- 算法实现
void Prim(MGraph g, int v0, int &sum) { int lowcost[maxSize], vset[maxSize], v; int i, j, k, min; v = v0; for(i = 0; i < g.vexnum; ++i) { lowcost[i] = g.Edge[v0][i]; vset[i] = 0; //顶点集合,在树中置为1;否则置为0 } vset[v0] = 1; //将v0并入树中 sum = 0; for(i = 0; i < g.vexnum-1; ++i) { min = INF; //INF是一个已经定义的比图中所有边权值都大的常量 /*下面这个循环用于选出候选边中的最小者*/ for(j = 0; j < g.vexnum; ++j) if(vset[j] == 0 && lowcost[j] < min) //选出当前生成树到其余顶点最短边中的最短的一条 { min = lowcost[j]; k = j; } vset[k] = 1; v = k; //v = 选中的顶点 sum += min; //sum记录了最小生成树的权值 /*以刚并入的顶点v为媒介更新候选边*/ for(j = 0; j < g.vexnum; ++j) if(vset[j] == 0 && g.Edge[v][j] < lowcost[j]) lowcost[j] = g.Edge[v][j]; } }
- 性能分析
- 时间复杂度:O( ∣ V ∣ 2 |V|^2 ∣V∣2)(适用于求解稠密图的最小生成树)
2. Kruskal算法
-
算法思想
- 每次选择一条权值最小的边,使这条边的两头连通(原本连通的就不选),直到所有结点都连通
-
算法实现
#define maxSize 10 typedef struct { int va, vb; //a,b为一条边所连的两个顶点 int weight; //边的权值 }Road; Road road[maxSize]; int parent[maxSize]; //定义并查集数组 int getRoot(int x) //在并查集中查找根结点 { while(x != parent[x]) x = parent[x]; return x; } //快速排序 void QuickSort(Road road[], int low, int high){ if(low < high){ int pivotpos = Partition(road, low, high); QuickSort(road, low, pivotpos-1); QuickSort(road, pivotpos+1,high); } } //一趟划分 int Partition(Road road[], int low, int high){ Road pivot = road[low]; while(low < high){ while(low < high && road[high].weight >= pivot.weight) --high; road[low] = road[high]; while(low < high && road[low].weight <= pivot.weight) ++low; road[high] = road[low]; } road[low] = pivot; return low; } void Kruskal(MGraph g, int &sum, Road road[]) { int i, Vertex, Edge, a_root, b_root; Vertex = g.vexnum; Edge = g.arcnum; sum = 0; for(i = 0; i < Vertex; ++i) parent[i] = i; //初始化父结点数组,每个顶点自成一个集合 QuickSort(road, 0, Edge-1); //road数组按其权值从小到大排序 for(i = 0; i < Edge; ++i) { a_root = getRoot(road[i].va); //查找边i的一端顶点 b_root = getRoot(road[i].vb); //查找边i的另一端顶点 if(a_root != b_root) //若两顶点不在同一集合 { parent[a] = b_root; //将b加入根结点为a的集合 sum += road[i].weight; //求生成树的权值 } } }
-
性能分析
- 时间复杂度:O(log |E|)(适用于边稀疏而顶点较多的图)
2. 最短路径
1. 概念
- 带权路径长度:从一个顶点 v 0 v_0 v0到图中其余任意一个顶点 v i v_i vi的一条路径(可能不止一条)所经过边上的权值之和
- 最短路径:带权路径长度最短的那条路径
- 单源最短路径:某一顶点到其他各顶点的最短路径(Dijkstra算法)
- 求顶点间的最短路径(Floyd算法)
2. BFS
- 仅适于求无权图单源最短路径
- 算法实现
void BFS(AGraph G, int v, int d[], int path[]) { for(int i = 0; i < G.vexnum; ++i) { d[i] = INF; //初始化路径长度,INF表示无穷大 path[i] = -1; //最短路径从哪个顶点过来 } d[v] = 0; //d[i]表示从v到i结点的最短路径 visit[v] = 1; int front = rear = 0; ArcNode *Q[maxSize]; ArcNode *p; Q[++rear] = G.adjlist[v]; while(front != rear) { p = Q[++front]; for(p = G.adjlist[p->adjvex].firstarc; p != NULL; p = p->nextarc) if(!visit[p->adjvex]) { d[p->adjvex] = d[v]+1; //路径长度加1 path[p->adjvex] = v; //最短路径应从v到p->adjlvex visit[p->adjvex] = 1; Q[++rear] = G.adjlist[p->adjvex]; //顶点入队 } } }
3. Dijkstra算法(无向图)
- 辅助数组
- dist[]:记录从源点 v 0 v_0 v0到其他各顶点当前的最短路径长度。初态:若 v 0 v_0 v0到 v i v_i vi有弧,则dist[i]为弧上的权值;否则置为∞
- path[]:path[i]表示从源点到顶点i之间的最短路径的前驱结点。算法结束时,可根据其值追溯得到源点 v 0 v_0 v0到 v i v_i vi的最短路径
- 算法步骤(不适用于有负权值的带权图)
- 集合S初始为{0},dist[]的初始值为∞
- 从顶点集合V-S中选出 v j v_j vj,满足dist[j] = Min{dist[i] | v i ∈ v_i \in vi∈V-S}, v j v_j vj就是当前求得的一条从 v 0 v_0 v0出发的最短路径的终点,令S = S ⋃ \bigcup ⋃ {j}
- 修改从 v 0 v_0 v0出发到集合V-S上任一顶点 v k v_k vk可达的最短路径长度:若dist[j]+arcs[j][k] < dist[k],则更新dist[k] = dist[j]+arcs[j][k]
- 重复2,3操作共n-1次,直到所有顶点都包含在S中
- 算法实现
void Dijkstra(MGraph g, int v, int dist[], int path[]) { int set[maxSize]; int min, i, j, u; /*初始化开始*/ for(i = 0; i < g.vexnum; ++i) { dist[i] = g.Edge[v][i]; set[i] = 0; if(g.Edge[v][i] < INF) //INF表示无穷大 path[i] = v; else path[i] = -1; //路径不存在,置为-1 } set[v] = 1; path[v] = -1; /*初始化结束*/ /*核心代码开始*/ for(i = 0; i < g.vexnum-1; ++i) { min = INF; /*这个循环每次从剩余顶点中选出一个顶点,通往这个顶点的路径在通往所有剩余顶点的路径中是最短的*/ for(j = 0; j < g.vexnum; ++j) if(set[j] == 0 && dist[j] < min) { u = j; min = dist[j]; } set[u] = 1; //将选出的顶点并入最短路径中 /*这个循环以刚并入的顶点作为中间点,对所有通往剩余顶点的路径进行检测*/ for(j = 0; j < g.vexnum; ++j) { /*这个if语句判断顶点u的加入是否会出现通往j的更短路径,若出现则改变原来路径及其长度,否则什么都不做*/ if(set[j] == 0 && dist[u] + g.Edge[u][j] < dist[j]) { dist[j] = dist[u] + g.Edge[u][j]; path[j] = u; } } } /*核心代码结束*/ } /*函数结束时,dist[]数组中存放了v到其余各顶点的最短路径长度,path[]中存放v到其余各顶点的最短路径*/
- 性能分析
- 时间复杂度: O ( ∣ V ∣ 2 ) O(|V|^2) O(∣V∣2)
- 空间复杂度: O ( ∣ V ∣ ) O(|V|) O(∣V∣)
4. Floyd算法(无向图)
- 算法思想:递推产生一个n阶方阵序列 A ( − 1 ) A^{(-1)} A(−1), A ( 0 ) A^{(0)} A(0),···, A ( k ) A^{(k)} A(k),···, A ( n − 1 ) A^{(n-1)} A(n−1),其中 A ( k ) [ i ] [ j ] A^{(k)}[i][j] A(k)[i][j]表示从顶点 v i v_i vi到 v j v_j vj的路径长度,k表示绕行第k个顶点的运算步骤。初始时,对于任意两个顶点 v i v_i vi和 v j v_j vj,若它们之间存在边,则以此边上的权值作为它们之间的最短路径长度;若它们之间不存在有向边,则以∞作为它们之间的最短路径长度。以后逐步尝试在原路径中加入顶点k(k=0,1,···,n-1)作为中间顶点。若增加中间顶点后,得到的路径比原来的路径长度减少了,则以此新路径代替原路径
- 算法步骤(允许有负权值,不允许有负权值的边组成的回路)
- 初始化:方阵 A ( − 1 ) [ i ] [ j ] A^{(-1)}[i][j] A(−1)[i][j] = arcs[i][j](arcs[i][j]是图的邻接矩阵)
- 第一轮:将 v 0 v_0 v0作为中间顶点,对于所有顶点对{i, j},如果有 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]> A − 1 [ i ] 0 ] A^{-1}[i]0] A−1[i]0]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],则将 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]更新为 A − 1 [ i ] 0 ] A^{-1}[i]0] A−1[i]0]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],更新后的矩阵标记为 A 0 A^{0} A0
- 第二轮:将 v 1 v_1 v1作为中间顶点,继续检测全部顶点对{i, j},如果有 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]> A − 1 [ i ] 0 j ] A^{-1}[i]0j] A−1[i]0j]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],则将 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]更新为 A − 1 [ i ] 0 j ] A^{-1}[i]0j] A−1[i]0j]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],更新后的矩阵标记为 A 1 A^{1} A1
- 第三轮:将 v 2 v_2 v2作为中间顶点,继续检测全部顶点对{i, j},如果有 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]> A − 1 [ i ] 0 j ] A^{-1}[i]0j] A−1[i]0j]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],则将 A − 1 [ i ] [ j ] A^{-1}[i][j] A−1[i][j]更新为 A − 1 [ i ] 0 j ] A^{-1}[i]0j] A−1[i]0j]+ A − 1 [ 0 ] [ j ] A^{-1}[0][j] A−1[0][j],更新后的矩阵标记为 A 2 A^{2} A2
- 直到检查到第n轮
- 算法实现
void Floyd(MGraph *g, int Path[][maxsize], int A[][maxSize]) { //用INF表示两点之间不存在边 int i, j, k; for(i = 0; i < g->vexnum; ++i) for(j = 0; j < g->vexnum; ++j) { A[i][j] = g->edges[i][j]; Path[i][j] = -1; } /*核心代码*/ for(k = 0; k < g->vexnum; ++k) for(i = 0; i < g->vexnum; ++i) for(j = 0; j < g->vexnum; ++j) if(A[i][j] > A[i][k] + A[k][j]) { A[i][j] = A[i][k] + A[k][j]; Path[i][j] = k; } /*核心代码*/ }
- 性能分析
- 时间复杂度:O( ∣ V ∣ 3 |V|^3 ∣V∣3)
- 空间复杂度:O( ∣ V ∣ 2 |V|^2 ∣V∣2)
3. 有向无环图描述表达式
- 有向无环图:不存在环的有向图,称为DAG图
- 适合描述含有公共子式的表达式(可实现对子式的共享,节省存储空间)
- 使用规则
4. 拓扑排序
-
AOV网:用DAG图表示一个工程,其顶点表示活动,有向边< V i , V j V_i, V_j Vi,Vj>表示活动 V i V_i Vi必须先于活动 V j V_j Vj进行的这样一种关系(边无权值,仅表示顶点间的前后关系)
-
拓扑排序:由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序(不唯一)
- 每个顶点出现且只出现一次
- 若顶点A在序列中排在顶点B的前面,则图中不存在B到A的路径
-
排序步骤
- 从AOV网中选择一个没有前驱的顶点并输出
- 从网中删除该顶点和所有以它为起点的有向边
- 重复上述步骤,直到当前AOV网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环
-
算法实现
typedef struct { char data; int count; //count来统计顶点当前的入度 ArcNode *firstarc; }VNode; int TopSort(AGraph *G) { int i, j, n = 0; int stack[maxSize], top = -1; //定义并初始化栈 ArcNode *p; for(i = 0; i < G->vexnum; ++i) //将图中入度为0的顶点入栈 if(G->adjlist[i].count == 0) stack[++top] = i; /*关键步骤开始*/ while(top != -1) { i = stack[top--]; //顶点出栈 ++n; //计数器加1,统计当前顶点 cout<<i<<“ ”; //输出当前结点 p = G->adjlist[i].firstarc; /*将所有由此顶点引出的边所指向的顶点的入度都减少1,并将这个过程中入度变为0的顶点入栈*/ while(p != NULL) { j = p->adjvex; //j为p的后继顶点 --(G->adjlist[j].count); //顶点j的入度减1 if(G->adjlist[j].count == 0) stack[++top] = j; p = p->nextarc; } } /*关键步骤结束*/ if(n == G->vexnum) return 1; else return 0; }
-
性能分析
- 时间复杂度: O ( ∣ V ∣ + ∣ E ∣ ) O(|V|+|E|) O(∣V∣+∣E∣)
- 空间复杂度: O ( ∣ V ∣ ) O(|V|) O(∣V∣)
-
逆拓扑排序
- 从AOV网中选择一个没有后继(出度为0)的顶点并输出
- 从网中删除该顶点和所有以ta为终点的有向边
- 重复上述步骤,直到当前的AOV网为空
-
注意
- 入度为0的顶点,工程可以从这个顶点所代表的活动开始或继续
- 若各个顶点已排在一个线性有序的序列中,拓扑排序的结果是唯一的
- 若邻接矩阵为三角矩阵,则必存在拓扑序列;反之不一定成立
5. 关键路径
1. AOE网
- 定义:带权有向图中,顶点表示事件,有向边表示活动,边上的权值表示完成该活动的开销(边有权值)
- 性质
- 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
- 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生
- 概念
- 源点(开始顶点):入度为0且仅有一个。表示整个工程开始
- 汇点(结束顶点):出度为0且仅有一个 。表示整个工程结束
- 关键路径:源点到汇点的所有路径中具有最大路径长度的路径。其长度是完成整个工程的最短时间
- 关键活动:关键路径上的活动
2. 相关参数定义
1. 事件 v k v_k vk的最早发生时间 v e ( k ) ve(k) ve(k)
- 定义:从源点 v 1 v_1 v1到顶点 v k v_k vk的最长路径长度(决定了所有从 v k v_k vk开始的活动能够开工的最早时间)
- 计算方法
- v e ve ve(源点) = 0
- v e ( k ) ve(k) ve(k) = M a x Max Max{ v e ( j ) + W e i g h t ( v j , v k ) ve(j) + Weight(v_j, v_k) ve(j)+Weight(vj,vk)}, v k v_k vk为 v j v_j vj的任意后继, W e i g h t ( v i , v k ) Weight(v_i, v_k) Weight(vi,vk)表示 < v j , v k > <v_j, v_k> <vj,vk>上的权值
- 计算ve()值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算
- 初始时,令 v e [ 1 … n ] ve[1…n] ve[1…n] = 0
- 输入一个入度为0的顶点
v
j
v_j
vj时,计算它所有直接后继顶点
v
k
v_k
vk的最早发生时间,若
v
e
[
j
]
+
W
e
i
g
h
t
(
v
j
,
v
k
)
>
v
e
[
k
]
ve[j] + Weight(v_j, v_k) > ve[k]
ve[j]+Weight(vj,vk)>ve[k],则
v
e
[
k
]
=
v
e
[
j
]
+
W
e
i
g
h
t
(
v
j
,
v
k
)
ve[k] = ve[j] + Weight(v_j, v_k)
ve[k]=ve[j]+Weight(vj,vk),以此类推,直到输出全部顶点
2. 事件 v k v_k vk的最迟发生时间 v l ( k ) vl(k) vl(k)
- 定义:在不推迟整个工程完成的前提下,即保证它的后继事件 v j v_j vj在其最迟发生时间 v l ( j ) vl(j) vl(j)能够发生时,该事件最迟必须发生的时间
- 计算方法
- v l vl vl(汇点) = v e ve ve(汇点)
- v l ( k ) = M i n vl(k) = Min vl(k)=Min{ v l ( j ) − W e i g h t ( v k , v j ) vl(j) - Weight(v_k, v_j) vl(j)−Weight(vk,vj)}, v k v_k vk为 v j v_j vj的任意前驱
- 计算vl()值时,按从后往前的顺序进行,在上述拓扑排序中,增设一个栈以记录拓扑序列,拓扑排序结束后从栈定至栈底变为逆拓扑排序
- 初始时,令 v l [ 1 … n ] = v e [ n ] vl[1…n] = ve[n] vl[1…n]=ve[n]
- 栈顶顶点
v
j
v_j
vj出栈,计算其所有前驱顶点
v
k
v_k
vk的最迟发生时间,若
v
l
[
j
]
−
W
e
i
g
h
t
(
v
k
,
v
j
)
<
v
l
[
k
]
vl[j] - Weight(v_k, v_j) < vl[k]
vl[j]−Weight(vk,vj)<vl[k],则
v
l
[
k
]
=
v
l
[
j
]
−
W
e
i
g
h
t
(
v
k
,
v
j
)
vl[k] = vl[j] - Weight(v_k, v_j)
vl[k]=vl[j]−Weight(vk,vj),以此类推,直到输出全部栈中顶点
3. 活动 a i a_i ai的最早开始时间 e ( i ) e(i) e(i)
- 定义:该活动弧的起点所表示的事件的最早发生时间。若边
<
v
k
,
v
j
>
<v_k, v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有
e
(
i
)
=
v
e
(
k
)
e(i) = ve(k)
e(i)=ve(k)
4. 活动 a i a_i ai的最迟开始时间 l ( j ) l(j) l(j)
- 定义:活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。若边
<
v
k
,
v
j
>
<v_k, v_j>
<vk,vj>表示活动
a
i
a_i
ai,则有
l
(
i
)
=
v
l
(
j
)
−
W
e
i
g
h
t
(
v
k
,
v
j
)
l(i) = vl(j) - Weight(v_k, v_j)
l(i)=vl(j)−Weight(vk,vj)
5. 一个活动 a i a_i ai的最迟开始时间 l ( i ) l(i) l(i)和其最早开始时间 e ( i ) e(i) e(i)的差额 d ( i ) = l ( i ) − e ( i ) d(i) = l(i) - e(i) d(i)=l(i)−e(i)
- 定义:活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动 a i a_i ai可以拖延的时间(关键活动的时间余量为0)
3. 关键路径
- 算法步骤
- 求所有事件的最早发生时间ve()
- 求所有事件的最迟发生时间vl()
- 求所有活动的最早发生时间e()
- 求所有活动的最迟发生时间l()
- 求所有活动的时间余量d(),d[i]=0的活动就是关键活动,由关键活动可得关键路径
- 特点
- 若关键活动耗时增加,则整个工程的工期将增长
- 缩短关键活动的时间,可以缩短整个工程的工期;当缩短到一定程度时,关键活动可能变成非关键活动
- 可能有多条关键路径,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的活动才能达到缩短工期的目的