//顶点表节点
class Vertex {
firstEdge: string = ''//指向第一个邻接边的指针
data: any//顶点域
outNum: number = 0//在无向图中表示与顶点邻接的边的数量,在有向图中为初度
inNum: number = 0//在有向图中为顶点的入度
constructor(data: any) {
this.data = data;
}
}
//边表节点
class Edge {
data: any
nextEdge: any
weight: number
constructor(data: any, weight: number = 0) {
this.data = data; // 邻接点域
this.weight = weight; // 权重
}
}
//图结构
class Graph {
eNum: number = 0 //边的数目
adj: Array<any> = [] //顶点表
isDirect: boolean
constructor(isDirect: boolean) {
this.isDirect = isDirect;//是否是有向图
}
// 初始化顶点表
initVertex(verArr: Array<string>) {
for (let i = 0; i < verArr.length; i++) {
let newVer = new Vertex(verArr[i]);
this.adj[i] = newVer;
}
}
// 找到节点x在adj中所在的位置
// 前面加上下划线表示不应该在具体实例中调用该方法
_find(x: string) {
let pos = -1;
for (let i = 0; i < this.adj.length; i++) {
if (x === this.adj[i].data) {
pos = i;
}
}
return pos;
}
// 向图中插入新的顶点
insertVertex(x: string) {
let newVer = new Vertex(x);
this.adj.push(newVer);
}
// 与顶点x邻接的所有节点
allNeightbors(x: string) {
let pos = this._find(x);
let curEdge = this.adj[pos].firstEdge;
let arr = [];
while (curEdge) {
arr.push(curEdge.data);
curEdge = curEdge.nextEdge;
}
return arr;
}
// 向图中插入边(x, y)
addEdge(x: string, y: string, w: number = 0) {
let posX = this._find(x);
let posY = this._find(y);
let newEdgeX = new Edge(x, w);
let newEdgeY = new Edge(y, w);
if (!this.isDirect) {//如果是无向图
if (!this.hasEdge(x, y) && !this.hasEdge(y, x)) {//如果边不存在就继续执行]
if (posX > -1) {//当前点存在顶点表中
let curEdge = this.adj[posX].firstEdge;
if (!curEdge) {//如果当前没有顶节点
this.adj[posX].firstEdge = newEdgeY;
} else {
let len = this.adj[posX].outNum - 1;//获取当前顶点的边的数量
while (len--) {
curEdge = curEdge.nextEdge;
}
curEdge.nextEdge = newEdgeY;
}
this.adj[posX].outNum++;
}
if (posY > -1) { // 如果顶点y在顶点表中
let curEdge = this.adj[posY].firstEdge;
if (!curEdge) { // 如果当前顶点没有第一个边节点
this.adj[posY].firstEdge = newEdgeX;
this.adj[posY].outNum++;
} else {
let len = this.adj[posY].outNum - 1;
while (len--) {
curEdge = curEdge.nextEdge;
}
curEdge.nextEdge = newEdgeX;
this.adj[posY].outNum++;
}
}
this.eNum++;
}
} else {
// 如果是有向图则只需要插入边<x, y>即可
if (!this.hasEdge(x, y)) {
if (posX > -1) {
let curEdge = this.adj[posX].firstEdge;
if (!curEdge) {
this.adj[posX].firstEdge = newEdgeY;
} else {
let len = this.adj[posX].outNum - 1;
while (len--) {
curEdge = curEdge.nextEdge;
}
curEdge.nextEdge = newEdgeY;
}
this.adj[posX].outNum++;
this.eNum++;
}
if (posY > -1) {
let curVer = this.adj[posY];
curVer.inNum++;//顶点y的入度增加
}
}
}
}
// 在图中删除边(x, y)
removeEdge(x: string, y: string) {
let posX = this._find(x);
let posY = this._find(y);
if (!this.isDirect) {//如果是无向图
if (this.hasEdge(x, y) && this.hasEdge(y, x)) {
if (posX > -1) {
let curEdge = this.adj[posX].firstEdge;
let preEdge = null;
if (curEdge.data === y) {
this.adj[posX].firstEdge = curEdge.nextEdge;
this.adj[posX].outNum--;
curEdge.data = 0;
curEdge = null;
}
while (curEdge) {
preEdge = curEdge;
curEdge = curEdge.nextEdge;
if (curEdge && curEdge.data === y) {
preEdge.nextEdge = curEdge.nextEdge;
this.adj[posX].outNum--;
curEdge.data = 0;
curEdge = null;
}
}
}
if (posY > -1) {
let curEdge = this.adj[posY].firstEdge;
let preEdge = null;
if (curEdge.data === x) {
this.adj[posY].firstEdge = curEdge.nextEdge;
this.adj[posY].outNum--;
curEdge.data = 0;
curEdge = null;
}
while (curEdge) {
preEdge = curEdge;
curEdge = curEdge.nextEdge;
if (curEdge && curEdge.data === x) {
preEdge.nextEdge = curEdge.nextEdge;
this.adj[posY].outNum--;
curEdge.data = 0;
curEdge = null;
}
}
}
this.eNum--;
}
} else {
//有向图
if (this.hasEdge(x, y)) {
if (posX > -1) {
let curEdge = this.adj[posX].firstEdge;
let preEdge = null;
if (curEdge.data === y) {
this.adj[posX].firstEdge = curEdge.nextEdge;
curEdge.data = 0;
this.adj[posX].outNum--;
curEdge = null;
}
while (curEdge) {
preEdge = curEdge;
curEdge = curEdge.nextEdge;
if (curEdge.data == y) {
preEdge.nextEdge = curEdge.nextEdge;
curEdge.data = 0;
this.adj[posX].outNum--;
curEdge = null;
}
}
if (this.adj[posY]) {
this.adj[posY].inNum--;
}
}
this.eNum--;
}
}
}
// // 从图中删除顶点x
deleteVertex(x: string) {
let pos = this._find(x);
if (pos > -1) {
let curEdge = this.adj[pos].firstEdge;
//删除从x出发的边
while (curEdge) {
this.removeEdge(x, curEdge.data);
curEdge = curEdge.nextEdge;
}
//删除所有终点是x的边
for (let i = 0; i < this.adj.length; i++) {
let temVer = this.adj[i].firstEdge;
while (temVer) {
if (temVer.data === x) {
this.removeEdge(this.adj[i].data, temVer.data);
}
temVer = temVer.nextEdge;
}
}
this.adj.splice(pos, 1);
}
}
// 判断是否存在边(x,y)或者<x, y>
hasEdge(x: string, y: string) {
let pos = this._find(x);
if (pos > -1) {
let curEdge = this.adj[pos].firstEdge;
while (curEdge) {
if (curEdge.data == y) return true;
curEdge = curEdge.nextEdge;
}
return false;
}
}
// 获取图中所有的边
getAllEdge(): Array<any> {
let arr = new Array(this.adj.length).fill(0).map(item => new Array(this.adj.length).fill(0));
let j = 0;
for (let i = 0; i < this.adj.length; i++) {
let curEdge = this.adj[i].firstEdge;
while (curEdge) {
arr[j++] = curEdge;
curEdge = curEdge.nextEdge;
}
}
return arr;
}
// 获取边(x, y)或<x, y>对应的权值
getEdgeWeight(x: string, y: string) {
let pos = this._find(x);
if (pos > -1) {
let curEdge = this.adj[pos].firstEdge;
while (curEdge) {
if (curEdge.data === y) {
return curEdge.weight;
}
curEdge = curEdge.nextEdge;
}
return 0;
}
}
// 获得图中最大的权值
getMaxEdgeWeight() {
let i = 0;
let curVer = this.adj[i];
let max = 0;
while (curVer) {
let curEdge = curVer.firstEdge;
while (curEdge) {
if (curEdge.weight > max) {
max = curEdge.weight;
}
curEdge = curEdge.next;
}
curVer = this.adj[++i];
}
return max;
}
// 获得图中最小的权值
getMinEdgeWeight() {
let i = 0;
let curVer = this.adj[i];
let min = Number.MAX_SAFE_INTEGER;
while (curVer) {
let curEdge = curVer.firstEdge;
while (curEdge) {
if (curEdge.weight < min) {
min = curEdge.weight;
}
curEdge = curEdge.next;
}
curVer = this.adj[++i];
}
return min;
}
// 设置边(x, y)或<x, y>的权值
setEdgeWeight(x: string, y: string, w: number) {
}
// 广度优先遍历
/**
* 初始时,BFSTraverse()函数设置一个visited数组,将其全部赋值为false表示所有的节点都没有被访问过。然后使用_BFS()函数来求以顶点x为起点的连通分量,并将visited数组作为参数传入。
* 当_BFS()函数求出以顶点x为起点的连通分量后,BFSTraverse()函数则再次遍历visited数组,查看是否还有未被访问过的节点。如果还有,则再次调用_BFS()函数,并传入未被访问过的顶点和visited数组。等到所有的节点都被访问过后,返回结果。
*/
BFSTraverse(x: string = this.adj[0].data) {//x为广度优先遍历的起始顶点
let len = this.adj.length;
let visited = new Array(len).fill(false); // 访问标记数组,标记数组和顶点表唯一的联系就是下标
let result = '';
result = this._BFS(x, visited);
for (let i = 0; i < len; i++) {
if (!visited[i]) {//如果还存在未访问的元素则重新调用
result += `&${this._BFS(this.adj[i].data, visited)}`;
}
}
return result;
}
// 实际进行广度遍历的函数,每次遍历都是得到一个以顶点x为起点的连通分量
/**
* _BFS()函数中使用了两个循环,外层循环每次从队列中取出一个顶点,使用内层循环依次访问该顶点的边表中的所有节点。
* 内层循环不断的将当前起始节点的所有边表节点加入队列(只有在这些节点未被访问过时),当遍历完当前顶点的所有的边表节点后,从队列中取出一个节点再次开始循环,直到队列为空结束。
* 简而言之,_BFS()函数干的事情就是从顶点x出发,依次访问与x相邻的节点,然后再从这些相邻的节点挨个出发再访问各自的相邻节点。为了不重复访问同一个节点,使用visited数组做访问标识,如果发现要访问的节点已经被访问过了就跳过这个节点。
*/
_BFS(x: string, visited: Array<any>) {
let pos: any = this._find(x);
let result = '';
let queue = [];
if (pos > -1) {
result += `${x}`;
queue.push(pos);
visited[pos] = true;//设置标记数组对应的下标为true
while (queue.length) {
pos = queue.shift();//获得当前队列顶元素
let curVer = this.adj[pos].firstEdge;//获得对应顶点的第一条边
while (curVer) {
pos = this._find(curVer.data);
if (!visited[pos]) {
visited[pos] = true;
result += `->${this.adj[pos].data}`;
queue.push(pos);
}
curVer = curVer.nextEdge;
}
}
}
return result;
}
// 深度优先遍历
/**
*
* 初始时,DFSTraverse()函数设置一个visited数组,将其全部赋值为false表示所有的节点都没有被访问过。然后使用_DFS()函数来求以顶点x为起点的连通分量,并将visited数组作为参数传入。
* 当_DFS()函数求出以顶点x为起点的连通分量后,DFSTraverse()函数再次遍历visited数组,查看是否还有未被访问过的节点。如果还有,则再次调用_DFS()函数,并传入未被访问过的顶点和visited数组。等到所有的节点都被访问过后,返回结果。
*/
DFSTraverse(x: string) {
let len = this.adj.length;
let visited = new Array(len).fill(false);
let result = '';
result = this._DFS(x, visited);
for (let i = 0; i < this.adj.length; i++) {
if (!visited[i]) {//如果未访问过则继续遍历
result += this._DFS(this.adj[i].data, visited)
}
}
return result;
}
/**
* 1、如果发现这个节点的相邻节点中有未被访问过的,则访问这个节点,并将其压入堆栈,做已访问标识,再访问这个节点的一个相邻节点...
* 2、如果这个节点的所有相邻节点都被访问过了,而此时堆栈还不为空,就将其弹出去。再取堆栈的栈顶元素重复步骤1的操作,如果这个节点的所有相邻节点又全被访问过了,就再将其弹出去...,依此往复,直到堆栈为空结束。
*/
_DFS(x: string, visited: Array<any>) {
let result = '';
let stack = [];//辅助栈
let pos = this._find(x);
let curVer = this.adj[pos];//获得当前节点
if (pos > -1) {
stack.push(curVer);
result += `${x}`;
visited[pos] = true;//设置标记数组对应的节点为true
while (stack.length) {
curVer = stack[stack.length - 1]//获取栈顶元素
pos = this._find(curVer.data);
curVer = this.adj[pos].firstEdge;
while (curVer) {
pos = this._find(curVer.data);
if (visited[pos]) { //如果改节点已经访问过了,则访问该节点的下一个节点
curVer = curVer.nextEdge;
} else {
stack.push(curVer);
result += `->${curVer.data}`;
visited[pos] = true;
break;
}
}
if (!curVer) stack.pop();//如果所有的邻接节点都访问过
}
}
return result;
}
// 判断当前的图是否是连通图
// 任选一个顶点作为起点
isConnected(x = this.adj[0].data) {
let len = this.adj.length;
let visited = new Array(len).fill(0);
//广度遍历一遍 如果可以全部访问则是连通图
this._BFS(x, visited);
for (let i = 0; i < len; i++) {
if (!visited[i]) {
return false;
}
}
return true;
}
//最小生成树(普利姆算法)
getPrimMSTree() {
if (!this.isConnected) {//如果不是连通图则直接退出
return false;
}
let V = this.adj;//顶点集合
let Vt = [V[0]];//添加任意一个顶点
let VVt = V.filter(item => Vt.indexOf(item) === -1);//VVt = Vt - V;
let MSTree = new Graph(this.isDirect);//初始化空树
V.forEach(x => MSTree.insertVertex(x.data));//先将所有顶点放入树中
while (Vt.length != V.length) {
let mVt = null; //当找到权值最小的边时,mVTs是边的一个顶点
let mVVt = null; //当找到权值最小的边时,mV_VT是边的另一个顶点
let minW = Number.MAX_SAFE_INTEGER;
let i = Vt.length - 1;
for (let j = 0; j < VVt.length; j++) {
let weight = this.getEdgeWeight(Vt[i].data, VVt[j].data);
if (weight && minW > weight) {
minW = weight;
mVt = Vt[i];
mVVt = VVt[j];
}
}
Vt.push(mVVt);
MSTree.addEdge(mVt.data, mVVt.data, minW);
VVt = V.filter(x => Vt.indexOf(x) === -1);
}
return MSTree;
}
// 克鲁斯卡尔算法
// 算法的基本思想是先找权重最小的边,再找权重次小的边
/**
*
* 在算法中,新建一颗空树,并用给定的连通图中的顶点来初始化这颗树。一开始的时候,由于这个空树中只有顶点,所以在这棵空树中的连通分量就是顶点的数目,每次找到一条符合条件的权值最小的边加入这颗空树中后,该空树的连通分量就会减一,如果循环一直继续的话,随着边的不断加入,如果不加以控制的话,该最小生成树最后就会形成回路,需要在其形成回路前终止循环,也就是随着边的不断加入,该最小生成树中的连通分量在等于1之前结束循环。
* 在之前的算法描述中曾提到:如果找到的边加入最小生成树中不构成回路就保留,否则舍弃。那么如何在算法中实现这个过程呢?
* 在找到权值最小的边之后,假设这条边叫做(u, v),从顶点u开始对该尚未形成的最小生成树进行一次广度或者深度优先遍历,因为之前的遍历算法中,如果一张图中具有多个连通分量,那么对这张图进行遍历后的结果就会以 '&' 将每个连通分量隔开,因为是从顶点u开始遍历的,那么在遍历后的结果中,顶点u所在的连通分量一定在第一个 '&' 分隔符之前,所以算法中在遍历的结果中只取第一个 '&' 前面的字符串,如果发现顶点v不在该字符串当中,那就说明u和v属于树中不同的连通分量,可以放心大胆的将边(u,v)加入要生成的最小生成树当中而不必担心产生回路。而如果顶点u和顶点v属于同一个连通分量的话,就说明顶点u和顶点v之间已经具有路径了,此时如果再直接连接顶点u和顶点v则必然会产生回路,所以就应该舍弃这条边了。
*/
getKruskalMST() {
if (!this.isConnected()) {//如果不是连通图则无意义 直接退出
return;
}
let V = this.adj; // 顶点集V
let numS = V.length; // 树中的连通分量
let E = this.getAllEdge(); // 在E中存放图中所有的边
let mEdge: any = null;
let MSTree = new Graph(this.isDirect); // 初始化空树
V.forEach(x => MSTree.insertVertex(x.data)); // 树中只有顶点
while (numS > 1) {
let mWeight = Number.MAX_SAFE_INTEGER;
let j: any = 0;
// 从图中取出权值最小的边(u, v);
for (let i = 0; i < E.length; i++) {
if (E[i].weight < mWeight) {
mEdge = E[i];
j = i + 1;
mWeight = mEdge.weight;
}
}
j = j / this.adj.length;//获得当前的顶点下标
j = parseInt(j);
let result = MSTree.BFSTraverse(this.adj[j].data); // 广度优先遍历
result = result.split('&')[0]; // 只取&前面的字符串
let pos = result.indexOf(mEdge.data);
// 如果u和v属于树中不同的连通分量,就将此边加入生成树中
// 从顶点j遍历一遍发现没有当前节点,说明两个顶点不在一个连通分量之中
if (pos === -1) {
MSTree.addEdge(this.adj[j].data, mEdge.data, mEdge.weight);
numS--;
}
E = E.filter(x => x !== mEdge); // 去掉E中权值最小的边
}
return MSTree;
}
//获得图中权重之和
getSumOfWeight() {
// 当图不是连通的时候,获取权重之和没有意义
if (!this.isConnected()) return;
let sum = 0;
let vertex = this.adj;
if (!this.isDirect) { // 如果是无向图
for (let i = 0; i < vertex.length - 1; i++) {
for (let j = i; j < vertex.length; j++) {
let weight = this.getEdgeWeight(vertex[i].data, vertex[j].data);
if (weight) sum += weight;
}
}
} else {
for (let i = 0; i < vertex.length; i++) {
for (let j = 0; j < vertex.length; j++) {
let weight = this.getEdgeWeight(vertex[i].data, vertex[j].data);
if (weight) sum += weight;
}
}
}
return sum;
}
/**
* 集合S初始时为{V0},dist的初始值dist[i] = arcs[0][i],i 表示顶点Vi。0表示顶点V0。
* 从顶点集合V-S中选出一个顶点,假设为Vj,其满足dist[j] = Min {dist[i] | Vi∈V-S},Vj就是当前求得的V0到Vj的最短路径的终点,并令S = S ∪ { Vj }。
* 修改从V0出发到集合V-S上任意一个顶点Vk可到达的最短路径,V0初始时可能并不能直接到达顶点Vk,这时可以通过顶点 Vj 作为中转看看是否能够到达,或者通过Vj作为中转后到达的路径权值之后小于之前已有的数值,那么就可以做出一些修改了:如果dist[j] + arcs[j][k] < dist[k],则令dist[k] = dist[j] + arcs[j][k]。
* 重复步骤2和3,直到所有的顶点都包含在集合S中。
*/
// 求带权图顶点x到其他顶点的最短路径
getShortestPath(x: string) {
// 使用Dijkstra算法,
// 如果是无向图或者边有负的权值时退出
// 如果x不存在于图中时退出
// 如果从顶点x到不了图中任意一个顶点则退出
if (!this.isDirect
|| this.getMinEdgeWeight() < 0
|| this._find(x) === -1
|| !this.isConnected(x)) { return -1; }
let MAX = Number.MAX_SAFE_INTEGER;
// 初始化
let len = this.adj.length;
// 在dist数组中,dist[i]的初值为顶点x到顶点i之间的权值,
// x到i没有路径时,dist[i]记为无穷大
let dist = [];
let path = []; // path[i]表示顶点x到i的最短路径
let vers = []; // 顶点集
let exts = [x]; // 已找到最短路径的点的集合
// 初始化path和dist数组
for (let i = 0; i < len; i++) {
vers[i] = this.adj[i].data;
dist[i] = this.getEdgeWeight(x, vers[i]) || MAX;
if (dist[i] !== MAX) {
path[i] = `${x}->${vers[i]}`;
} else {
path[i] = '';
}
}
let rem = vers.filter(x => exts.indexOf(x) === -1); // 剩余的顶点
let n = 1;
while (n < len) {
// 在dist中寻找最小值
let min = MAX;
let idx = -1;
for (let i = 0; i < len; i++) {
if (min > dist[i]) {
min = dist[i];
idx = i;
}
}
let Vj = vers[idx]; // 直接找到Vj
dist[idx] = MAX;
exts.push(Vj);
rem = vers.filter(x => exts.indexOf(x) === -1);
console.log(path[idx]); // 输出最短路径
// 松弛工作
for (let i = 0; i < rem.length; i++) {
// Vj到其他节点的距离
let w = this.getEdgeWeight(Vj, rem[i]) || MAX;
let k = vers.indexOf(rem[i]);
if (w + min < dist[k]) {
dist[k] = w + min;
path[k] = `${path[idx]}->${rem[i]}`;
}
}
n++;
}
}
// 拓扑排序
/**
* 1,找出当前入度为0的顶点,将其入栈
* 2,删除顶点与之相关的连线,并改变其他顶点与之相连的入度
* 3.不断重复执行1,2操作,直到顶点数组为空,如果中途有一次1操作之后栈的长度为空则说明图有环路,返回false
*/
getTopoSort() {
if (this.isDirect) {
let adj = JSON.parse(JSON.stringify(this.adj));//深拷贝,防止改变原图数组
let stack: any = [];
let result = '';
let count = 0;
for (let i = 0; i < adj.length; i++) {
if (adj[i].inNum === 0) {
stack.push(i);
}
}
while (stack.length) {
let gettop = stack.pop();
result+=gettop+'->';
count++;
for (let e = adj[gettop].firstEdge; e; e = e.nextEdge) {
let k = this._find(e.data);
if(!(adj[k].inNum -= 1)) {
stack.push(k);
}
}
}
//图中有环
if(count != adj.length) {
return false;
}
return result;
}
}
// 获得Etv数组
topologicalSort() {
if (!this.isDirect) {
return;
}
let top = 0, count = 0;
let adj = JSON.parse(JSON.stringify(this.adj));
let gettop: any, k;
let result = '';//结果
let stack = [];
let stack2 = [];
let etv = new Array(adj.length).fill(0);
for (let i = 0; i < adj.length; i++) {
if (this.adj[i].inNum == 0) {
stack.push(i);
}
}
while (stack.length) {
gettop = stack.pop();
result += adj[gettop].data + '-> ';
count++;
stack2.push(gettop);
for (let e = adj[gettop].firstEdge; e; e = e.nextEdge) {
k = this._find(e.data);
if (!(adj[k].inNum -= 1)) {
stack.push(k);
}
if (etv[gettop] + e.weight > etv[k]) {
etv[k] = etv[gettop] + e.weight;
}
}
}
if (count < adj.length) {
console.info('发生错误,有环路存在');
return false;
}
return {
etv: etv,
stack: stack2
};
}
// 关键路径
getCriticalPath() {
let topological:any = this.topologicalSort();
let etv = topological.etv;//最早发生时间
let stack = topological.stack;
let adj = JSON.parse(JSON.stringify(this.adj));
console.info('可计算的最早发生时间数组etv:' + etv);
console.info('拓扑序列:' + stack);
let gettop, k;
let ltv = new Array();//最迟发生时间
for (let i = 0; i < this.adj.length; i++) {
ltv[i] = etv[this.adj.length - 1];
}
while (stack.length) {
gettop = stack.pop();
for (let e = adj[gettop].firstEdge; e; e = e.nextEdge) {
k = this._find(e.data);
if (ltv[k] - e.weight < ltv[gettop]) {
ltv[gettop] = ltv[k] - e.weight;
}
}
}
for (let j = 0; j <adj.length; j++) {
for (let e = adj[j].firstEdge; e; e = e.nextEdge) {
k = this._find(e.data);
if (etv[j] == ltv[k] - e.weight) {
console.info(adj[j].data + '到' + adj[k].data + '(' + e.weight + ')');
}
}
}
}
}
let arr2 = ['v0', 'v1', 'v2', 'v3', 'v4', 'v5', 'v6', 'v7', 'v8', 'v9'];
let myGraph = new Graph(true); // 1表示有向图
myGraph.initVertex(arr2);
myGraph.addEdge('v0', 'v2', 4);
myGraph.addEdge('v0', 'v1', 3);
myGraph.addEdge('v1', 'v4', 6);
myGraph.addEdge('v1', 'v3', 5);
myGraph.addEdge('v2', 'v5', 7);
myGraph.addEdge('v2', 'v3', 8);
myGraph.addEdge('v3', 'v4', 3);
myGraph.addEdge('v4', 'v7', 4);
myGraph.addEdge('v4', 'v6', 9);
myGraph.addEdge('v5', 'v7', 6);
myGraph.addEdge('v6', 'v9', 2);
myGraph.addEdge('v7', 'v8', 5);
myGraph.addEdge('v8', 'v9', 3);
myGraph.getCriticalPath();