961全部内容链接
拓扑排序:在一个有向无环图中,把节点排成一个序列,当且仅当满足以下两个特性时,称该序列为该图的一个拓扑排序:
拓扑排序的实际应用:
比如该图是一个有向无环图。每个节点代表着一种活动。但是每个活动都需要一个前置的触发条件,比如,如果你不买菜,就没有鸡蛋,就没法打鸡蛋。所以打鸡蛋的前置条件是买菜。其他的也同理。 但是在这个所以,这些所有活动应该按照什么样的顺序来进行,最后排一下序,这个排序后的序列就是这个有向无环图的拓扑排序。比如,对于该图,它的拓扑排序不止一个,可以举例如下两个:
除了上述两种情况,还有很多种情况存在,所以拓扑排序是不唯一的。
AOV网(Activity On Vertex Network)是用顶点表示活动的网络。主要用于描述各个活动的优先级。所以它的边没有权值。如定义中举的例子,我们并不关心哪一项活动到底花费多长时间,我们只关心哪一步先做,哪一步后做。
拓扑排序的算法相对简单,主要思路为:
算法伪代码:
Java代码如下:
// AOV网拓扑排序
public static List topologicalSort(DirectedGraph graph) {
List sortResult = new ArrayList();
Map<Object, Integer> vertexInDegree = new HashMap<>(); // 存储每个节点的入度
Stack stack = new Stack(); // 存储入度为0的节点
for (Object vertex : graph.getVertexes()) {
// 遍历所有节点,初始化所有的入度
int inDegree = graph.getInDegree(vertex);
vertexInDegree.put(vertex, inDegree); // 将节点的度存储到map中
if (inDegree == 0) stack.push(vertex); // 如果节点的入度为0,则入栈
} // 初始化完成
while (!stack.isEmpty()) {
// 如果栈中还有入度为0的点,则继续,当栈中没有入度为0的点,说明拓扑排序已经完成
Object vertex = stack.pop();
sortResult.add(vertex);
for (Object neighbor : graph.neighbors(vertex)) {
// 遍历当前节点的所有邻接节点(后继节点)
// 将该节点的所有后继节点的入度减1,相当于删除了该节点以及该节点的边。
vertexInDegree.put(neighbor, vertexInDegree.get(neighbor) - 1);
// 若该节点的后继节点入度减1后为0,则将其入栈
if (vertexInDegree.get(neighbor) == 0) stack.push(neighbor);
}
}
return sortResult;
}
这里使用到了一个之前没有用过的有向图接口和邻接矩阵有向图的实现,代码如下:
public interface DirectedGraph<VertexType> extends Graph<VertexType> {
int getInDegree(VertexType vertex); // 获取节点入度
int getOutDegree(VertexType vertex); // 获取节点出度
}
这个有向图接口继承自普通图,多了两个接口
// 邻接矩阵实现有向图
public class AdjacencyMatrixDirectedGraph<VertexType> extends AdjacencyMatrixUndirectedGraph<VertexType>
implements DirectedGraph<VertexType> {
public AdjacencyMatrixDirectedGraph(int maxVertexNum) {
super(maxVertexNum);
}
@Override
public void addEdge(VertexType x, VertexType y) {
int xIndex = findIndex(x);
int yIndex = findIndex(y);
edges[xIndex][yIndex] = 1; // 因为是有向图,所以只给边赋值
}
@Override
public int getInDegree(VertexType vertex) {
int inDegree = 0;
int xIndex = findIndex(vertex);
for (int i = 0; i < edges.length; i++) {
if (edges[i][xIndex] > 0) inDegree++;
}
return inDegree;
}
@Override
public int getOutDegree(VertexType vertex) {
int outDegree = 0;
int[] edge = edges[findIndex(vertex)];
for (int i : edge) {
if (i > 0) outDegree++;
}
return outDegree;
}
}
这个是邻接矩阵有向图的实现
AOE(Activity On Edge Network)网和AOV网很像,从名字中基本可以看出,AOV的V是vertex,所以AOV网是顶点代表活动。而AOE网的E是Edge,也就是边代表活动。所以,用边表示活动的网,简称AOE网。而AOE网中的顶点代表着事件。
比如如图一张AOE网,事件就是顶点,比如V1(开始事件),V2(可以切了事件),V3(可以抄了事件)。而边代表活动,比如a1(打鸡蛋活动)等等。
如图所示,可以看出事件只代表一种状态,不消耗时间。而活动是需要消耗一定时间的,而这个时间一般用权值代表。
AOE网具有如下性质:
关键路径的概念:
从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径。而把关键路径上的活动称为关键活动。注意,多条路径是可以并行执行的,比如打鸡蛋可以和洗番茄并行执行。
解释:汇点就是除开始之外的其他顶点。简单点说关键路径就是影响整个工期的路径。比如在上图中炒菜(a4)就是一个关键活动,因为在整个工期中,它快整个就快,它慢,整个就慢。而切番茄(a3)也是一个关键活动。因为切番茄的快慢也是可以直接影响整个进度的,因为打蛋只需要两分钟,所以并不会影响整体进度。
下面这几个概念很重要,这几个概念涉及到算法实现,估计也会是出题重点。
1. 事件Vk的最早发生时间ve(k)
事件最早发生时间就是字面意思。比如:“开始(V1)”的最早发生时间是0。“可以切了(V2)”的最早发生时间是1(因为他前面需要花一分钟的时间切番茄)。“可以炒了(V3)”的最早发生时间是4(因为他前面要经历a2和a3,而打鸡蛋的时间小于它们俩之和,所以不影响)。结束的最早发生时间是6。
2.事件Vk最迟发生时间vl(k)
最迟发生时间是指在不推迟整个工程完成的前提下,即保证它的后继事件Vj在其最迟发生时间vl(j)能够发生时,该事件最迟必须发生的时间。
上面这个图不太能体现这个例子,我对其进行加工一下,如图
假设对上述图进行改造,在打鸡蛋之前又加了拿鸡蛋,增加了事件1.5。切番茄的时间也相应增加一些。此时关键路径没有变,还是a1,a3,a4。
对上述例子中,V1.5事件的最晚发生时间为4。因为在第6分钟的时候,洗番茄和切番茄已经干完了,虽然打鸡蛋这件事不是关键路径,但是你不能影响别人的进度,所以你在第4分钟的时候,就必须开始打鸡蛋,要不然你就来不及了。
3.活动ai的最早开始时间e(i)
理解了上面两个时间,那么这个也就不难理解了。字面意思。还用上面的老图。
“洗番茄(a2)”的最早发生时间是0,“打鸡蛋(a1)”的最早发生时间也是0。“炒菜(a4)”的最早发生时间是4。也就是说,活动的最早发生时间等于该边对应起点的最早发生时间。即 e(i)=ve(k)。
4.活动ai的最迟开始时间l(i)
这个概念与时间的最迟发生时间差不多。比如,“打鸡蛋(a1)”这个活动的最晚发生时间为2。因为打鸡蛋需要2分钟,而洗番茄+切番茄需要4分钟。如果在2分钟的时候还不开始打鸡蛋,就会耽误整体进度。注意区分事件最迟发生时间和活动最迟发生时间的区别。事件是顶点,活动是边
5.时间余量
一个活动的“最迟开始时间 - 最早开始时间”就是时间余量。简单点说,就是这个活动可以拖多长时间再开始。比如上图中,打鸡蛋这个活动可以在开始之后拖两分钟再去做,它的活动余量就是2。你决定考研到来不及必须学政治的时候,相差了100天,那学政治这个活动的时间余量就是100天。
核心思想:那些时间余量为0的活动,就是关键活动,他们构成的路径就是关键路径。简单点说就是那些不能够推迟的活动组成的路径就是关键路径。
算法步骤:
算法步骤解释:
关于王道书上的步骤的个人理解:王道书上是五步,王道书在求完节点后,又求了每条边(活动)的最早发生时间和最迟发生时间,然后使用的是边的两个时间之差判断是否为0。它这样的好处个人认为有两个:1. 可以直接求出关键活动,而不用根据两个节点夹出关键活动。 2. 可以适用于复杂图,如图所示
比如对于该图,很明显,该图是一个复杂图,假设用两个节点之间有多条活动,那么用第一种方法就会认为a1和a2都是关键路径,但实际上只有a2是关键路径。
Java代码如下:
// 求关键路径
public static List<WeightedGraph.Edge> criticalPath(DirectedWeightedGraph graph) {
List topoVertexes = topologicalSort(graph); // 求出节点的拓扑排序
List reverseTopoVertexes = new ArrayList(); // 求出逆拓扑排序
for (int i = topoVertexes.size() - 1; i >= 0; i--) {
reverseTopoVertexes.add(topoVertexes.get(i));
}
Map<Object, Integer> ve = new HashMap<>(); // 存储节点的最早发生时间
Map<Object, Integer> vl = new HashMap<>(); // 存储节点的最迟发生时间
for (Object vertex : topoVertexes) {
// 根据拓扑排序顺序,求出各个节点的最早发生时间
List predecessors = graph.predecessors(vertex); // 获取该节点的所有前驱节点
int maxWeight = 0; // vertex节点中,“前驱顶点最早发生时间+边活动的消耗时间”的最大值
for (Object predecessor : predecessors) {
// weight = 前驱顶点最早发生时间+边活动的消耗时间
int weight = ve.get(predecessor) + graph.getEdgeWeight(predecessor, vertex);
if (weight > maxWeight) maxWeight = weight; // 从它所有的前驱节点中找出消耗时间最长的那个作为最早发生时间
}
ve.put(vertex, maxWeight);
}
vl.put(reverseTopoVertexes.get(0), ve.get(reverseTopoVertexes.get(0))); // 初始化结束顶点的最晚开始时间(等于它的最早开始时间)
for (int i = 1; i < reverseTopoVertexes.size(); i++) {
// 按逆拓扑排序,遍历各个顶点
Object vertex = reverseTopoVertexes.get(i);
Object[] neighbors = graph.neighbors(vertex); // 求出当前节点的所有后继节点
int latestStartTime = Integer.MAX_VALUE; // 用于存储当前节点的最小最晚开始时间
for (Object neighbor : neighbors) {
// 遍历当前节点的所有后继节点,找出最小的最晚开始时间
int delay = vl.get(neighbor) - graph.getEdgeWeight(vertex, neighbor); // 求出这条路径的最晚开始时间
if (delay < latestStartTime) latestStartTime = delay; // 如果它比最晚开始时间小,则更新最晚开始时间
}
vl.put(vertex, latestStartTime);
}
List<WeightedGraph.Edge> result = new ArrayList<>();
List<WeightedGraph.Edge> edges = graph.getEdges();
for (WeightedGraph.Edge edge : edges) {
// 遍历所有边,若该边的两个节点都满足“最晚开始时间=最早开始时间”,那么该边就是关键搞活动
if (vl.get(edge.vertex1) == ve.get(edge.vertex1)
&& vl.get(edge.vertex2) == ve.get(edge.vertex2
)) {
result.add(edge);
}
}
return result;
}