`
若是人间
  • 浏览: 76238 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

java AOE网与关键路径

阅读更多

通常我们用有向图表示一个工程。在这种有向图中,用顶点表示活动,用有向边

<Vi,Vj>表示活动Vi必须先于活动Vj进行。如果在无有向环的带权有向图中用有向边表示一个工程中的各项活动 (ACTIVITY),用有向边上的权值表示活动的持续时间(DURATION),用顶点表示事件(EVENT),则这种的有向图叫做用边表示活动的网 络,简称AOE(active on edges)网络。

  AOE网络在某些工程估算方面非常有用。他可以使人们了解:

     (1):研究某个工程至少需要多少时间?

     (2):那些活动是影响工程进度的关键?

在AOE网络中,有些活动可以并行的进行。从源点到各个顶点,以至从源点到汇点的有向路径可能不止一条。这些路径的长度也可能不同。完成不同路径 的活动所需的时间虽然不同,但只有各条路径上所有活动都完成了,这个工程才算完成。因此,完成整个工程所需的时间取决于从源点到汇点的最长路径长度,即在 这条路径上所有活动的持续时间之和。这条路径长度就叫做关键路径(critical path)。

 

java代码实现:

 

Node类:

package com.javaeye.rsrt.regular;

/**
 * 
 * @author nishiting
 *
 */

public class Node {

	private Object data;

	private Node next;
	
	private int weight;
	
	/**
	 * 构造一个节点
	 * @param data	节点的数据,用来标识这个节点
	 * @param next	下一个节点
	 * @param weight	节点的权值,用来表示任务完成所要花费的时间
	 */
	Node(Object data, Node next,int weight) {
		this.data = data;
		this.next = next;
		this.weight = weight;
	}
	
	/**
	 * 
	 * @return 节点标识
	 */

	public Object getData() {
		return data;
	}
	
	/**
	 * 
	 * @param data 节点标识
	 */

	public void setData(Object data) {
		this.data = data;
	}
	
	/**
	 * 
	 * @return 下一个结点
	 */

	public Node getNext() {
		return next;
	}
	
	/**
	 * 
	 * @param next 下一个结点
	 */

	public void setNext(Node next) {
		this.next = next;
	}
	
	/**
	 * 
	 * @return  完成任务所要花费的时间
	 */

	public int getWeight() {
		return weight;
	}
	
	/**
	 * 
	 * @param weight 完成任务所要花费的时间
	 */

	public void setWeight(int weight) {
		this.weight = weight;
	}
	
	

}

 Link类:(用来存储图的链表)

package com.javaeye.rsrt.regular;

public class Link {

	private Node head;
	private int length;
	
	/**
	 * 构造函数,初始化链表
	 * @param index
	 */

	public Link(int index) {
		head = new Node(index, null, 0);
		length = 0;
	}
	
	/**
	 * 增加节点,每次都加在链表的头部
	 * @param item	节点标识
	 * @param weight	完成任务所需要的时间
	 */

	public void addhead(Object item, int weight) {
		Node node = new Node(item, null, weight);
		node.setNext(head.getNext());
		head.setNext(node);
		length++;
	}  
	
	/**
	 * 增加节点,每次都加在链表的尾部
	 * @param item	节点标识
	 * @param weight	完成任务所需的时间
	 */

	public void addtail(Object item, int weight) {
		Node node = new Node(item, null, weight);
		Node temp = head;
		while (null != temp.getNext()) {
			temp = temp.getNext();
		}
		temp.setNext(node);
		length++;
	}
	
	/**
	 * 
	 * @return 链表的头元素
	 */

	public Node head() {
		return head;
	}
	
	/**
	 * 找到指定位置的链表元素
	 * @param index	指定的位置
	 */

	public void find(int index) {
		if (index < 1 || index > length) {
			System.out.print(" 此位置空!");
		}
		Node temp = head;
		for (int i = 0; i < index; i++) {
			temp = temp.getNext();
		}
		System.out.println("链表中第" + index + "个位置的值为" + temp.getData());
	}
	
	/**
	 * 删除指定位置的链表元素
	 * @param index	指定的位置
	 */

	public void delindex(int index) {
		if (index < 1 || index > length) {
			System.out.print("位置不存在!");
		}
		Node temp = head;
		for (int i = 0; i < index - 1; i++) {
			temp = temp.getNext();
		}
		temp.setNext(temp.getNext().getNext());
		length--;

	}
	
	/**
	 * 打印链表的元素
	 */

	public void print() {
		Node temp = head;
		while (null != temp.getNext()) {
			System.out.println(temp.getNext().getData());
			temp = temp.getNext();
		}
		System.out.println("链表长度为:" + length);
	}

}

 Stack类:(栈)

package com.javaeye.rsrt.regular;

/**
 * 栈,遵循先进后出的原则,用来保存元素
 * 
 * @author nishiting
 * 
 */
public class Stack {

	private int[] st;
	private int top;
	private int count;

	/**
	 * 构造一个栈
	 * 
	 * @param size
	 *            栈的大小
	 */
	public Stack(int size) {
		st = new int[size];
		top = -1;
		count = 0;
	}

	/**
	 * 元素进栈
	 * 
	 * @param j
	 *            要进栈的元素
	 */

	public void push(int j) {
		count++;
		st[++top] = j;
	}

	/**
	 * 元素出栈
	 * 
	 * @return 出栈的元素
	 */

	public int pop() {

		return st[top--];
	}

	/**
	 * 查询栈顶元素
	 * 
	 * @return 栈顶元素
	 */

	public int peek() {
		return st[top];
	}

	/**
	 * 查询栈是否为空
	 * 
	 * @return 栈是否为空
	 */

	public boolean isEmpty() {
		count--;
		return (top == -1);
	}

	/**
	 * 查看栈里的所有元素
	 */

	public void list() {

		for (int i = 0; i < count; i++) {

			System.out.print(st[i] + "   ");

		}
		System.out.println();
	}

	/**
	 * 得到栈里一共有多少元素
	 * 
	 * @return 栈里的元素个数
	 */
	public int getCount() {
		return count;
	}

	/**
	 * 查看栈里是否包含某个元素
	 * 
	 * @param i
	 *            要查询的元素
	 * @return 是否包含了要查询的元素
	 */

	public boolean isContains(int i) {
		for (int k = 0; k < st.length; k++) {

			System.out.print("开始比较" + i + "此时的result:");
			list();
			System.out.println();
			if (st[k] == i) {
				return true;
			}
		}
		return false;
	}
	
	/**
	 * 得到栈里的元素集
	 * @return 栈里的元素集合
	 */
	public int[] getSt(){
		return st;
	}
	
	/**
	 * 返回指定位置的栈元素
	 * @param i	要取得的栈元素所在的位置
	 * @return 指定位置的栈元素
	 */
	public int getElement(int i){
		return st[i];
	}

}

 Vertex类:(顶点)

package com.javaeye.rsrt.regular;

public class Vertex {
	
	public  int in;
	private Object value;
	private boolean isVisited;
	Vertex(Object value){
		this.value = value;
	}
	
	Object value(){
		return value;
	}
	
	
	
}

 Graph类:

package com.javaeye.rsrt.regular;

public class Graph {

	private Vertex[] vertexs;// 节点数组,用来保存结点
	private Link[] adjTab;// 存取图链表
	private int pos = -1;
	private Stack stack;// 栈
	private Stack restack;// 栈的备份
	private Stack backstack;// 栈的备份
	private int vertexNum;// 结点数
	private Node start;
	private int edgeCount;// 记录边的个数
	private int kNum;// 记录关键活动的数量

	private int[] ve;// 事件的最早发生时间
	private int[] vl;// 事件的最迟发生时间
	private int[] e;// 活动的最早发生时间
	private int[] l;// 活动的最晚发生时间
	private int[] key;// 用来存储关键路径

	/**
	 * 构造函数,用来初始化一个图
	 * 
	 * @param size
	 *            图的结点数量
	 */

	public Graph(int size) {
		vertexNum = size;
		vertexs = new Vertex[size];
		adjTab = new Link[size];
		stack = new Stack(size);
		restack = new Stack(size);
		backstack = new Stack(size);
		for (int i = 0; i < size; i++) {
			adjTab[i] = new Link(i);
		}
		ve = new int[size];
		vl = new int[size];
		for (int d = 0; d < size; d++) {
			vl[d] = -1;
		}
		edgeCount = 0;
	}

	/**
	 * 增加结点
	 * 
	 * @param obj
	 *            结点的名称
	 */
	void add(Object obj) {
		/**
		 * 所添加的结点个数必须小于所申请的结点空间
		 */
		assert pos < vertexs.length;
		vertexs[++pos] = new Vertex(obj);
	}

	/**
	 * 增加任务之间的依赖关系和任务运行时间
	 * 
	 * @param from
	 *            被依赖的事件
	 * @param to
	 *            依赖的事件
	 * @param weight
	 *            从被依赖的事件到到依赖的事件之间所要花费的时间
	 */
	void addEdge(int from, int to, int weight) {

		adjTab[from].addtail(to, weight);
		vertexs[to].in++;
		edgeCount++;
	}

	/**
	 * 根据拓扑排序算法判断图是有环的存在
	 * 
	 * @return 是否有环的存在
	 */

	boolean topo() {

		int count = 0;// 用来记录拓扑排序时,没有入度的结点数是否小于节点数

		/**
		 * 扫描顶点表,将入度为0的顶点压栈
		 */

		for (int i = 0; i < vertexNum; i++) {
			if (vertexs[i].in == 0) {
				stack.push(i);
				start = adjTab[i].head();
			}
		}

		/**
		 * 如果栈不为空的话,则进行循环 退出栈顶元素,输出,累加器加1,将顶点的各个邻接点的入度减1,将新的入度为0的顶点入栈
		 * 
		 */

		while (!stack.isEmpty()) {
			/**
			 * 利用restack将栈的拓扑顺序进行备份
			 */
			restack.push(stack.peek());
			/**
			 * 取出栈顶元素,累加器加1
			 */
			int j = stack.pop();
			count++;
			Node p = adjTab[j].head();
			Node earlyest = p;

			/**
			 * 记录当前的事件最早发生时间
			 */
			int preweight = ve[j];

			/**
			 * 将与此节点有关的点的入度减1,若入度减为0,则入栈
			 */
			while (p != null) {
				int k = ((Integer) p.getData()).intValue();
				vertexs[k].in--;
				if (vertexs[k].in == 0)
					stack.push(k);

				/**
				 * 求事件的最早发生时间ve[k]
				 */

				p = p.getNext();
				if (p != null) {

					int temp = ((Integer) p.getData()).intValue();

					/**
					 * 判断新得到的事件最早发生时间是否比原先的事件最早发生时间长 公式:ve[1] = 0; ve[k] =
					 * max{ve[j]+len<vj,vk>}
					 */
					if (p.getWeight() + preweight > ve[temp]) {
						ve[temp] = p.getWeight() + preweight;
					}

				}
			}

		}

		/**
		 * 如果得到的节点数量比原先的结点少,则证明有回路存在
		 */

		if (count < vertexNum) {

			System.out.println("有回路,无法得到关键路径!");
			return false;

		}
		return true;

	}

	/**
	 * 计算出事件最早开始时间,事件最晚开始时间,活动最早开始时间,活动最晚开始时间
	 */

	public void calculate() {

		int s = 0;// 控制e(活动的最早发生时间)的增长
		int t = 0;// 控制l(活动的最迟发生时间)的增长

		/**
		 * 初始化活动的最早开始时间与最迟开始时间
		 */
		e = new int[edgeCount];
		l = new int[edgeCount];
		key = new int[edgeCount];

		/**
		 * 按逆拓扑有序来求其余各顶点的最迟发生时间
		 * 原理:restack里保存着拓扑排序的顺序数列,将从restack里将最后一个节点取出,即没有事件依赖于它的结点
		 * 取出的数放进backstack中,
		 * 再从restack里一个一个取出节点元素,看其是否跟已放入backstack中的元素相连,若有相连,则进行vl的计算,如此重复
		 * 计算公式:vl[k] = min{vl[j]-len<vk,vj>}
		 */
		backstack.push(restack.peek());
		int z = restack.pop();
		vl[z] = ve[z];

		/**
		 * 当记录原拓扑顺序的restack不为空时,则进行循环
		 */
		while (!restack.isEmpty()) {

			/**
			 * 将已经比较完的结点放进backstack中,然后将restack里取出来的值与backstack里的值一一对比
			 */
			backstack.push(restack.peek());
			int q = restack.pop();

			Node vertex = adjTab[q].head();

			for (int k = 0; k < backstack.getCount(); k++) {
				Node ver = vertex;
				while (ver.getNext() != null) {

					/**
					 *查询这个节点的下一个结点是否有backstack里的结点,从而证明两者是否相连
					 */
					if (((Integer) ver.getNext().getData()).intValue() == backstack
							.getElement(k)) {
						int yuanxian = vl[((Integer) vertex.getData())
								.intValue()];
						int jiangyao = vl[backstack.getElement(k)]
								- ver.getNext().getWeight();
						/**
						 *若两点之间相连的话,更新结点的vl值 计算vl的公式是:发vl[n] = ve[n] vl[k] =
						 * min{vl[j]-len<vk,vj>}
						 */
						if (jiangyao < yuanxian || yuanxian == -1) {
							vl[((Integer) vertex.getData()).intValue()] = vl[backstack
									.getElement(k)]
									- ver.getNext().getWeight();
						}

					}
					ver = ver.getNext();
				}

			}

		}

		/**
		 * 求活动的最早开始时间e[i]与活动的最晚开始时间l[i]
		 * 若活动ai是由弧<Vk,Vj>表示,根据AOE网的性质,只有事件vk发生了,活动ai才能开始
		 * 。也就是说,活动ai的最早开始时间应等于事件vk的最早发生时间。 因此有e[i] = ve[k];
		 * 活动ai的最晚开始时间是指,在不推迟整个工期的前提下
		 * ,ai必须开始的最晚开始时间。若ai由有向边<vi,vj>表示,则ai的最晚开始时间要保证事件vj的最迟发生时间不拖后
		 * 因此,应该有l[i] = vl[j] - len<vk,vj>
		 */

		for (int h = 0; h < vertexNum; h++) {

			Node begin = adjTab[h].head();
			Node backbegin = begin;
			if (begin != null) {
				/**
				 * 查看所有当前节点的下一个结点
				 */
				while (begin.getNext() != null) {
					e[s++] = ve[((Integer) backbegin.getData()).intValue()];
					l[t++] = vl[((Integer) begin.getNext().getData())
							.intValue()]
							- begin.getNext().getWeight();
					begin = begin.getNext();
				}

			}

		}

		kNum = 0;
		for (int w = 0; w < e.length; w++) {

			if (l[w] - e[w] <= 0) {
				key[kNum++] = w;
			}

		}
	}

	/**
	 * 
	 * @return 事件的最早开始时间
	 */
	public int[] getVE() {
		return ve;
	}

	/**
	 * 
	 * @return 事件的最迟开始时间
	 */
	public int[] getVl() {
		return vl;
	}

	/**
	 * @return 活动的最早开始时间
	 */
	public int[] getE() {
		return e;
	}

	/**
	 * 
	 * @return 活动的最晚开始时间
	 * 
	 */
	public int[] getL() {
		return l;
	}

	/**
	 * 
	 * @return 关键活动的点
	 */
	public int[] getKey() {
		return key;
	}

	/**
	 * 
	 * @return 关键活动的个数
	 */
	public int getKNum() {
		return kNum;
	}
}

 测试类:

package com.javaeye.rsrt.regular;

import com.javaeye.rsrt.regular.Graph;

import junit.framework.TestCase;

public class GraphTest extends TestCase {

	public void testGraph() {

		/**
		 * 测试没有回路的情况
		 * 
		 */
		System.out.println("<!--------测试没有回路的情况-------------->");
		Graph graph = new Graph(9);
		graph.add("a");
		graph.add("b");
		graph.add("c");
		graph.add("d");
		graph.add("e");
		graph.add("f");
		graph.add("g");
		graph.add("h");
		graph.add("i");
		graph.addEdge(0, 1, 6);
		graph.addEdge(0, 2, 4);
		graph.addEdge(0, 3, 5);
		graph.addEdge(1, 4, 1);
		graph.addEdge(2, 4, 1);
		graph.addEdge(3, 5, 2);
		graph.addEdge(4, 6, 9);
		graph.addEdge(4, 7, 7);
		graph.addEdge(5, 7, 4);
		graph.addEdge(6, 8, 2);
		graph.addEdge(7, 8, 4);
		if (graph.topo()) {
			graph.calculate();

			int[] e = graph.getE();
			int[] l = graph.getL();
			int[] key = graph.getKey();
			int[] ve = graph.getVE();
			int[] vl = graph.getVl();

			System.out.println("事件的最早发生时间:");
			for (int w = 0; w < ve.length; w++) {
				System.out.print(ve[w] + "      ");
			}
			System.out.println();

			System.out.println("事件的最晚发生时间:");
			for (int w = 0; w < vl.length; w++) {
				System.out.print(vl[w] + "      ");
			}
			System.out.println();

			System.out.println("活动的最早发生时间:");
			for (int w = 0; w < e.length; w++) {
				System.out.print(e[w] + "      ");
			}

			System.out.println();
			System.out.println("活动的最迟发生时间:");
			for (int w = 0; w < l.length; w++) {
				System.out.print(l[w] + "      ");
			}

			System.out.println();

			System.out.println("关键活动有:");
			for (int w = 0; w < graph.getKNum(); w++) {
				System.out.print(key[w] + "      ");
			}
			System.out.println();

			/**
			 * 测试有回路的情况
			 * 
			 */
			System.out.println("<!--------测试没有回路的情况-------------->");
			graph = new Graph(9);
			graph.add("a");
			graph.add("b");
			graph.add("c");
			graph.add("d");
			graph.add("e");
			graph.add("f");
			graph.add("g");
			graph.add("h");
			graph.add("i");
			graph.addEdge(0, 1, 6);
			graph.addEdge(2, 0, 4);
			graph.addEdge(0, 3, 5);
			graph.addEdge(1, 4, 1);
			graph.addEdge(4, 2, 1);
			graph.addEdge(3, 5, 2);
			graph.addEdge(4, 6, 9);
			graph.addEdge(4, 7, 7);
			graph.addEdge(5, 7, 4);
			graph.addEdge(6, 8, 2);
			graph.addEdge(7, 8, 4);

			if (graph.topo()) {
				graph.calculate();

				int[] e1 = graph.getE();
				int[] l1 = graph.getL();
				int[] key1 = graph.getKey();
				int[] ve1 = graph.getVE();
				int[] vl1 = graph.getVl();

				System.out.println("事件的最早发生时间:");
				for (int w = 0; w < ve.length; w++) {
					System.out.print(ve[w] + "      ");
				}
				System.out.println();

				System.out.println("事件的最晚发生时间:");
				for (int w = 0; w < vl.length; w++) {
					System.out.print(vl[w] + "      ");
				}
				System.out.println();

				System.out.println("活动的最早发生时间:");
				for (int w = 0; w < e.length; w++) {
					System.out.print(e[w] + "      ");
				}

				System.out.println();
				System.out.println("活动的最迟发生时间:");
				for (int w = 0; w < l.length; w++) {
					System.out.print(l[w] + "      ");
				}

				System.out.println();

				System.out.println("关键活动有:");
				for (int w = 0; w < graph.getKNum(); w++) {
					System.out.print(key[w] + "      ");
				}
			}

		}

	}

}
 
4
0
分享到:
评论

相关推荐

    Java 编写的AOE网络求关键路径

    Java编程实现AOE(Arc-Of-Event)网络来寻找关键路径是一种常见的项目管理和工程计划优化技术。AOE网络是一种图形表示法,用于表示任务之间的依赖关系和它们所需的时间,其中节点代表活动,边代表活动之间的顺序关系。...

    求AOE网络关键路径

    本压缩包文件包含了一个用C++编程语言实现的AOE网络关键路径求解器。C++是一种强类型、静态类型的编程语言,因其高效性和灵活性而常被用于系统级编程和大型软件开发。在C++中实现AOE网络的关键路径算法,可以提供...

    C语言求AOE网关键路径

    标题中的"C语言求AOE网关键路径"是指利用C语言编程来解决活动网络图(Activity On Edge, AOE)中的关键路径问题。关键路径是项目管理中的一个重要概念,它表示了从项目开始到结束的最长路径,决定了项目的最短完成...

    邻接表表示的AOE网与邻接矩阵表示的AOE网求解AOE网的关键路径方法

    在Windows7 64位+VS2015上运行求解AOE网关键路径的算法,邻接表表示的AOE网提示网中有回路,邻接矩阵表示的AOE网显示正确的信息?使用的算法是一样的,两种方法的相关类的接口函数也一致,为什么会出现这种问题?

    AoE算法求解关键路径

    传统AoE算法求解关键路径的C++代码实现

    关于AOE网中关键路径求解算法的研究

    ### 关于AOE网中关键路径求解算法的研究 #### 摘要 本文主要讨论了AOE网中关键路径的求解问题及其相关的几种算法。AOE网(Activity On Edge Network)是一种特殊的有向无环图,用来表示工程项目中的活动与依赖关系...

    教你轻松计算AOE网关键路径1

    AOE网的关键路径计算旨在找出决定项目总时长的路径,以便进行有效的资源分配和进度规划。 计算AOE网的关键路径主要涉及以下几个步骤: 1. **最早发生时间Ve(j)**: 从起点开始,向前遍历图中的所有顶点,计算每个...

    图论- AOE 网与关键路径.rar

    掌握图论中的AOE网与关键路径知识对于软件开发、系统分析、项目管理等领域的专业人士来说极为重要,它可以帮助他们更高效地规划和执行复杂项目,提高工作效率,减少不必要的延误。因此,深入理解和应用这些理论是...

    数据结构 AOE网关键路径

    在AOE网中,关键路径是从起点到终点的最长路径,所有非关键路径的延迟都不会影响项目的总完成时间,而关键路径上任何活动的延迟都将导致项目延期。 求解关键路径通常涉及以下步骤: 1. **拓扑排序**:首先,对AOE...

    AOE关键路径 邻接表存储

    C++数据结构 AOE CPP源文件 采用数据结构中的AOE方法对图的关键路径进行计算 :建立邻接表

    AOE网与关键路径 - Learn as if you were to live forever - 博客频道 - CSDN.N

    《AOE网与关键路径详解》 在项目管理和工程规划中,AOE网(Activity On Edge Network)是一种常用的工具,它以有向图的形式来表示一系列活动及其相互依赖关系。这种网络模型对于理解和优化工程进度至关重要,特别是...

    AOE网活动和关键路径

    在文档"AOE与关键路径.doc"中,可能详细阐述了AOE网的概念、计算方法以及关键路径的识别过程,并可能包含了一些实例分析和代码示例。 总的来说,AOE网分析和关键路径的计算是项目管理中非常重要的工具,它们可以...

    数据结构关键路径AOE网

    在这个项目中,我们将使用数据结构中的图来表示关键路径问题,并通过读取字符文件来构建AOE(Activity On Edge)网络,这是一种特殊的有向无环图(DAG),其中边表示活动,节点表示事件。 首先,我们需要理解AOE...

    AOE 求关键路径程序

    本程序实现了根据你所输入的数据建图,并利用AOE求出最长路径长度,并标明关键路径

    AOE-net.rar_AOE_AOE network_AOE关键活动_AOE网_关键路径

    该程序能实现的功能,若活动图有回路则无法计算出关键路径,即解决了判断工程的可行性问题。...对于输入的网,可以计算出每个活动的最早开始时间,最迟开始时间和全工程可以完成的最早时间,并找出关键路径和关键活动。

    基于AOE网络的关键路径方法研究1

    【基于AOE网络的关键路径方法研究】 在项目管理中,关键路径法(Critical Path Method, CPM)是一种重要的工具,用于确定项目中最长的活动序列,这些活动对项目的完成时间有直接影响。在传统CPM中,活动之间的依赖...

    AOE图中关键路径的MATLAB实现.doc

    AOE 图中关键路径的 MATLAB 实现 AOE 图(Activity on Edge)是一种常用的网络图形式,用于描述复杂的工程项目中各个活动之间的关系。AOE 图中关键路径的计算是项目管理中一个非常重要的问题。 MATLAB 是一个功能...

    AOE 求关键路径 程序

    本程序用c#编写,实现了建图,利用AOE实现求解关键路径问题,并标明那些路径为关键路径

    用C/C++写的求关键路径AOE

    程序+报告+说明 功能: 0 (创建一个工程) 1 (从文本导入一个工程) 2 (用邻接表输出工程及输出工程的关键路径)

    java做的关于AOE网络的课程设计

    在IT行业中,AOE(Activity On Edge)网络是一种用于项目管理的图形表示法,它源自关键路径方法(CPM)。AOE网络通过图形化展示任务之间的依赖关系,帮助项目经理优化资源分配,确定项目的最短完成时间。在这个Java...

Global site tag (gtag.js) - Google Analytics