开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第20天,点击查看活动详情
8.2 拓扑排序
8.2.1 拓扑排序
例:计算机专业课排课
把课程列表转换为图,其中顶点代表课程,则从V到W有一条边代表:V是W的预修课程
以下是专业课的依赖图(也叫做AOV,是Activity On Vertex的缩写)
以上抽象成一个拓扑排序的问题
- 拓扑序:如果图中从V到W有一条有向路径,则V一定排在W之前。满足此条件的顶点序列称为一个拓扑序。(意思就是要学习W之前必须要学习V的话,那么在输出的时候V一定要在W之前被输出)
- 获得一个拓扑序的过程就是拓扑排序
- AOV如果有合理的拓扑序,则必定是有向无环图(Directed Acyclic Grap,简称DAG)
如图:什么是合理的拓扑序?有环意味着这个活动他自己是他自己的前驱节点(也就是他在他开始之前就必须要已经结束了),而V必须在V开始之前结束是不合理的。所以这个是不行的,得不到合理的拓扑序
经过排版得出: 规律是:每次我们要输出哪个顶点?输出没有前驱顶点的那个顶点
怎么只是没有前驱顶点?对于顶点来说,我们有两个度可以记得,一个是入度一个是出度
没有前驱顶点的那些课程的特点:他们的入度都是0,没有任何一条边指向他。
拓扑序步骤:
- 在输出的时候我们要选择入度为0的那个顶点
- 在输出的同时我们要把这个顶点从原始的图里面彻底抹掉,当我们把原来的图抹光的时候,一个正常的拓扑序就产生了
拓扑排序的算法
void TopSort()
{
for( cnt = 0; cnt < |V|; cnt++ ){
//在TopSort函数中,如果外循环还没结束,就已经找不到“未输出的入度为0的顶点”,则说明:图中必定存在回路
V = 未输出的入度为0的顶点;//程序复杂度的复杂程度主要取决于这一步。
//方法1:简单粗暴的把所有点都扫描一遍然后去找那个没有输出过的且入度又为0的顶点,复杂度就为O(|V|)。
//整体复杂度:T = O(|V|²) 这是不聪明的做法
//方法2(聪明的算法):随时将入度变为0的顶点放到一个容器(可以理解为任何东西,数组、链表、堆栈、队列之类里都可以,总之放到一个特殊的地方)里面。这样就不用重新去扫描所有的顶点集合,直接从容器里面取一个出来就行了,这样复杂度就直接变成常数级别的了
if( 这样的V不存在 ){
Error("图中有回路");
break;
}
输出V,或者记录V的输出序号;
for( V 的每个邻接点 W )
Indegree[W]--;//意味着把V到W的这条边给减掉了,W少了一条进来的边,所以入度就会减少1。就是把V抹掉的意思(抹掉不是删掉)
}
}
聪明的算法
方法2(聪明的算法):随时将入度变为0的顶点放到一个容器(以下是用队列做排序)
void TopSort()
{
for ( 图中每个顶点 V )
if( Indegree[V]==0)//检查有没有入度为0的
Enqueue( V,Q );//有的都放到容器里面
while(!IsEmpty(Q)){
V = Dequeue( Q );
输出V,或者记录V的输出序号;cnt++//输出后就需要抹掉,就通过下面那个for循环解决,入度减一
for( V 的每个邻接点 W)
if( --Indegree[W]==0)//剪完入度为0就放入下面那个容器里面,下次可以取出来用,否则就说明都不做,继续去容器里取下一个顶点
Enqueue( W,Q );
}
if( cnt !=|V|)//检查顶点有没有输出完,没有输出完就意味着遇到回路了
Error("图中有回路");
}
此时时间复杂度为:T = Q(|V|+|E|)
如果是稀疏图的话是QV的复杂度,稠密图的话|V|²数量级的
8.2.2 关键路径
AOE(Activity On Edge)网络
- 一般用于安排项目的工序
Earliest:所有的任务最早的完成时间
Latest:所有任务最晚完成时间
0顶点表示开始,8顶点表示结束
每一条边代表一件事情
每件事情按照相互依赖的顺序完成了之后到达顶点8为结束
边上的权重代表持续时间
边之间的关系:两个小组之间开工就必须两个小组都完工才能往下走
假设不仅得1、2组结束,同时还需要等3一起全部完工才能往下走
问题1:整个工期有多长
问题2:哪几个组有机动时间?(就是可以随时拉出去干活的,不需要赶工的)
绿色部分为机动时间:
什么是关键路径:整个manager最需要关注的那些组,哪些组是一天都不能耽误的(耽误一条整个工期都得往后耽误)。所以关键路径就是指绝对不允许延误的活动组成的路径
图之习题选讲-旅游规划
图习题.1 核心算法
题意理解
-
城市为结点
-
公路为边
- 权重1:距离
- 权重2:收费
红色是起点,蓝色是终点。;绿色字体是距离,紫色字体是收费
-
单源最短路
- Dijkstra算法 ——距离 (一个结点一个结点往那个集合里面去收集,每收进来一个就要检查以下其他结点距离有没有被新进来的结点影响,得到更短的距离就刷新掉,得不到就保持原样)
- 等距离时按收费更新
核心算法
最基础版本的Dijkstra算法
void Dijkstra( Vertex s )
{
while(1){
V = 未收录顶点中dist最小者;
if( 这样的V不存在 )
break;
collected[V] = true;
for( V 的每个邻接点 W )
if( dist[V] + E<v,w> < dist[W] ){//Vd点的加入使我们得到一个更短的w的距离
dist[W] = dist[V] + E<v,w>;//更新距离
path[W] = V;//更新最短路径
}
}
}
根据基础进行改良过后的
最基础版本的Dijkstra算法
void Dijkstra( Vertex s )
{
while(1){
V = 未收录顶点中dist最小者;
if( 这样的V不存在 )
break;
collected[V] = true;
for( V 的每个邻接点 W )
if( dist[V] + E<v,w> < dist[W] ){//Vd点的加入使我们得到一个更短的w的距离
dist[W] = dist[V] + E<v,w>;//更新距离
path[W] = V;//更新最短路径,s走到V的所有费用加上V走到W这条边的费用
}
else if((dist[V]+E<v,w> == dist[W]) && (cost[V] + C<v,w> < cost[W])){//等长最短路径并且这条路径上面的新费用比原来的费用小的话那也要更新路径
cost[W] = cost[V] + C<v,w>;//更新费用
path[w] = V;//更新路径
}
}
}
图习题.2 其他推广
其他类似问题
-
要求最短路径有多少条?
- count[s] = 1;//计数器的作用
- 在要求数最短路径有多少条的问题中,如果找到更短路,则count[W]应该更新为要求边数最少的最短路:count[W]=count[V]
- 在要求数最短路径有多少条的问题中,如果找到等长路,则count[W]应该更新为:count[W]+=count[V]
-
要求边数最少的最短路
- 费用初始化怎么做?初始化为0
- count[s] = 0;
- 如果找到更短路:count[W] = count[V] + 1;
- 如果找到等长路:count[W] = count[V] + 1;//count[V]加上新的权重