差点就脱更了啊,微臣嘴干玩死,忙碌的暑假,还有头痛的new house, 我这junk food 也是吃一大堆,please不要长胖啊。
图的应用 这一章内容也是很多啊,概念真是比牛毛还多。。。看了两遍才缓过来啊 fighting
文章目录
- @[toc]
- 图
- 图的定义和基本术语
- 图的抽象数据类型定义
- 图的存储结构
- 邻接矩阵
- 邻接表链式表示法
- 无向图:
- 有向图
- 邻接表链式的存储表示
- **算法思想:**
- 邻接表特点
- 邻接矩阵和邻接表的关系
- 十字链表和邻接多重表
- 十字链表
- 邻接多重表
- 图的遍历
- 定义
- 深度优先遍历(DFS)
- 算法实现
- 算法效率
- 广度优先遍历(BFS)
- 实现过程
- DFS/BFS算法比较
- 图的应用
- 生成树
- 最小生成树
- 构造最小生成树(Minium Spanning Tree) - MST 性质
- 普里姆(prim)算法
- 克鲁斯卡尔(Kruskal)算法
- 最短路径
- 定义
- Dijistra算法
- Floyd算法
- 有向无环图(DAG)
- 定义
- 应用解决方法
- AOV网 - 拓扑排序
- AOE网 - 关键路径
文章目录
- @[toc]
- 图
- 图的定义和基本术语
- 图的抽象数据类型定义
- 图的存储结构
- 邻接矩阵
- 邻接表链式表示法
- 无向图:
- 有向图
- 邻接表链式的存储表示
- **算法思想:**
- 邻接表特点
- 邻接矩阵和邻接表的关系
- 十字链表和邻接多重表
- 十字链表
- 邻接多重表
- 图的遍历
- 定义
- 深度优先遍历(DFS)
- 算法实现
- 算法效率
- 广度优先遍历(BFS)
- 实现过程
- DFS/BFS算法比较
- 图的应用
- 生成树
- 最小生成树
- 构造最小生成树(Minium Spanning Tree) - MST 性质
- 普里姆(prim)算法
- 克鲁斯卡尔(Kruskal)算法
- 最短路径
- 定义
- Dijistra算法
- Floyd算法
- 有向无环图(DAG)
- 定义
- 应用解决方法
- AOV网 - 拓扑排序
- AOE网 - 关键路径
图
图的定义和基本术语
有向树:当有向图中仅有1个顶点的入度为0,其余顶点的入度均为1,此时是一个有向树。
权与网:图中边或弧所具有的相关数称为权。表明从一个顶点到另一个顶点的距离或耗费。带权的图称为网。
连通分量
极小连通子图
图的抽象数据类型定义
图的存储结构
- 数组表示法:邻接矩阵(重点)
- 链式存储结构:邻接表(重点),邻接多重表,十字链表
邻接矩阵
存储表示,用两个数组分别存储定点表和邻接矩阵
#define Maxlnt 32767 // 表示极大值,即∞
#define MVNum 100 // 最大顶点数
typedef char VerTexType; // 设顶点的数据类型为字符型
typedef int ArcType; // 假设边的权值类型为整型
typedef struct{
VerTexType vexs[MVNum]; // 顶点表
ArcType arcs[MVNum][MVNum]; // 邻接矩阵
int vexnum, arcnum; // 图的当前点数和边数
}AMGraph; // Adjacency Matrix Graph
采用邻接矩阵表示法创建无向网
思想:
- 输入总顶点数和总边数 。
- 依次输入点的信息存入顶点表中。
- 初始化邻接矩阵,使每个权值初始化为极大值。
- 构造邻接矩阵 。
// 采用邻接矩阵法创建无向网
Status CreateUDN (AMGraph &G) {
cin>>G.vexnum>>G.arcnum; // 输入总顶点数,总边数
for (i=0; i<G.vexnum; ++i)
cin>>G.vex[i]; //依次输入点的信息,构造顶点表
for (i=0;i<G.vexnum;++i) // 初始化邻接矩阵
for (j=0;j<G.vexnum;++j)
G.arcs[i][j] = Maxlnt; //边的权值均设为最大
for (k=0;k<G.arcnum;++k) { //构造邻接矩阵
cin>>v1>>v2>>w; 输入一条边的两个顶点以及权值
i = LocateVex(G,v1);
j = LocateVex(G,v2); // 确定v1,v2在G中位置
G.arcs[i][j] = w; //边<v1,v2>的权值设为w
G.arcs[j][i] = G.arcs[i][j]; // 无向网(图)的对称性质
} // for
return OK;
} // craeteUDN
无向网稍加改动就可以创建无向图/有向图/有向网
邻接矩阵优点:
- 直观 、 简单 、 好理解
- 方便检查任意一对顶点间是否存在边
- 方便找任一顶点的所有 " 邻接点 " (有边直接相连的顶点)
。 - 方便计算任一顶点的 " 度 "
- 无向图:对应行(或列)非0元素的个数
- 有向图:对应行非0元素的个数是 " 出度“,对应列非 0 元素的个数是 " 入度 ”
邻接矩阵缺点:
- 不便增加/删除结点
- 浪费空间 - 稀疏图(点多边少)
- 浪费时间 - 统计稀疏图中多少条边
邻接表链式表示法
无向图:
特点:
- 邻接表不唯一,因为边的顺序可变
- 若无向图中有7个顶点 、e 条边,则其邻接表需 n 个头结点和 2e 个
表结点。适宜存储稀疏图。 - 无向图中顶点Vi的度为第 i 个单链表中的结点数。
有向图
邻接表链式的存储表示
==> AdjList v 相当于 VNode v[MVNum]
算法思想:
- 输入总顶点数和总边数
- 建立顶点表
- 依次输入点的信息存入顶点表中
- 使每个表头结点的指针域初始化为 NULL
- 创建邻接表
- 依次输入每条边依附的两个顶点
- 确定两个顶点的序号 i 和 j ,建立边结点
- 将此边结点分别插入到 vi 和 vj 对应的两个边链表的头部(头插法)
// 邻接链表创建无向网
Status CreateUDG(ALGraph &G){
cin>>G.vexnum>>G.arcnum; // 输入顶点数、边数
for (i=0;i<G.vexnum;++i){ // 初始化顶点表,顶点firstarc指针置空
cin>>G.vertices[i].data;
G.vertices[i].firstarc=NULL;
}// for
for (k=0;k<G.arcnum;++k){ // 依次输入每条边的信息并建立单链表
cin>v1>>v2;
i=LocateVex(G,v1); // 得到两个顶点在顶点表中的位置
j=locateVex(G,v2);
p1=new ArcNode; // 生成一个新的边结点*p1
p1->adjvex=j; // 邻接点序号为j
p1->nextarc=G.vertices[i].firstarc; // 头插法
G.vertices[i].firstarc=p1; // 新结点插入顶点vi的边表头部
p2=new ArcNode; // 无向网的对称性,相同的步骤生成新结点*p2
p2->adjvex=i;
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2;
}// for
return OK;
}
邻接表特点
- 方便找任一顶点的所有"邻接点"
- 节约稀疏图的空间 – 需要 N 个头指针+ 2E 个结点(每个结点至少 2 个域)
- 方便计算任一顶点的“度"?·
- 对无向图:是的
- 对有向图:只能计算"出度";需要构造"逆邻接表"来方便计算"入度"
- 不方便检查任意一对顶点间是否存在边
邻接矩阵和邻接表的关系
- 联系:邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
- 区别:
- 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
- 邻接矩阵的空间复杂度为 O( n2 )而邻接表的空间复杂度为 O( n + e )。
- 用途:邻接矩阵多用于稠密图;而邻接表多用于稀疏图
十字链表和邻接多重表
十字链表
Orthogonal List - 有向图的另一种链式存储形式,是邻接表和逆邻接表的结合体
邻接多重表
图的遍历
定义
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算。
实质:找每个顶点的邻接点的过程
方法:
- 深度优先搜索 (Depth First Search - DFS )
- 广度优先搜索(Breadth First Search - BFS)
深度优先遍历(DFS)
连通图的深度优先遍历类似于树的先根遍历
非连通图的遍历和连通图一样不过是以连通分量划分,一个连通分量访问完了再访问下一个直到全部访问。
算法实现
// 邻接矩阵实现深度优先遍历
void DFS(AMGraph G, int v) { // G为邻接矩阵类型
count<<v; visit[v]=True; // 访问输入的v顶点
for (w=0; w<G.vexnum; w++){ // 依次检查邻接矩阵v所在的行
if ((G.arcs[v][w]!=0)&&(!visit[w])){
DFS(G,w); // w是v的邻接点,如果没被访问过就递归调用DFS
}
}
}
算法效率
- 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在行,时间复杂度为 O (n2)
- 用邻接表来表示图,虽然有 2e 个表结点,但只需扫描 e 个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为 O(n+e)
结论:稠图适于在邻接矩阵上进行深度遍历, 稀疏图适于在邻接表上进行深度遍历。
广度优先遍历(BFS)
- 方法:从图的某一结点出发,首先依次访问该结点的所有邻接点V1,V2…Vn, 再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点, 重复此过程,直至所有顶点均被访问为止。
实现过程
// 按广度优先非递归遍历连通图G
void BFS(Graph G, int v){
count<<v; visited[v]=True; // 访问结点v
InitQueue(Q); // 辅助循环队列Q初始化,置空
EnQueue(Q,v); // v进队
while (!EmptyQueue(Q)){ // 当队列不空时循环
DeQueue(Q,u); // 队头元素出队置为u
for (w=FirstAdjVex(G,u); w>=0; w=NextAdjVex(G,u,w)){ // 依次寻找元素u的邻接元素
if (!visited[w]){ // 邻接元素w没被访问时,标记访问flag并入队
count<<w; visited[w]=True; EnQueue(Q,w)
}
}
}
}
算法效率:邻接矩阵O(n2), 邻接表O(n+e) (n为元素,e为结点)
DFS/BFS算法比较
空间复杂度相同,都是O(n)(借用了堆栈或队列);时间复杂度只与存储结构(邻接矩阵或邻接表)有关,而与搜索路径无关。
图的应用
生成树
定义:所有顶点均由边连接在一起,但不存在回路的图。
-
一个图可以有许多棵不同的生成树
-
所有生成树具有以下共同特点:
- 生成树的顶点个数与图的顶点个数相同,
- 生成树是图的极小连通子图,去掉一条边则非连通;
- 一个有n个顶点的连通图的生成树有n-1 条边,
- 在生成树中再加一条边必然形成回路。
- 生成树中任意两个顶点间的路径是唯一的
-
含 n 个顶点n-1 条边的图不一定是生成树。
无向图的生成树
最小生成树
最小生成树:给定一个无向网络,在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
典型应用:n个城市(顶点)间建立通信网(连通网),铺设n-1条线路(边),每条线路的花费为(权值),如何费用最小?
构造最小生成树(Minium Spanning Tree) - MST 性质
MST性质内容: 设 N = (V, E) 是一个连通网, U是顶点集 V 的一个非空子集。若边 (u, v) 是一条具有最小权值的边,其中 u属于U, v属于V-U, 则必存在一棵包含边 (u, v) 的最小生成树。
普里姆(prim)算法
思想:
克鲁斯卡尔(Kruskal)算法
两种算法比较:
最短路径
定义
问题抽象:在有向网中 A 点(源点)到达 B 点(终点)的多条路径中,寻找一条各权值之和最小的路径,就是最短路径。
最短路径与最小生成树不同,路径上不一定包含n个顶点,也不一定包含n-1条边。
第一类问题:两点间最短距离
第二类问题:某源点到其他各顶点最短路径
解决办法:
- 单源最短路径 – 用Dijkstra(迪杰斯特拉)算法
- 所有顶点间的最短路径 – 用Floyd(弗洛伊德)算法
Dijistra算法
- 初始化:先找出从源点 v0 到各终点的直达路径 (v0,vk) ,即通过一条弧到达的路径。
- 选择: 从这些路径中找出一条长度最短的路径 (v0,u)。
- 更新:然后对其余各条路径进行适当调整.若在图中存在弧 (u,vk) ,且 (v0,u) + (u,vk) < (v0,vk)则以路径 (v0,u,vk) 代替(v0,vk )
- 在调整后的各条路径中,再找长度最短的路径,依此类推。
核心:按路径长度递增次序产生最短路径
算法步骤:
Floyd算法
思想:
- 逐个顶点试探
- 从vi到vj的所有可能路径中
- 选出一条长度最短的路径
有向无环图(DAG)
定义
无环的有向图,directed Acycline Graph
注意与有向树和有向图的区别,一个顶点可以有多个前驱但是不存在环。
有向无环图常用来描述一个工程或系统的进行过程。一个工程可以分为若干个子工程,只要完成了这些子工程(活动),就可以导致整个工程的完成。
应用解决方法
AOV网(activity on vertex network, 顶点表示活动,边表示活动之间优先级制约关系) – 拓扑排序
AOE网(activity on edge, 边表示活动,顶点表示活动的开始/结束事件) – 关键路径
AOV网 - 拓扑排序
特点:
-
若从 i 到 j 有一条有向路径,则 i 是 j 的前驱;j 是 i 的后继。
-
若<i,j>是网中有向边,则 i 是 j 的直接前驱; j 是 i 的直接后继。
-
AOV 网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为央条件,显然这是荒谬的。
如何判别 AOV 网中是否存在回路? ==> 拓扑排序
拓扑排序:
在 AOV 网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若 AOV 网中有弧< i, j >存在则在这个序列中, i 一定排在 j的前面,具有这种性质的线性序列称为拓扑有序序列,相应的拓扑有序排序的算法称为拓扑排序。
步骤:
-
在有向图中选一个没有前驱的顶点且输出之。
-
从图中删除该顶点和所有以它为尾的弧。
-
重复上述两步,直至全部顶点均已输出,或者当图中不存在无前驱的顶点为止
检测 AOV 网中是否存在环方法
- 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该 AOV 网必定不存在环。反之,则存在环,如下图:
AOE网 - 关键路径
把工程计划表示为边表示活动的网络,即 AOE 网,用顶点表示事件,弧表示活动,弧的权表示活动持续时间。
关键路径:路径长度最长的路径。
路径长度:路径上各活动持续时间之和。
下图的概念先理解,后面要用:
- 事件的最早发生时间ve(vj)
- 事件最晚发生时间 vl(vj)
- 活动的最早发生时间 e(i)
- 活动的最晚发送时间 l(i)
l(i) - e(i)
表示完成活动 ai 的时间余量。l(3) - e(3) = 90
关键活动: 关键路径上的活动,即l(i) == e(i)
(即l(i) - e(i) = 0
)的活动
如何计算ve(j), vl(i):
求解关键路径步骤:
讨论:
- 若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。如: a11, a10, a8, a7
- 如果一个活动处于所有的关键路径上,那么提高这个活动的速度就能缩短整个工程的完成时间。如: a1、a4
- 处有的关键路径上的活动完成时间不能缩短太多,否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。如: a1 由6 天变成 3 天,就会改变关键路径。
TO BE CONTINUED…