文章目录
图的应用
最小生成树
最小生成树的两种算法中
P算法从一个点出发(满足连通的性质)扩展到一个包含所有顶点的树(依然满足连通的性质),其本质是构造递增序列(添加满足条件的点和边)来逼近目标值(极大连通图-最小生成树),类似于莫奈从一张空白的画纸上描绘出阿佛尔港口日出的景象;//这也是贪心算法的一种体现,不从整体考虑,每一步都考虑局部最适、最优解。
K算法从整个图出发,“挑选”满足条件构成无回路子图的边,其本质是构造一个递减序列逼近目标值,类似于米开朗基罗将一块岩石凿刻出优雅的大理石雕塑大卫;
图G的最小生成树T是G的所有生成树中边的权值之和最小的树
最小生成树:Minimum-Spanning-Tree,MST
性质:
- 最小生成树不是唯一的
- 最小生成树的边的权值之和是唯一的
- 最小生成树的边数为定点数减一
构成最小生成树的大多数算法利用了最小生成树的下列性质:假设G是一个带全连通的无向图,Y是顶点集V的一个非空子集。假设U是顶点集V的一个非空子集,假设有序对(u_0,v_0)对应{(u,v)|u ∈U ∩ v ∈ V/U}中最小权值的边,则必存在一颗包含(u_0,v_0)的最小生成树。
Prim算法
解决方法:
- 任取一个顶点
- 选取与顶点路径最短的顶点构成顶点集
- 选取与顶点集中任意顶点路径最短的顶点构成新的顶点集
- 重复3过程直至顶点集中存在vernum个顶点
实现步骤:
- 假设G={V,E}是连通图,其最小生成树T = (U,E_T),E_T是最小生成树的边的集合。
- 初始化:向空树T = (U,E_T)中添加图G = (V,E)的任一顶点u_0,使U = {u_0},E_T等于空集
- 循环(重复直至U = V):从图G中选择{(u,v)|u ∈U ∩ v ∈ V/U}中具有最小权值的边(u_0,v_0),加入树T,置U = U ∪ {v},E_T = E_T∪{(u_0,v_0)}
#define MaxVertexNum 100;
VertexType U[MaxVertexNum + 1];
VertexType[0] = 0; //用数组的第一个元素来记录数组实际使用长度
void Prim(G,T){
InitTree(T);
U[1] = w; //w为G中的任一个顶点元素
TreeInsert(T,w);
U[0]++;
while(U[0] < G.vernum){
w = MiniPath(G,U);
TreeNode s = TreeTransferGraphNodeToTreeNode(w)
TreeInsert(T,s);
}
}
//在不确定存储结构和操作函数的情况下,用伪代码标识更准确,不会出现上述表示不明的状况
void Prim(G,T){
T = {}; //初始化空树
U = {w}; //U为树的顶点集,添加任一顶点w
while( (V - U)!= {}){ //若树中不含图中的全部顶点,则循环继续
T = T ∪ {(u,v)}; //将边归入树
U = U ∪ {v}; //将顶点归入树
}
}
Prim算法的时间复杂度为O(|V|2):在上述伪代码中,循环 (V - U)!= {}的时间成本为|V|,循环内要找到MiniPath点,时间成本为|V|,故算法的时间复杂度为O(|V|2)
Kruskal算法
按权值递增的方式构造最小生成树
解决方法:
- 初始化树的顶点集
- 此时有n个连通分量,即所有顶点互不连通,构成单独的连通分量
- 从图的边集取权值最小的边
- 判断边的两个关联的顶点是够在同一个连通分量,如果不在同一个连通分量,则将边加入到树的边集
- 重复上述过程直至连通分量数为1
伪代码简单实现:
void Kruskal(V,T){
T.v = V; //初始化树的顶点集
numS = n; //连通分量数
while(nums > 1){
(v,u) = MiniPath(E);
if(v,u属于不同的连通分量){
T.e = T.e∪{(v,u)};
numS --;
}
}
}
最短路径
广度优先查找最短路径只是对无权图而言的。当图为带权图时,把从一个顶点到另一个顶点的路径的所经过边上的权值之和定义为该路径的带权路径长度,把带权路径长度最短的那条路径称为最短路径。
求解最短路径依赖于最短路径的性质:在两个顶点一条最短路径A上的任意两个顶点的最短路径B包含于A。
带权有向图G的最短路径问题一般可以分为两类:
- 单源最短路径:求图中某一顶点到其他顶点的最短路径问题
- 顶点间的最短路径
Dijkstra算法求单源最短路径问题
D算法设置一个集合S记录以求得最短路径的顶点,初始时把源点v_0放入S,集合S每并入一个新顶点v_i,都要修改源点v_0到集合V-S中顶点当前的最短路径长度值
D算法也是贪心算法的一种应用
- 将最短路径顶点集设为只含源点的顶点集
- 将最短路径数组初始化
- 选择离选定的顶点集有最小权值的顶点 j ,将其加入最短路径顶点集
- 规划最短路径顶点集的补集中各点的最短路径,具体做法为比较dist[k](新顶点未加入时的最短路径)和dist[ j ] + arcs[ j ][ k ] (新顶点加入后添加的新路径中最短的路径长度)。取两者中较小的设置为补集的新最短路径长度。
- 重复过程3.4,知道所有顶点都加入最短路径顶点集
在上述过程中用到的存储结构有:
- 最短路径顶点集;
- 最短路径数组;
用到的操作有:
- 遍历顶点集除去顶点子集的补集
- 求到一个顶点集有最小权值的点
- 在顶点集中加入新的顶点
- 规划移动某点后补集的最短路径
求最短路径的D算法和求最小生成树的P算法有何相似之处?
都是基于贪心策略,每步都选择局部最优的顶点组成新的最短路径顶点子集
无论用什么存储结构存储图(邻接矩阵、邻接表),算法的时间复杂度都为O(|V|)
需要注意,在D算法中,基于贪心策略,每步都应该获得一个局部最短路径数组,后续不再变更,但当存在负权值时,这个策略是无法实现的,因为在加入负权值的路径时,先前的局部最优路径总能被减小,因此局部最优无法作为全局最优使用。
Floyd算法求顶点之间的最短路径问题
问题:已知一个各边权值均大于0的带权有向图,对任意两个顶点v_i != v_j,要求求出其最短路径和最短路径长度。
基本思想:递推产生一个n阶的方阵序列A(-1),A(0),A(1),…,A(K),…,A(n-1),其中A(k)[i][j]表示添加前k个顶点作为路线上的绕行节点的最短路径长度。
A(-1)与带权有向图的邻接矩阵表示相同,递推公式为
A
(
k
)
[
i
]
[
j
]
=
m
i
n
(
A
(
k
−
1
)
[
i
]
[
j
]
,
A
(
k
−
1
)
[
i
]
[
k
]
+
A
(
k
−
1
)
[
k
]
[
j
]
)
A^{(k)}[i][j] = min{ (A^{(k-1)}[i][j],A^{(k-1)}[i][k] +A^{(k-1)}[k][j])}
A(k)[i][j]=min(A(k−1)[i][j],A(k−1)[i][k]+A(k−1)[k][j])
外层地推的时间复杂度是O(|V|),每次递推过程方阵序列的时间复杂度为O(|V|^2),因此算法的时间复杂度为
O
(
∣
V
∣
3
)
O(|V|^3)
O(∣V∣3)
F算法由于代码相当紧凑,仅仅设计对方阵序列的计算,所以在计算中等规模的输入时仍然是有效的。
F算法也可以用于求无向图的最短路径长度,因为无向图可以看作拥有往返权值相等的两条边的有向图。
有向无环图描述表达式
若一个有向图中不存在环,则成为有向无环图,简称DAG(Directed Acyclic Graph/有向非循环图)图;
我们已知表达式可以用二叉树表示,但是二叉树表示的表达式存在相同的部分表达式重复存储的问题,这里我们就可以用有向无环图,将相同的表达式部分只单一的表示,并在初始时重复指向这个单一表示的表达式,从而节省存储空间。
拓扑排序
AOV网
AOV网:(Activity On Vertex NetWork)如果在一个DAG图,顶点表示活动,有向边<V_I,V_J>表示活动v_i必须在活动v_j前进行的关系,则称这种有向图为顶点表示活动的网络,几位AOV网。
在AOV网中,活动V_I是活动V_J的直接前驱,活动V_J是活动V_I的直接后继,这种前驱和后继关系具有传递性,且任何活动V_I都不能以自己作为自己的前驱或者后继。
拓扑排序
拓扑排序:
一个顶点排序是一个有向无环图的拓扑排序,当且仅当这个排序满足:
- 每个顶点出现且仅出现一次
- 如果顶点A出现在顶点B的前面,则在图中找不到从顶点B到顶点A的路径
一种常见的对AOV网进行拓扑排序的算法:
- 从AOV网中选取一个没有前驱的顶点并输出
- 删除与输出节点有关的有向边,余下的节点和边构成新的AOV子网
- 重复1.2的过程直至不存在顶点或者不存在没有前驱的顶点,如果存在顶点但不存在没有前驱的顶点,则原图不构成AOV网(必然存在环)
算法的实现总可以借鉴的点:
- 返回布尔类型
- 初始化一个栈用于存储入度为0的顶点
- 图的存储用链接表,vertices[]表示顶点数组,firstarc表示顶点表的第一个弧节点,弧节点的adjvex元素表示弧指向的点,即删除弧后入度-1的点
- 单独用一个数组indegree[ ]记录顶点入度。
bool TopologicalSort(Graph G){
InitStack(S);
int count = 0;
for(int i = 0 ; i < vexnum ; i++ ){
if(indegree[i] == 0)
Push(S,i);
}
while(!IsEmpty(S)){
Pop(S,i);
print[count++] = i;
for(p = G.vertices[i].firstarc;p; p = p -> nextarc){
v = p -> adjvex;
if(!(--indegree(v)) //如果删掉的边指向的顶点入度减为0
Push(S,v);
}
}
if(count < G.vernum)return false;
return true;
}
由于删除每个节点的同时还要删除节点的边,时间复杂度为 O ( ∣ V ∣ + ∣ ∣ E ∣ ) O(|V|+||E|) O(∣V∣+∣∣E∣)
注意:
- 入度为0的顶点,即没有前驱活动的或前去活动都已经完成的节点,工程上可以从这个活动开始或继续;
- 若一个顶点有多个前驱或后继,则拓扑排序的结果不唯一
- 对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑排序;若其存在拓扑排序,则邻接矩阵不一定是三角矩阵;?
逆拓扑排序
- 从AOV网中选取没有后继的顶点并输出;
- 从网中删除该顶点和所有以它为终点的有向边
- 重复12直至网中不存在顶点或不存哎没有后继的顶点(必有环存在)
关键路径
AOE网:Acticity On Edge Network
用顶点表示事件,有向带权边表示活动的有向无环图
事件是在时间点发生的,活动是持续的时间区间
AOE网只有一个入度为0的点,称之为开始顶点(源点),表示整个工程的开始
只有一个出度为0的点,称之为结束顶点(汇点),表示整个工程的结束
在AOE网中,活动可以并行执行,从源点到汇点的路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已经完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,拥有最大路径长度的路径称为关键路径。
完成整个活动的最短时间就是关键路径的长度,即关键路径活动花销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成则整个工程的完成时间就会延长。因此,只要找到了关键活动就能找到关键路径,也就可以得出最短完成时间。
下面给出在寻找关键路径时所用到的几个参量的定义
-
事
件
v
k
的
最
早
发
生
时
间
v
e
(
k
)
事件v_k的最早发生时间ve(k)
事件vk的最早发生时间ve(k)
从原点v到顶点v_k的最长路径长度。
事件v_k的最早发生时间决定了所有从v_k开始的活动最早开始的时间。
因为vk是由前驱唯一决定的,可以在拓扑排序的基础上进行
v e ( 源 点 ) = 0 ve(源点) = 0 ve(源点)=0
v e ( k ) = M a x [ v e ( j ) + W e i g h t ( v j , v k ) ] , v k 时 v j 的 任 意 后 继 ve(k) = Max{[ve(j) + Weight(v_j,v_k)]},v_k时v_j的任意后继 ve(k)=Max[ve(j)+Weight(vj,vk)],vk时vj的任意后继
注 意 到 v k 至 少 有 一 个 前 驱 , 因 此 迭 代 公 式 中 的 比 较 的 变 量 是 v ( j ) 注意到v_k至少有一个前驱,因此迭代公式中的比较的变量是v(j) 注意到vk至少有一个前驱,因此迭代公式中的比较的变量是v(j)
计算ve()值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:- 初始化,令ve[1…n] = 0;
- 输出一个入度为0的顶点v_j时,计算它所有直接后继顶点v_k的最早发生时间,若 v e [ j ] + W e i g h t ( v j , v k ) > v e [ k ] , 则 v e ( k ) = v e [ j ] + W e i g h t ( v j , v k ) ve[j] + Weight(v_j,v_k) > ve[k],则ve(k) = ve[j] + Weight(v_j,v_k) ve[j]+Weight(vj,vk)>ve[k],则ve(k)=ve[j]+Weight(vj,vk)
- 重复2过程直到输出全部顶点
-
事
件
v
k
的
最
迟
发
生
时
间
v
l
(
k
)
事件v_k的最迟发生时间vl(k)
事件vk的最迟发生时间vl(k)
最迟发生时间指在不推迟整个工程完成的前提下,即保证他的后继事件v_j在其最迟发生时间vl(j)能够发生时,该事件最迟必须发生的时间。
递推公式为:
v l ( 汇 点 ) = v e ( 汇 点 ) / / 初 始 值 vl(汇点) = ve(汇点) //初始值 vl(汇点)=ve(汇点)//初始值
v l ( k ) = M i n [ v l ( j ) − W e i g h t ( v k , v j ) ] , v k 为 v j 的 任 意 前 驱 vl(k) = Min{[vl(j) - Weight(v_k,v_j)]},v_k为v_j的任意前驱 vl(k)=Min[vl(j)−Weight(vk,vj)],vk为vj的任意前驱
注意:在计算vl时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算 -
活
动
a
i
的
最
早
开
始
时
间
e
(
i
)
活动a_i的最早开始时间e(i)
活动ai的最早开始时间e(i)
它是指该活动弧的起点所表示的事件的最早发生时间, 若 边 < v k , v j > 表 示 活 动 a i , 则 有 e ( i ) = v e ( k ) 若边<v_k,v_j>表示活动a_i,则有e(i) = ve(k) 若边<vk,vj>表示活动ai,则有e(i)=ve(k) -
活
动
a
i
的
最
迟
开
始
时
间
l
(
i
)
活动a_i的最迟开始时间l(i)
活动ai的最迟开始时间l(i)
它是指该活动的弧所表示的时间的最迟发生时间, 若 边 < v k , v j > 表 示 活 动 a i , 则 有 l ( i ) = v l ( j ) − W e i g h t ( v k , v j ) 若边<v_k,v_j>表示活动a_i,则有l(i) = vl(j) - Weight(v_k,v_j) 若边<vk,vj>表示活动ai,则有l(i)=vl(j)−Weight(vk,vj)
5. 一 个 活 动 的 最 迟 开 始 时 间 和 最 早 开 始 时 间 的 差 额 d ( i ) = l ( i ) − e ( i ) 一个活动的最迟开始时间和最早开始时间的差额d(i) = l(i) - e(i) 一个活动的最迟开始时间和最早开始时间的差额d(i)=l(i)−e(i)
它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动a_i可以拖延的时间。若一个活动的时间余量为0,则说明该活动必须如期完成,否则会拖延整个工程的进度。所以称 d ( i ) = 0 的 活 动 为 a i 的 关 键 活 动 d(i) = 0的活动为a_i的关键活动 d(i)=0的活动为ai的关键活动
关键路径算法步骤:
- 从源点出发,令ve(源点) = 0,按拓扑排序求其余顶点的最早发生时间ve()。
- 从汇点出发,令vl(汇点) = ve(汇点),按逆拓扑排序求其余顶点的最迟发生时间vl()。
- 根据各顶点的ve()求所有弧(活动)的最早发生时间e()
- 根据个顶点的vl()求所有弧(活动)的最迟发生时间l()
- 求AOE网中所有弧(活动)的差额d(),找出所有d() = 0的活动构成关键路径
关键路径的所有活动都是关键活动
关键路径不唯一